mirror of
https://github.com/RealKai42/qwerty-learner.git
synced 2024-11-25 16:22:53 +08:00
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:
parent
ccda622214
commit
b47fd0a8c2
35
.github/workflows/e2e.yml
vendored
Normal file
35
.github/workflows/e2e.yml
vendored
Normal 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
6
.gitignore
vendored
@ -29,4 +29,8 @@ pnpm-lock.yaml
|
||||
.env
|
||||
stats.html
|
||||
|
||||
.idea
|
||||
.idea
|
||||
/test-results/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
playwright-report
|
@ -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
78
playwright.config.ts
Normal 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,
|
||||
// },
|
||||
})
|
90
tests/e2e/dictionary.spec.ts
Normal file
90
tests/e2e/dictionary.spec.ts
Normal 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
15
tests/e2e/main.spec.ts
Normal 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();
|
||||
// });
|
||||
})
|
26
tests/e2e/practice-list.spec.ts
Normal file
26
tests/e2e/practice-list.spec.ts
Normal 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
121
tests/e2e/practice.spec.ts
Normal 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
24
tests/e2e/theme.spec.ts
Normal 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('')
|
||||
})
|
||||
})
|
23
yarn.lock
23
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user