test: Add e2e tests with playwright for website (#736)

* docs: update README, add deploy to vercel

* docs: update README

* feat: add e2e tests

* feat: add main feature e2e tests with playwright

* fix: ci env value and update gitignore

* fix: ci job conditions for execution

* chore: merge from main

---------

Co-authored-by: KaiyiWing <Zhang.kaiyi42@gmail.com>
This commit is contained in:
rubickecho 2024-02-17 16:04:59 +08:00 committed by GitHub
parent ccda622214
commit b47fd0a8c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 418 additions and 2 deletions

35
.github/workflows/e2e.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Qwerty E2E Workflows
on:
push:
branches:
- master
- dev/e2e
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Cache node modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm i
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run all workflows
run: npm run test:e2e || echo "status=failure" >> $GITHUB_ENV
continue-on-error: true
- uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Check workflows status
run: if [ "$status" = "failure" ]; then exit 1; fi

6
.gitignore vendored
View File

@ -29,4 +29,8 @@ pnpm-lock.yaml
.env
stats.html
.idea
.idea
/test-results/
/blob-report/
/playwright/.cache/
playwright-report

View File

@ -57,6 +57,7 @@
"start": "vite",
"build": "cross-env CI=false vite build --base=./",
"test": "echo \"No tests\"",
"test:e2e": "playwright test",
"lint": "eslint .",
"prettier": "prettier --write .",
"prepare": "husky install"
@ -80,6 +81,7 @@
},
"devDependencies": {
"@iconify/json": "^2.2.56",
"@playwright/test": "^1.40.1",
"@svgr/core": "^7.0.0",
"@svgr/plugin-jsx": "^7.0.0",
"@tailwindcss/forms": "^0.5.3",

78
playwright.config.ts Normal file
View File

@ -0,0 +1,78 @@
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests/e2e',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [['list'], ['html']],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'https://qwerty.kaiyi.cool',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
timeout: 30 * 1000, // default 30s
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
})

View File

@ -0,0 +1,90 @@
import { test, expect } from '@playwright/test'
test.describe('Dictionary manage', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.getByLabel('关闭提示').click()
})
test('Homepage default dictionary', async ({ page }) => {
await expect(await page.getByText('CET-4').isVisible()).toBeTruthy()
await page.getByText('CET-4').hover()
await expect(await page.getByText('词典切换').isVisible()).toBeTruthy()
})
test('Switch language', async ({ page }) => {
await page.getByText('CET-4').click()
await page.waitForURL('**/gallery')
await expect(await page.getByRole('radio', { name: /^英语$/ }).getAttribute('aria-checked')).toBeTruthy()
await page.getByRole('radio', { name: /^日语$/ }).click()
await expect(await page.getByRole('radio', { name: /^日语$/ }).getAttribute('aria-checked')).toBeTruthy()
await expect(
await page
.getByRole('button', { name: /日语常见词/g })
.first()
.isVisible(),
).toBeTruthy()
await page.getByRole('radio', { name: /^Code$/ }).click()
await expect(await page.getByRole('radio', { name: /^Code$/ }).getAttribute('aria-checked')).toBeTruthy()
await expect(
await page
.getByRole('button', { name: /Coder Dict/g })
.first()
.isVisible(),
).toBeTruthy()
})
test('Switch category', async ({ page }) => {
await page.getByText('CET-4').click()
await page.waitForURL('**/gallery')
await expect(await page.getByRole('radio', { name: /^大学英语$/ }).getAttribute('aria-checked')).toBeTruthy()
await page.getByRole('radio', { name: /^考研$/ }).click()
await expect(await page.getByRole('radio', { name: /^考研$/ }).getAttribute('aria-checked')).toBeTruthy()
await expect(await page.getByRole('button', { name: /考研/g }).first().isVisible()).toBeTruthy()
await page.getByRole('radio', { name: /^GRE$/ }).click()
await expect(await page.getByRole('radio', { name: /^GRE$/ }).getAttribute('aria-checked')).toBeTruthy()
await expect(await page.getByRole('button', { name: /GRE/g }).first().isVisible()).toBeTruthy()
})
test('Switch dictionary', async ({ page }) => {
await page.getByText('CET-4').click()
await page.waitForURL('**/gallery')
await page
.getByRole('button', { name: /六级巧记速记/g })
.first()
.click()
await page.getByRole('heading', { name: '第 2 章' }).click()
await page.waitForURL('**/')
await expect(await page.getByRole('button', { name: '第 2 章' }).first().isVisible()).toBeTruthy()
})
test('Close dictionary settings', async ({ page }) => {
await page.getByText('CET-4').click()
await page.waitForURL('**/gallery')
// should use testId
await page.locator('main > div > svg').first().click()
await page.waitForURL('**/')
await expect(await page.getByText('Start').first().isVisible()).toBeTruthy()
})
test('Switch dictionary chapter', async ({ page }) => {
await page.getByText('第 1 章').first().hover()
await expect(await page.getByText('章节切换').isVisible()).toBeTruthy()
await page.getByText('第 1 章').click()
await page.getByRole('option', { name: '第 2 章' }).click()
await page.getByText('第 2 章').first().hover()
await expect(await page.getByText('章节切换').isVisible()).toBeTruthy()
})
})

