From: Evan You Date: Sat, 8 Feb 2025 13:33:40 +0000 (+0800) Subject: test(vapor): vapor todomvc e2e test X-Git-Tag: v3.6.0-alpha.1~16^2~80 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ba0594de0b3cce897b989d0ff5ef848660608936;p=thirdparty%2Fvuejs%2Fcore.git test(vapor): vapor todomvc e2e test --- diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 70dc822481..a520aa147e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,6 +80,32 @@ jobs: - name: verify treeshaking run: node scripts/verify-treeshaking.js + e2e-vapor: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup cache for Chromium binary + uses: actions/cache@v4 + with: + path: ~/.cache/puppeteer + key: chromium-${{ hashFiles('pnpm-lock.yaml') }} + + - name: Install pnpm + uses: pnpm/action-setup@v4.0.0 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.node-version' + cache: 'pnpm' + + - run: pnpm install + - run: node node_modules/puppeteer/install.mjs + + - name: Run e2e tests + run: pnpm run test-e2e-vapor + lint-and-test-dts: runs-on: ubuntu-latest env: diff --git a/packages-private/vapor-e2e-test/__tests__/e2eUtils.ts b/packages-private/vapor-e2e-test/__tests__/e2eUtils.ts deleted file mode 100644 index 2ffebeb595..0000000000 --- a/packages-private/vapor-e2e-test/__tests__/e2eUtils.ts +++ /dev/null @@ -1,223 +0,0 @@ -import puppeteer, { - type Browser, - type ClickOptions, - type LaunchOptions, - type Page, -} from 'puppeteer' - -export const E2E_TIMEOUT: number = 30 * 1000 - -const puppeteerOptions: LaunchOptions = { - args: process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [], - headless: true, -} - -const maxTries = 30 -export const timeout = (n: number): Promise => - new Promise(r => setTimeout(r, n)) - -export async function expectByPolling( - poll: () => Promise, - expected: string, -): Promise { - 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) - } - } -} - -interface PuppeteerUtils { - page: () => Page - click(selector: string, options?: ClickOptions): Promise - count(selector: string): Promise - text(selector: string): Promise - value(selector: string): Promise - html(selector: string): Promise - classList(selector: string): Promise - style(selector: string, property: keyof CSSStyleDeclaration): Promise - children(selector: string): Promise - isVisible(selector: string): Promise - isChecked(selector: string): Promise - isFocused(selector: string): Promise - setValue(selector: string, value: string): Promise - typeValue(selector: string, value: string): Promise - enterValue(selector: string, value: string): Promise - clearValue(selector: string): Promise - timeout(time: number): Promise - nextFrame(): Promise -} - -export function setupPuppeteer(args?: string[]): PuppeteerUtils { - let browser: Browser - let page: Page - - const resolvedOptions = args - ? { - ...puppeteerOptions, - args: [...puppeteerOptions.args!, ...args], - } - : puppeteerOptions - - beforeAll(async () => { - browser = await puppeteer.launch(resolvedOptions) - }, 20000) - - beforeEach(async () => { - page = await browser.newPage() - - await page.evaluateOnNewDocument(() => { - localStorage.clear() - }) - - page.on('console', e => { - if (e.type() === 'error') { - console.error(`Error from Puppeteer-loaded page:\n`, e.text()) - } - }) - }) - - afterEach(async () => { - await page.close() - }) - - afterAll(async () => { - await browser.close() - }) - - async function click( - selector: string, - options?: ClickOptions, - ): Promise { - await page.click(selector, options) - } - - async function count(selector: string): Promise { - return (await page.$$(selector)).length - } - - async function text(selector: string): Promise { - return page.$eval(selector, node => node.textContent) - } - - async function value(selector: string): Promise { - return page.$eval(selector, node => (node as HTMLInputElement).value) - } - - async function html(selector: string): Promise { - return page.$eval(selector, node => node.innerHTML) - } - - async function classList(selector: string): Promise { - return page.$eval(selector, (node: any) => [...node.classList]) - } - - async function children(selector: string): Promise { - return page.$eval(selector, (node: any) => [...node.children]) - } - - async function style( - selector: string, - property: keyof CSSStyleDeclaration, - ): Promise { - return await page.$eval( - selector, - (node, property) => { - return window.getComputedStyle(node)[property] - }, - property, - ) - } - - async function isVisible(selector: string): Promise { - 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, - style, - children, - isVisible, - isChecked, - isFocused, - setValue, - typeValue, - enterValue, - clearValue, - timeout, - nextFrame, - } -} diff --git a/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts b/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts index 23f9e18e6d..3de8392e5e 100644 --- a/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts @@ -1 +1,195 @@ -test('bar', () => {}) +import path from 'node:path' +import { + E2E_TIMEOUT, + setupPuppeteer, +} from '../../../packages/vue/__tests__/e2e/e2eUtils' +import connect from 'connect' +import sirv from 'sirv' + +describe('e2e: todomvc', () => { + const { + page, + click, + isVisible, + count, + text, + value, + isChecked, + isFocused, + classList, + enterValue, + clearValue, + timeout, + } = setupPuppeteer() + + let server: any + const port = '8194' + beforeAll(() => { + server = connect() + .use(sirv(path.resolve(import.meta.dirname, '../dist'))) + .listen(port) + process.on('SIGTERM', () => server && server.close()) + }) + + afterAll(() => { + server.close() + }) + + 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') + } + + test( + 'vapor', + async () => { + const baseUrl = `http://localhost:${port}/todomvc/` + 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') + await timeout(1) + 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') + await timeout(1) + 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') + await timeout(1) + 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 timeout(1) + 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) + }, + E2E_TIMEOUT, + ) +}) diff --git a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts index 9a60a26ebc..360f48085a 100644 --- a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts @@ -25,7 +25,7 @@ describe('vdom / vapor interop', () => { test( 'should work', async () => { - const baseUrl = `http://localhost:${port}/interop` + const baseUrl = `http://localhost:${port}/interop/` await page().goto(baseUrl) expect(await text('.vapor > h2')).toContain('Vapor component in VDOM') diff --git a/packages-private/vapor-e2e-test/todomvc/App.vue b/packages-private/vapor-e2e-test/todomvc/App.vue new file mode 100644 index 0000000000..e7f97968b2 --- /dev/null +++ b/packages-private/vapor-e2e-test/todomvc/App.vue @@ -0,0 +1,230 @@ + + + diff --git a/packages-private/vapor-e2e-test/todomvc/index.html b/packages-private/vapor-e2e-test/todomvc/index.html index 689a5a8edf..79052a023b 100644 --- a/packages-private/vapor-e2e-test/todomvc/index.html +++ b/packages-private/vapor-e2e-test/todomvc/index.html @@ -1 +1,2 @@ +
diff --git a/packages-private/vapor-e2e-test/todomvc/main.ts b/packages-private/vapor-e2e-test/todomvc/main.ts index e69de29bb2..42497ab518 100644 --- a/packages-private/vapor-e2e-test/todomvc/main.ts +++ b/packages-private/vapor-e2e-test/todomvc/main.ts @@ -0,0 +1,5 @@ +import { createVaporApp } from 'vue' +import App from './App.vue' +import 'todomvc-app-css/index.css' + +createVaporApp(App).mount('#app')