From b31e989e4198753896d0644f3b7e1a5dc714eac3 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Tue, 14 May 2024 17:26:24 +0800 Subject: [PATCH 1/3] feat: setup e2e test env and add a failed unit test --- package.json | 2 +- packages/vue-vapor/__tests__/e2e/e2eUtils.ts | 181 +++++++++++++ .../vue-vapor/__tests__/e2e/todomvc.spec.ts | 179 +++++++++++++ .../examples/composition/todomvc.html | 229 ++++++++++++++++ playground/src/example-todomvc.vue | 249 ++++++++++++++++++ vitest.e2e.config.ts | 5 +- 6 files changed, 843 insertions(+), 2 deletions(-) create mode 100644 packages/vue-vapor/__tests__/e2e/e2eUtils.ts create mode 100644 packages/vue-vapor/__tests__/e2e/todomvc.spec.ts create mode 100644 packages/vue-vapor/examples/composition/todomvc.html create mode 100644 playground/src/example-todomvc.vue diff --git a/package.json b/package.json index aa1f3b59b..2feb62ad7 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "format-check": "prettier --check --cache .", "test": "vitest", "test-unit": "vitest -c vitest.unit.config.ts", - "test-e2e": "node scripts/build.js vue -f global -d && vitest -c vitest.e2e.config.ts", + "test-e2e": "node scripts/build.js vue vue-vapor -f global -d && vitest -c vitest.e2e.config.ts", "test-dts": "run-s build-dts test-dts-only", "test-dts-only": "tsc -p packages/dts-built-test/tsconfig.json && tsc -p ./packages/dts-test/tsconfig.test.json", "test-coverage": "vitest -c vitest.unit.config.ts --coverage", diff --git a/packages/vue-vapor/__tests__/e2e/e2eUtils.ts b/packages/vue-vapor/__tests__/e2e/e2eUtils.ts new file mode 100644 index 000000000..792e4452f --- /dev/null +++ b/packages/vue-vapor/__tests__/e2e/e2eUtils.ts @@ -0,0 +1,181 @@ +import puppeteer, { + type Browser, + type ClickOptions, + type Page, + type PuppeteerLaunchOptions, +} from 'puppeteer' + +export const E2E_TIMEOUT = 30 * 1000 + +const puppeteerOptions: PuppeteerLaunchOptions = { + args: process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [], + headless: 'new', +} + +const maxTries = 30 +export const timeout = (n: number) => new Promise(r => setTimeout(r, n)) + +export async function expectByPolling( + poll: () => Promise, + expected: string, +) { + for (let tries = 0; tries < maxTries; tries++) { + const actual = (await poll()) || '' + if (actual.indexOf(expected) > -1 || tries === maxTries - 1) { + expect(actual).toMatch(expected) + break + } else { + await timeout(50) + } + } +} + +export function setupPuppeteer() { + let browser: Browser + let page: Page + + beforeAll(async () => { + browser = await puppeteer.launch(puppeteerOptions) + }, 20000) + + beforeEach(async () => { + page = await browser.newPage() + + await page.evaluateOnNewDocument(() => { + localStorage.clear() + }) + + page.on('console', e => { + if (e.type() === 'error') { + const err = e.args()[0] + console.error( + `Error from Puppeteer-loaded page:\n`, + err.remoteObject().description, + ) + } + }) + }) + + afterEach(async () => { + await page.close() + }) + + afterAll(async () => { + await browser.close() + }) + + async function click(selector: string, options?: ClickOptions) { + await page.click(selector, options) + } + + async function count(selector: string) { + return (await page.$$(selector)).length + } + + async function text(selector: string) { + return await page.$eval(selector, node => node.textContent) + } + + async function value(selector: string) { + return await page.$eval(selector, node => (node as HTMLInputElement).value) + } + + async function html(selector: string) { + return await page.$eval(selector, node => node.innerHTML) + } + + async function classList(selector: string) { + return await page.$eval(selector, (node: any) => [...node.classList]) + } + + async function children(selector: string) { + return await page.$eval(selector, (node: any) => [...node.children]) + } + + async function isVisible(selector: string) { + const display = await page.$eval(selector, node => { + return window.getComputedStyle(node).display + }) + return display !== 'none' + } + + async function isChecked(selector: string) { + return await page.$eval( + selector, + node => (node as HTMLInputElement).checked, + ) + } + + async function isFocused(selector: string) { + return await page.$eval(selector, node => node === document.activeElement) + } + + async function setValue(selector: string, value: string) { + await page.$eval( + selector, + (node, value) => { + ;(node as HTMLInputElement).value = value as string + node.dispatchEvent(new Event('input')) + }, + value, + ) + } + + async function typeValue(selector: string, value: string) { + const el = (await page.$(selector))! + await el.evaluate(node => ((node as HTMLInputElement).value = '')) + await el.type(value) + } + + async function enterValue(selector: string, value: string) { + const el = (await page.$(selector))! + await el.evaluate(node => ((node as HTMLInputElement).value = '')) + await el.type(value) + await el.press('Enter') + } + + async function clearValue(selector: string) { + return await page.$eval( + selector, + node => ((node as HTMLInputElement).value = ''), + ) + } + + function timeout(time: number) { + return page.evaluate(time => { + return new Promise(r => { + setTimeout(r, time) + }) + }, time) + } + + function nextFrame() { + return page.evaluate(() => { + return new Promise(resolve => { + requestAnimationFrame(() => { + requestAnimationFrame(resolve) + }) + }) + }) + } + + return { + page: () => page, + click, + count, + text, + value, + html, + classList, + children, + isVisible, + isChecked, + isFocused, + setValue, + typeValue, + enterValue, + clearValue, + timeout, + nextFrame, + } +} diff --git a/packages/vue-vapor/__tests__/e2e/todomvc.spec.ts b/packages/vue-vapor/__tests__/e2e/todomvc.spec.ts new file mode 100644 index 000000000..6221d702a --- /dev/null +++ b/packages/vue-vapor/__tests__/e2e/todomvc.spec.ts @@ -0,0 +1,179 @@ +import path from 'node:path' +import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils' + +describe('e2e: todomvc', () => { + const { + page, + click, + isVisible, + count, + text, + value, + isChecked, + isFocused, + classList, + enterValue, + clearValue, + } = setupPuppeteer() + + async function removeItemAt(n: number) { + const item = (await page().$('.todo:nth-child(' + n + ')'))! + const itemBBox = (await item.boundingBox())! + await page().mouse.move(itemBBox.x + 10, itemBBox.y + 10) + await click('.todo:nth-child(' + n + ') .destroy') + } + + async function testTodomvc(apiType: 'classic' | 'composition') { + const baseUrl = `file://${path.resolve( + __dirname, + `../../examples/${apiType}/todomvc.html`, + )}` + + await page().goto(baseUrl) + expect(await isVisible('.main')).toBe(false) + expect(await isVisible('.footer')).toBe(false) + expect(await count('.filters .selected')).toBe(1) + expect(await text('.filters .selected')).toBe('All') + expect(await count('.todo')).toBe(0) + + await enterValue('.new-todo', 'test') + expect(await count('.todo')).toBe(1) + expect(await isVisible('.todo .edit')).toBe(false) + expect(await text('.todo label')).toBe('test') + expect(await text('.todo-count strong')).toBe('1') + expect(await isChecked('.todo .toggle')).toBe(false) + expect(await isVisible('.main')).toBe(true) + expect(await isVisible('.footer')).toBe(true) + expect(await isVisible('.clear-completed')).toBe(false) + expect(await value('.new-todo')).toBe('') + + await enterValue('.new-todo', 'test2') + expect(await count('.todo')).toBe(2) + expect(await text('.todo:nth-child(2) label')).toBe('test2') + expect(await text('.todo-count strong')).toBe('2') + + // toggle + await click('.todo .toggle') + expect(await count('.todo.completed')).toBe(1) + expect(await classList('.todo:nth-child(1)')).toContain('completed') + expect(await text('.todo-count strong')).toBe('1') + expect(await isVisible('.clear-completed')).toBe(true) + + await enterValue('.new-todo', 'test3') + expect(await count('.todo')).toBe(3) + expect(await text('.todo:nth-child(3) label')).toBe('test3') + expect(await text('.todo-count strong')).toBe('2') + + await enterValue('.new-todo', 'test4') + await enterValue('.new-todo', 'test5') + expect(await count('.todo')).toBe(5) + expect(await text('.todo-count strong')).toBe('4') + + // toggle more + await click('.todo:nth-child(4) .toggle') + await click('.todo:nth-child(5) .toggle') + expect(await count('.todo.completed')).toBe(3) + expect(await text('.todo-count strong')).toBe('2') + + // remove + await removeItemAt(1) + expect(await count('.todo')).toBe(4) + expect(await count('.todo.completed')).toBe(2) + expect(await text('.todo-count strong')).toBe('2') + await removeItemAt(2) + expect(await count('.todo')).toBe(3) + expect(await count('.todo.completed')).toBe(2) + expect(await text('.todo-count strong')).toBe('1') + + // remove all + await click('.clear-completed') + expect(await count('.todo')).toBe(1) + expect(await text('.todo label')).toBe('test2') + expect(await count('.todo.completed')).toBe(0) + expect(await text('.todo-count strong')).toBe('1') + expect(await isVisible('.clear-completed')).toBe(false) + + // prepare to test filters + await enterValue('.new-todo', 'test') + await enterValue('.new-todo', 'test') + await click('.todo:nth-child(2) .toggle') + await click('.todo:nth-child(3) .toggle') + + // active filter + await click('.filters li:nth-child(2) a') + expect(await count('.todo')).toBe(1) + expect(await count('.todo.completed')).toBe(0) + // add item with filter active + await enterValue('.new-todo', 'test') + expect(await count('.todo')).toBe(2) + + // completed filter + await click('.filters li:nth-child(3) a') + expect(await count('.todo')).toBe(2) + expect(await count('.todo.completed')).toBe(2) + + // filter on page load + await page().goto(`${baseUrl}#active`) + expect(await count('.todo')).toBe(2) + expect(await count('.todo.completed')).toBe(0) + expect(await text('.todo-count strong')).toBe('2') + + // completed on page load + await page().goto(`${baseUrl}#completed`) + expect(await count('.todo')).toBe(2) + expect(await count('.todo.completed')).toBe(2) + expect(await text('.todo-count strong')).toBe('2') + + // toggling with filter active + await click('.todo .toggle') + expect(await count('.todo')).toBe(1) + await click('.filters li:nth-child(2) a') + expect(await count('.todo')).toBe(3) + await click('.todo .toggle') + expect(await count('.todo')).toBe(2) + + // editing triggered by blur + await click('.filters li:nth-child(1) a') + await click('.todo:nth-child(1) label', { clickCount: 2 }) + expect(await count('.todo.editing')).toBe(1) + expect(await isFocused('.todo:nth-child(1) .edit')).toBe(true) + await clearValue('.todo:nth-child(1) .edit') + await page().type('.todo:nth-child(1) .edit', 'edited!') + await click('.new-todo') // blur + expect(await count('.todo.editing')).toBe(0) + expect(await text('.todo:nth-child(1) label')).toBe('edited!') + + // editing triggered by enter + await click('.todo label', { clickCount: 2 }) + await enterValue('.todo:nth-child(1) .edit', 'edited again!') + expect(await count('.todo.editing')).toBe(0) + expect(await text('.todo:nth-child(1) label')).toBe('edited again!') + + // cancel + await click('.todo label', { clickCount: 2 }) + await clearValue('.todo:nth-child(1) .edit') + await page().type('.todo:nth-child(1) .edit', 'edited!') + await page().keyboard.press('Escape') + expect(await count('.todo.editing')).toBe(0) + expect(await text('.todo:nth-child(1) label')).toBe('edited again!') + + // empty value should remove + await click('.todo label', { clickCount: 2 }) + await enterValue('.todo:nth-child(1) .edit', ' ') + expect(await count('.todo')).toBe(3) + + // toggle all + await click('.toggle-all+label') + expect(await count('.todo.completed')).toBe(3) + await click('.toggle-all+label') + expect(await count('.todo:not(.completed)')).toBe(3) + } + + test.fails( + 'composition', + async () => { + await testTodomvc('composition') + }, + E2E_TIMEOUT, + ) +}) diff --git a/packages/vue-vapor/examples/composition/todomvc.html b/packages/vue-vapor/examples/composition/todomvc.html new file mode 100644 index 000000000..9e1cade18 --- /dev/null +++ b/packages/vue-vapor/examples/composition/todomvc.html @@ -0,0 +1,229 @@ + + + +
+ + diff --git a/playground/src/example-todomvc.vue b/playground/src/example-todomvc.vue new file mode 100644 index 000000000..82c7e88ea --- /dev/null +++ b/playground/src/example-todomvc.vue @@ -0,0 +1,249 @@ + + + diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts index 90a67d229..2436baf2b 100644 --- a/vitest.e2e.config.ts +++ b/vitest.e2e.config.ts @@ -8,6 +8,9 @@ export default mergeConfig(config, { singleThread: !!process.env.CI, }, }, - include: ['packages/vue/__tests__/e2e/*.spec.ts'], + include: [ + 'packages/vue/__tests__/e2e/*.spec.ts', + 'packages/vue-vapor/__tests__/e2e/*.spec.ts', + ], }, }) From 959fe71ed8a936fb9458a67be502f9f3e23661ea Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Tue, 14 May 2024 19:44:50 +0800 Subject: [PATCH 2/3] chore: update e2e test --- packages/vue-vapor/__tests__/e2e/e2eUtils.ts | 2 +- .../vue-vapor/__tests__/e2e/todomvc.spec.ts | 6 +-- .../examples/composition/todomvc.html | 40 ++++++++----------- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/packages/vue-vapor/__tests__/e2e/e2eUtils.ts b/packages/vue-vapor/__tests__/e2e/e2eUtils.ts index 792e4452f..6e98ffc1d 100644 --- a/packages/vue-vapor/__tests__/e2e/e2eUtils.ts +++ b/packages/vue-vapor/__tests__/e2e/e2eUtils.ts @@ -9,7 +9,7 @@ export const E2E_TIMEOUT = 30 * 1000 const puppeteerOptions: PuppeteerLaunchOptions = { args: process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [], - headless: 'new', + headless: true, } const maxTries = 30 diff --git a/packages/vue-vapor/__tests__/e2e/todomvc.spec.ts b/packages/vue-vapor/__tests__/e2e/todomvc.spec.ts index 6221d702a..3d39ee086 100644 --- a/packages/vue-vapor/__tests__/e2e/todomvc.spec.ts +++ b/packages/vue-vapor/__tests__/e2e/todomvc.spec.ts @@ -24,10 +24,8 @@ describe('e2e: todomvc', () => { } async function testTodomvc(apiType: 'classic' | 'composition') { - const baseUrl = `file://${path.resolve( - __dirname, - `../../examples/${apiType}/todomvc.html`, - )}` + let baseUrl = `../../examples/${apiType}/todomvc.html` + baseUrl = `file://${path.resolve(__dirname, baseUrl)}` await page().goto(baseUrl) expect(await isVisible('.main')).toBe(false) diff --git a/packages/vue-vapor/examples/composition/todomvc.html b/packages/vue-vapor/examples/composition/todomvc.html index 9e1cade18..73bf87042 100644 --- a/packages/vue-vapor/examples/composition/todomvc.html +++ b/packages/vue-vapor/examples/composition/todomvc.html @@ -4,7 +4,7 @@
From 20486c792f56d30b4555e5088bd4345e85125f96 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Wed, 15 May 2024 16:29:50 +0800 Subject: [PATCH 3/3] feat: skip v-model case + skip directives case --- .../vue-vapor/__tests__/e2e/todomvc.spec.ts | 2 +- .../examples/composition/todomvc.html | 25 +++++++++--------- playground/src/example-todomvc.vue | 26 +++++++++---------- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/packages/vue-vapor/__tests__/e2e/todomvc.spec.ts b/packages/vue-vapor/__tests__/e2e/todomvc.spec.ts index 3d39ee086..c26079c06 100644 --- a/packages/vue-vapor/__tests__/e2e/todomvc.spec.ts +++ b/packages/vue-vapor/__tests__/e2e/todomvc.spec.ts @@ -167,7 +167,7 @@ describe('e2e: todomvc', () => { expect(await count('.todo:not(.completed)')).toBe(3) } - test.fails( + test( 'composition', async () => { await testTodomvc('composition') diff --git a/packages/vue-vapor/examples/composition/todomvc.html b/packages/vue-vapor/examples/composition/todomvc.html index 73bf87042..47faa6412 100644 --- a/packages/vue-vapor/examples/composition/todomvc.html +++ b/packages/vue-vapor/examples/composition/todomvc.html @@ -4,7 +4,7 @@
@@ -178,7 +173,6 @@ export default defineComponent({ id="toggle-all" class="toggle-all" type="checkbox" - v-model="state.allDone" :checked="state.allDone" @change="state.allDone = $event.target.checked" /> @@ -194,15 +188,21 @@ export default defineComponent({ }" >
- +