15
tests/e2e/main.spec.ts Normal file
View File

@ -0,0 +1,15 @@
import { test, expect } from '@playwright/test'
test.describe('Main page', () => {
test('has title', async ({ page }) => {
await page.goto('/')
await expect(await page.locator('h1').getByText('Qwerty Learner').isVisible()).toBeTruthy()
})
// you should run 'yarn update:snapshots' before this test, create base snapshots for visual comparison
// test('visual comparison', async ({ page }) => {
// await page.goto('/');
// await expect(page).toHaveScreenshot();
// });
})

View File

@ -0,0 +1,26 @@
import { test, expect, Page } from '@playwright/test'
test.describe('Practice List', () => {
test.beforeEach(async ({ page }) => {
test.slow()
await page.goto('/')
await page.getByLabel('关闭提示').click()
await page.waitForTimeout(1000)
// should use testId
await page.locator('#root > div').locator('.fixed').locator('svg').last().click()
await page.waitForTimeout(1000)
})
test('Practice list button click to open the list ', async ({ page }) => {
await expect(await page.getByRole('heading', { name: 'CET-4 第 1 章' }).isVisible()).toBeTruthy()
// should use testId
await page.locator('#headlessui-portal-root').locator('div > p', { hasText: 'cancel' }).click()
})
test('Close practice list', async ({ page }) => {
await page.getByRole('heading', { name: 'CET-4 第 1 章' }).getByRole('img').click()
await page.waitForTimeout(1000)
await expect(await page.locator('h3', { hasText: 'CET-4 第 1 章' }).isVisible()).not.toBeTruthy()
})
})

121
tests/e2e/practice.spec.ts Normal file
View File

