diff --git a/.gitignore b/.gitignore index 89b035d0..446213b1 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,8 @@ node_modules /coverage/ playwright/.auth /actions-runner/ -nohup.out \ No newline at end of file +nohup.out +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/package-lock.json b/package-lock.json index 285386af..ed289b37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,13 +34,14 @@ "winston-daily-rotate-file": "^4.7.1" }, "devDependencies": { - "@playwright/test": "^1.38.0", + "@playwright/test": "^1.40.0", "@sveltejs/adapter-node": "^1.3.1", "@sveltejs/kit": "^1.25.0", "@types/express": "^4.17.17", "@types/glob": "^8.1.0", "@types/imap-simple": "^4.2.6", "@types/morgan": "^1.9.5", + "@types/node": "^20.9.3", "@types/nodemailer": "^6.4.10", "@types/nprogress": "^0.2.0", "@types/prettier": "^2.7.3", @@ -2732,12 +2733,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.0.tgz", - "integrity": "sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.0.tgz", + "integrity": "sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==", "dev": true, "dependencies": { - "playwright": "1.38.0" + "playwright": "1.40.0" }, "bin": { "playwright": "cli.js" @@ -3306,9 +3307,12 @@ } }, "node_modules/@types/node": { - "version": "20.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz", - "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==" + "version": "20.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.3.tgz", + "integrity": "sha512-nk5wXLAXGBKfrhLB0cyHGbSqopS+nz0BUgZkUQqSHSSgdee0kssp1IAqlQOu333bW+gMNs2QREx7iynm19Abxw==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/nodemailer": { "version": "6.4.10", @@ -9192,12 +9196,12 @@ } }, "node_modules/playwright": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz", - "integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.0.tgz", + "integrity": "sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==", "dev": true, "dependencies": { - "playwright-core": "1.38.0" + "playwright-core": "1.40.0" }, "bin": { "playwright": "cli.js" @@ -9210,9 +9214,9 @@ } }, "node_modules/playwright-core": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz", - "integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.0.tgz", + "integrity": "sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -12753,6 +12757,11 @@ "node": ">=8" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", diff --git a/package.json b/package.json index 3106a44a..5eaa0460 100644 --- a/package.json +++ b/package.json @@ -27,13 +27,14 @@ "seed": "tsx prisma/seed.ts" }, "devDependencies": { - "@playwright/test": "^1.38.0", + "@playwright/test": "^1.40.0", "@sveltejs/adapter-node": "^1.3.1", "@sveltejs/kit": "^1.25.0", "@types/express": "^4.17.17", "@types/glob": "^8.1.0", "@types/imap-simple": "^4.2.6", "@types/morgan": "^1.9.5", + "@types/node": "^20.9.3", "@types/nodemailer": "^6.4.10", "@types/nprogress": "^0.2.0", "@types/prettier": "^2.7.3", diff --git a/playwright.config.ts b/playwright.config.ts index 9efb16f3..a4048c12 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,67 +1,25 @@ -import type { PlaywrightTestConfig } from '@playwright/test' -import { devices } from '@playwright/test' -import dotenv from 'dotenv' +import { PlaywrightTestConfig, devices } from '@playwright/test' -dotenv.config() - -const base = '' -// const base = '/talk' - -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// require('dotenv').config(); - -/** - * See https://playwright.dev/docs/test-configuration. - */ const config: PlaywrightTestConfig = { - testDir: './e2e', - /* Maximum time one test can run for. */ - timeout: process.env.CI ? 20 * 1000 : 10 * 1000, - expect: { - /** - * Maximum time expect() should wait for the condition to be met. - * For example in `await expect(locator).toHaveText();` - */ - timeout: 3000, + webServer: { + command: 'npm run dev', + port: 5173, + reuseExistingServer: !process.env.CI, }, - /* 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: 0, - retries: process.env.CI ? 1 : 0, - /* Opt out of parallel tests on CI. */ - // workers: undefined, - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [['html', { open: 'never' }]], - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + // webServer: { + // command: 'npm run build && npm run preview', + // port: 4173, + // }, + testDir: 'tests', + testMatch: /(.+\.)?(test|spec)\.[jt]s/, + + reporter: 'html', use: { - baseURL: `http://127.0.0.1:5173${base}/`, - - /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 0, - /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://localhost:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - - // headless: false, video: 'retain-on-failure', - - contextOptions: { - permissions: ['clipboard-read', 'clipboard-write', 'accessibility-events'], - }, }, - /* Configure projects for major browsers */ projects: [ - { name: 'setup', testMatch: /[^.]{1,100}\.setup\.ts/ }, + { name: 'setup', testMatch: /.*\.setup\.ts/ }, { name: 'chromium', @@ -70,65 +28,6 @@ const config: PlaywrightTestConfig = { }, dependencies: ['setup'], }, - - // { - // 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: { - // channel: 'msedge', - // }, - // }, - // { - // name: 'Google Chrome', - // use: { - // channel: 'chrome', - // }, - // }, - ], - - /* Folder for test artifacts such as screenshots, videos, traces, etc. */ - // outputDir: 'test-results/', - - /* Run your local dev server before starting the tests */ - webServer: [ - { - command: 'npm run dev', - url: `http://127.0.0.1:5173${base}/`, - reuseExistingServer: !process.env.CI, - }, - // { - // command: 'npm run build && npm run preview', - // port: 4173, - // reuseExistingServer: !process.env.CI, - // }, ], } diff --git a/e2e/api/log.spec.ts b/tests/api/log.spec.ts similarity index 100% rename from e2e/api/log.spec.ts rename to tests/api/log.spec.ts diff --git a/e2e/auth.setup.ts b/tests/auth.setup.ts similarity index 61% rename from e2e/auth.setup.ts rename to tests/auth.setup.ts index 9a363f50..79817b7a 100644 --- a/e2e/auth.setup.ts +++ b/tests/auth.setup.ts @@ -1,27 +1,7 @@ import { expect, test as setup } from '@playwright/test' import { PrismaClient } from '@prisma/client' -// import { promises as fs } from 'fs' -// import { sleep } from '../src/lib/general/system.js' -// import { get_pin_code_from_mail } from './lib/get_pin_code_from_mail.js' import { auth_file_path } from './lib/setup.js' -// async function find_auth_file(): Promise { -// try { -// await fs.access(auth_file_path, fs.constants.F_OK) -// return true -// } catch { -// return false -// } -// } - -// async function get_pin_code_from_mail(): Promise { -// await sleep(process.env.CI ? 5000 : 1000) -// const pin_code = await get_pin_code_from_mail() -// expect(pin_code).toMatch(/^\d{6}$/) - -// return pin_code -// } - async function get_pin_code_from_database(gmail_user: string): Promise { const prisma_client = new PrismaClient() @@ -36,13 +16,11 @@ async function get_pin_code_from_database(gmail_user: string): Promise { } setup('sign in', async ({ page }) => { - // if (await find_auth_file()) return + if (process.env['CI']) return await page.goto('./sign-in', { waitUntil: 'networkidle' }) - const gmail_user = process.env.GMAIL_USER ?? '' - - // expect(gmail_user).toEqual('iam.o.sin@gmail.com') + const gmail_user = process.env['GMAIL_USER'] ?? '' await page.getByPlaceholder('Enter email').fill(gmail_user) await page.getByRole('button', { name: 'Continue' }).click() diff --git a/e2e/chat.spec.ts b/tests/chat.spec.ts similarity index 60% rename from e2e/chat.spec.ts rename to tests/chat.spec.ts index a29edc93..cd784c0f 100644 --- a/e2e/chat.spec.ts +++ b/tests/chat.spec.ts @@ -1,4 +1,5 @@ -import { Page, expect, test } from '@playwright/test' +import { expect, test } from '@playwright/test' +import type { Page } from '@playwright/test' import { auth_file_path } from './lib/setup.js' test.beforeEach(async ({ page }) => { @@ -10,6 +11,7 @@ test('before sign in', async ({ page }) => { }) test.describe('after sign in', () => { + if (process.env['CI']) return test.use({ storageState: auth_file_path }) test('has title', async ({ page }) => { @@ -69,7 +71,7 @@ test.describe('after sign in', () => { const room_id_div = page.getByTestId('room-id') const room_id = await room_id_div.innerText() - await expect(room_id).not.toContain('lobby') + expect(room_id).not.toContain('lobby') await expect(page).toHaveURL(`./chat/${room_id}`) }) @@ -96,31 +98,58 @@ test.describe('after sign in', () => { return true } - test('send message', async ({ page }) => { - const input = 'Hello World!' - const output = 'Hello World!' + // test('send message', async ({ page }) => { + // const input = 'Hello World!' + // const output = 'Hello World!' - await test_send(page, input, output) - }) + // await test_send(page, input, output) + // }) - test('trim message', async ({ page }) => { - const input = '\nHello World!\n' - const output = 'Hello World!' + // test('trim message', async ({ page }) => { + // const input = '\nHello World!\n' + // const output = 'Hello World!' - await test_send(page, input, output) - }) + // await test_send(page, input, output) + // }) - test('indent message', async ({ page }) => { - const input = 'Hello World!\nHello World!' - const output = 'Hello World!\nHello World!' + // test('indent message', async ({ page }) => { + // const input = 'Hello World!\nHello World!' + // const output = 'Hello World!\nHello World!' - await test_send(page, input, output) - }) + // await test_send(page, input, output) + // }) - test('excess indentions', async ({ page }) => { - const input = 'Hello World!\n\n\n\nHello World!' - const output = 'Hello World!\n\nHello World!' + // test('excess indentions', async ({ page }) => { + // const input = 'Hello World!\n\n\n\nHello World!' + // const output = 'Hello World!\n\nHello World!' + + // await test_send(page, input, output) + // }) + + type Spec = { + description: string + input: string + output: string + } - await test_send(page, input, output) + const specs: Spec[] = [ + { description: 'send message', input: 'Hello World!', output: 'Hello World!' }, + { description: 'trim message', input: '\nHello World!\n', output: 'Hello World!' }, + { + description: 'indent message', + input: 'Hello World!\nHello World!', + output: 'Hello World!\nHello World!', + }, + { + description: 'excess indentions', + input: 'Hello World!\n\n\n\nHello World!', + output: 'Hello World!\n\nHello World!', + }, + ] + + specs.forEach(({ description, input, output }) => { + test(description, async ({ page }) => { + await test_send(page, input, output) + }) }) }) diff --git a/e2e/example.spec.ts b/tests/example.spec.ts similarity index 100% rename from e2e/example.spec.ts rename to tests/example.spec.ts diff --git a/e2e/learn.spec.ts b/tests/learn.spec.ts similarity index 78% rename from e2e/learn.spec.ts rename to tests/learn.spec.ts index 9a631cc4..1cdbfd30 100644 --- a/e2e/learn.spec.ts +++ b/tests/learn.spec.ts @@ -1,19 +1,25 @@ import { test, expect } from '@playwright/test' +import type { Page } from '@playwright/test' import { auth_file_path } from './lib/setup.js' +async function run_test(page: Page, title: string): Promise { + await expect(page).toHaveTitle(title) +} + test.beforeEach(async ({ page }) => { await page.goto('./learn', { waitUntil: 'networkidle' }) }) test('before sign in', async ({ page }) => { - await expect(page).toHaveTitle('Sign in - Talk') + await run_test(page, 'Sign in - Talk') }) test.describe('after sign in', () => { + if (process.env['CI']) return test.use({ storageState: auth_file_path }) test('has title', async ({ page }) => { - await expect(page).toHaveTitle('Learn - Talk') + await run_test(page, 'Learn - Talk') }) // test('sign in button', async ({ page }) => { @@ -22,13 +28,22 @@ test.describe('after sign in', () => { // }) test('from locale combo box', async ({ page }) => { - await expect(page.getByRole('combobox').nth(0)).toHaveValue('en-US') + await check_combo_box_value(page, 0, 'en-US') }) test('to locale combo box', async ({ page }) => { - await expect(page.getByRole('combobox').nth(1)).toHaveValue('ja-JP') + await check_combo_box_value(page, 1, 'ja-JP') }) + async function check_combo_box_value( + page: Page, + combo_box_index: number, + expected_value: string + ): Promise { + const combo_box = page.getByRole('combobox').nth(combo_box_index) + await expect(combo_box).toHaveValue(expected_value) + } + test.describe('after sign in', () => { test.use({ storageState: auth_file_path }) diff --git a/e2e/lib/get_pin_code_from_mail.ts b/tests/lib/get_pin_code_from_mail.ts similarity index 84% rename from e2e/lib/get_pin_code_from_mail.ts rename to tests/lib/get_pin_code_from_mail.ts index 3c243f1a..e90064b9 100644 --- a/e2e/lib/get_pin_code_from_mail.ts +++ b/tests/lib/get_pin_code_from_mail.ts @@ -2,8 +2,8 @@ import * as imaps from 'imap-simple' export async function get_pin_code_from_mail(): Promise { - const gmail_user = process.env.GMAIL_USER ?? '' - const gmail_password = process.env.GMAIL_PASS ?? '' + const gmail_user = process.env['GMAIL_USER'] ?? '' + const gmail_password = process.env['GMAIL_PASS'] ?? '' // expect(gmail_user).toBeDefined() // expect(gmail_password).toBeDefined() @@ -34,7 +34,7 @@ export async function get_pin_code_from_mail(): Promise { const subjects: string[] = messages.map((message) => { const header_part = message.parts.find((part) => part.which === 'HEADER') - if (header_part && header_part.body.subject) { + if (header_part?.body?.subject) { return header_part.body.subject[0] } @@ -44,7 +44,12 @@ export async function get_pin_code_from_mail(): Promise { // expect(subjects.length).toBeGreaterThan(0) const latest_subject = subjects[subjects.length - 1] + + if (!latest_subject) return '' + const pin_code = latest_subject.split(' ')[0] + if (!pin_code) return '' + return pin_code } diff --git a/e2e/lib/setup.ts b/tests/lib/setup.ts similarity index 100% rename from e2e/lib/setup.ts rename to tests/lib/setup.ts diff --git a/e2e/main.spec.ts b/tests/main.spec.ts similarity index 100% rename from e2e/main.spec.ts rename to tests/main.spec.ts diff --git a/tests/test.ts b/tests/test.ts deleted file mode 100644 index 8492aef9..00000000 --- a/tests/test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect, test } from '@playwright/test' - -test('index page has expected h1', async ({ page }) => { - await page.goto('/', { waitUntil: 'networkidle' }) - expect(await page.textContent('h1')).toBe('Welcome to SvelteKit') -}) diff --git a/e2e/translate.spec.ts b/tests/translate.spec.ts similarity index 77% rename from e2e/translate.spec.ts rename to tests/translate.spec.ts index df4b5235..c28692b2 100644 --- a/e2e/translate.spec.ts +++ b/tests/translate.spec.ts @@ -1,4 +1,5 @@ -import { test, expect, Page } from '@playwright/test' +import { expect, test } from '@playwright/test' +import type { Page } from '@playwright/test' import { auth_file_path } from './lib/setup.js' test.beforeEach(async ({ page }) => { @@ -9,7 +10,7 @@ test.beforeEach(async ({ page }) => { await page.locator('#language_1').selectOption('en-US') await page.locator('#language_2').selectOption('ja-JP') - await page.waitForTimeout(process.env.CI ? 1000 : 500) + await page.waitForTimeout(process.env['CI'] ? 1000 : 500) }) test('before sign in', async ({ page }) => { @@ -17,6 +18,7 @@ test('before sign in', async ({ page }) => { }) test.describe('after sign in', () => { + if (process.env['CI']) return test.use({ storageState: auth_file_path }) test('has title', async ({ page }) => { @@ -34,7 +36,7 @@ test.describe('after sign in', () => { // await expect(bottom_textarea).toHaveValue(text) // }) - test('check main box heights', async ({ page }) => { + async function check_main_box_heights(page: Page): Promise { const glass_panels = page.locator('.main-box') const count = await glass_panels.count() @@ -48,32 +50,19 @@ test.describe('after sign in', () => { box_heights.push(box.height) - if (box_heights.length > 0) { - await expect(box.height).toBeCloseTo(box_heights[0], 1) + if (box_heights.length > 0 && box_heights[0] && typeof box_heights[0] === 'number') { + expect(box.height).toBeCloseTo(box_heights[0], 1) } } + } + + test('check main box heights', async ({ page }) => { + await check_main_box_heights(page) }) test('check main box heights on mobile', async ({ page }) => { await page.setViewportSize({ width: 375, height: 812 }) - - const glass_panels = page.locator('.main-box') - const count = await glass_panels.count() - - const box_heights: Array = [] - - for (let i = 0; i < count; i++) { - const glass_panel = glass_panels.nth(i) - const box = await glass_panel.boundingBox() - - if (!box) throw new Error('box is null') - - box_heights.push(box.height) - - if (box_heights.length > 0) { - await expect(box.height).toBeCloseTo(box_heights[0], 1) - } - } + await check_main_box_heights(page) }) test('check if having no history hides box', async ({ page }) => { @@ -195,56 +184,50 @@ test.describe('after sign in', () => { await expect(button).toBeEnabled() }) - test('having no text disables tts button', async ({ page }) => { + async function setup_text_area(page: Page, text: string): Promise { await page.waitForSelector('.text-area') const from_text_area = page.locator('.text-area').first() + await from_text_area.fill(text) + await from_text_area.press(text ? 'Enter' : 'Meta+Enter') + } - await from_text_area.fill('') - await from_text_area.press('Meta+Enter') - - const button = page.getByTestId('tts_button').first().getByRole('button') - - await expect(button).toBeDisabled() - }) - - test('having text enables tts button', async ({ page }) => { - await page.waitForSelector('.text-area') - - const from_text_area = page.locator('.text-area').first() - - await from_text_area.fill('Hello') - await from_text_area.press('Enter') - - const button = page.getByTestId('tts_button').first().getByRole('button') - - await expect(button).toBeEnabled() - }) - - test('having no text disables copy button', async ({ page }) => { - await page.waitForSelector('.text-area') - - const from_text_area = page.locator('.text-area').first() - - await from_text_area.fill('') - await from_text_area.press('Meta+Enter') - - const button = page.getByTestId('copy_button').first().getByRole('button') - - await expect(button).toBeDisabled() - }) - - test('having text enables copy button', async ({ page }) => { - await page.waitForSelector('.text-area') + async function test_button_state( + page: Page, + button_test_id: string, + text: string, + should_be_enabled: boolean + ): Promise { + await setup_text_area(page, text) - const from_text_area = page.locator('.text-area').first() + const button = page.getByTestId(button_test_id).first().getByRole('button') - await from_text_area.fill('Hello') - await from_text_area.press('Enter') + if (should_be_enabled) { + await expect(button).toBeEnabled() + } else { + await expect(button).toBeDisabled() + } + } - const button = page.getByTestId('copy_button').first().getByRole('button') + interface Specs { + button_test_id: string + text: string + should_be_enabled: boolean + } - await expect(button).toBeEnabled() + const specs: Specs[] = [ + { button_test_id: 'tts_button', text: '', should_be_enabled: false }, + { button_test_id: 'tts_button', text: 'Hello', should_be_enabled: true }, + { button_test_id: 'copy_button', text: '', should_be_enabled: false }, + { button_test_id: 'copy_button', text: 'Hello', should_be_enabled: true }, + ] + + specs.forEach(({ button_test_id, text, should_be_enabled }) => { + test(`'${button_test_id}' should be ${ + should_be_enabled ? 'enabled' : 'disabled' + } with text '${text}'`, async ({ page }) => { + await test_button_state(page, button_test_id, text, should_be_enabled) + }) }) async function clear_text(page: Page): Promise {