@ -0,0 +1,121 @@
import { test, expect, Page } from '@playwright/test'
const pressWord = async (page: Page, word: string) => {
const letters = word.split('')
for (const letter of letters) {
await page.keyboard.press(letter)
}
}
const pressWords = async (page: Page, words: string[]) => {
for (const word of words) {
await pressWord(page, word)
await page.waitForTimeout(300)
}
}
test.describe('Practice', () => {
test.beforeEach(async ({ page }) => {
test.slow()
await page.goto('/')
await page.getByLabel('关闭提示').click()
})
test('Press any key to start', async ({ page }) => {
await expect(await page.getByText('按任意键开始').isVisible()).toBeTruthy()
await page.keyboard.press('Enter')
await page.waitForTimeout(300)
await expect(await page.locator('p').getByText('按任意键开始').isHidden()).toBeTruthy()
})
test('Enter the correct word', async ({ page }) => {
await page.keyboard.press('Enter')
await pressWord(page, 'cancel')
await page.locator('div', { hasText: '正确率' }).locator('span', { hasText: '100' }).click()
// auto show next word: explosive
await expect(await page.locator('span', { hasText: /^e$/ }).first().isVisible()).toBeTruthy()
await expect(await page.locator('span', { hasText: /^x$/ }).isVisible()).toBeTruthy()
await expect(await page.locator('span', { hasText: /^p$/ }).isVisible()).toBeTruthy()
await expect(await page.locator('span', { hasText: /^l$/ }).isVisible()).toBeTruthy()
await expect(await page.locator('span', { hasText: /^o$/ }).isVisible()).toBeTruthy()
await expect(await page.locator('span', { hasText: /^s$/ }).isVisible()).toBeTruthy()
await expect(await page.locator('span', { hasText: /^i$/ }).isVisible()).toBeTruthy()
await expect(await page.locator('span', { hasText: /^v$/ }).isVisible()).toBeTruthy()
await expect(await page.locator('span', { hasText: /^e$/ }).last().isVisible()).toBeTruthy()
})
test('Enter the wrong word', async ({ page }) => {
await page.keyboard.press('Enter')
await pressWord(page, 'canca')
await page.locator('div', { hasText: '输入数' }).locator('span', { hasText: /^5$/ }).first().click()
await page.locator('div', { hasText: '正确数' }).locator('span', { hasText: /^4$/ }).first().click()
await page.waitForTimeout(500)
await page.locator('div', { hasText: '正确率' }).locator('span', { hasText: /^80$/ }).click()
})
test('Enter the correct letter, should show green color', async ({ page }) => {
await page.keyboard.press('Enter')
await page.keyboard.press('c')
await expect(page.locator('span', { hasText: /^c$/ }).first()).toHaveClass(/text-green-600/)
await page.keyboard.press('a')
await expect(page.locator('span', { hasText: /^a$/ })).toHaveClass(/text-green-600/)
await page.keyboard.press('n')
await expect(page.locator('span', { hasText: /^n$/ })).toHaveClass(/text-green-600/)
})
test('Enter the wrong letter, should show red color', async ({ page }) => {
await page.keyboard.press('Enter')
await expect(page.locator('span', { hasText: /^c$/ }).first()).toHaveClass(/text-gray-600/)
await page.keyboard.press('a')
await expect(page.locator('span', { hasText: /^c$/ }).first()).toHaveClass(/text-red-600/)
await page.waitForTimeout(500)
await expect(page.locator('span', { hasText: /^c$/ }).first()).toHaveClass(/text-gray-600/)
})
test('Complete the exercises for 1 chapter, enter the next chapter', async ({ page }) => {
await page.keyboard.press('Enter')
const chapter1 = [
'cancel',
'explosive',
'numerous',
'govern',
'analyse',
'discourage',
'resemble',
'remote',
'salary',
'pollution',
'pretend',
'kettle',
'wreck',
'drunk',
'calculate',
'persistent',
'sake',
'conceal',
'audience',
'meanwhile',
]
await pressWords(page, chapter1)
await expect(await page.getByText('100%').isVisible()).toBeTruthy
await expect(await page.getByText('表现不错!全对了!').isVisible()).toBeTruthy()
await page.getByRole('button', { name: '下一章节' }).click()
await expect(await page.getByText('第 2 章').first().isVisible()).toBeTruthy()
})
})

24
tests/e2e/theme.spec.ts Normal file
View File

@ -0,0 +1,24 @@
import { test, expect } from '@playwright/test'
test.describe('Theme switch', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.getByLabel('关闭提示').click()
})
test('Has theme switch button', async ({ page }) => {
await expect(await page.getByLabel('开关深色模式').isVisible()).toBeTruthy()
})
test('Theme mode switch', async ({ page }) => {
// light to dark
await page.getByLabel('开关深色模式').click()
await expect(await page.locator('html').getAttribute('class')).toBe('dark')
await page.waitForTimeout(500)
// dark to light
await page.getByLabel('开关深色模式').click()
await expect(await page.locator('html').getAttribute('class')).toBe('')
})
})

View File

@ -1460,6 +1460,13 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@playwright/test@^1.40.1":
version "1.40.1"
resolved "https://registry.npmmirror.com/@playwright/test/-/test-1.40.1.tgz#9e66322d97b1d74b9f8718bacab15080f24cde65"
integrity sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==
dependencies:
playwright "1.40.1"
"@radix-ui/number@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/number/-/number-1.0.0.tgz"
@ -3734,7 +3741,7 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@~2.3.2:
fsevents@2.3.2, fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
@ -4969,6 +4976,20 @@ pirates@^4.0.1:
resolved "https://registry.npmmirror.com/pirates/-/pirates-4.0.5.tgz"
integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==
playwright-core@1.40.1:
version "1.40.1"
resolved "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.40.1.tgz#442d15e86866a87d90d07af528e0afabe4c75c05"
integrity sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==
playwright@1.40.1:
version "1.40.1"
resolved "https://registry.npmmirror.com/playwright/-/playwright-1.40.1.tgz#a11bf8dca15be5a194851dbbf3df235b9f53d7ae"
integrity sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==
dependencies:
playwright-core "1.40.1"
optionalDependencies:
fsevents "2.3.2"
postcss-functions@^3:
version "3.0.0"
resolved "https://registry.npmmirror.com/postcss-functions/-/postcss-functions-3.0.0.tgz"