Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.

Commit 2e2f3e2

Browse files
authored
test(vue-vapor): todomvc e2e test (#115)
1 parent d77c7ad commit 2e2f3e2

File tree

6 files changed

+836
-2
lines changed

6 files changed

+836
-2
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"format-check": "prettier --check --cache .",
1919
"test": "vitest",
2020
"test-unit": "vitest -c vitest.unit.config.ts",
21-
"test-e2e": "node scripts/build.js vue -f global -d && vitest -c vitest.e2e.config.ts",
21+
"test-e2e": "node scripts/build.js vue vue-vapor -f global -d && vitest -c vitest.e2e.config.ts",
2222
"test-dts": "run-s build-dts test-dts-only",
2323
"test-dts-only": "tsc -p packages/dts-built-test/tsconfig.json && tsc -p ./packages/dts-test/tsconfig.test.json",
2424
"test-coverage": "vitest -c vitest.unit.config.ts --coverage",
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import puppeteer, {
2+
type Browser,
3+
type ClickOptions,
4+
type Page,
5+
type PuppeteerLaunchOptions,
6+
} from 'puppeteer'
7+
8+
export const E2E_TIMEOUT = 30 * 1000
9+
10+
const puppeteerOptions: PuppeteerLaunchOptions = {
11+
args: process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [],
12+
headless: true,
13+
}
14+
15+
const maxTries = 30
16+
export const timeout = (n: number) => new Promise(r => setTimeout(r, n))
17+
18+
export async function expectByPolling(
19+
poll: () => Promise<any>,
20+
expected: string,
21+
) {
22+
for (let tries = 0; tries < maxTries; tries++) {
23+
const actual = (await poll()) || ''
24+
if (actual.indexOf(expected) > -1 || tries === maxTries - 1) {
25+
expect(actual).toMatch(expected)
26+
break
27+
} else {
28+
await timeout(50)
29+
}
30+
}
31+
}
32+
33+
export function setupPuppeteer() {
34+
let browser: Browser
35+
let page: Page
36+
37+
beforeAll(async () => {
38+
browser = await puppeteer.launch(puppeteerOptions)
39+
}, 20000)
40+
41+
beforeEach(async () => {
42+
page = await browser.newPage()
43+
44+
await page.evaluateOnNewDocument(() => {
45+
localStorage.clear()
46+
})
47+
48+
page.on('console', e => {
49+
if (e.type() === 'error') {
50+
const err = e.args()[0]
51+
console.error(
52+
`Error from Puppeteer-loaded page:\n`,
53+
err.remoteObject().description,
54+
)
55+
}
56+
})
57+
})
58+
59+
afterEach(async () => {
60+
await page.close()
61+
})
62+
63+
afterAll(async () => {
64+
await browser.close()
65+
})
66+
67+
async function click(selector: string, options?: ClickOptions) {
68+
await page.click(selector, options)
69+
}
70+
71+
async function count(selector: string) {
72+
return (await page.$$(selector)).length
73+
}
74+
75+
async function text(selector: string) {
76+
return await page.$eval(selector, node => node.textContent)
77+
}
78+
79+
async function value(selector: string) {
80+
return await page.$eval(selector, node => (node as HTMLInputElement).value)
81+
}
82+
83+
async function html(selector: string) {
84+
return await page.$eval(selector, node => node.innerHTML)
85+
}
86+
87+
async function classList(selector: string) {
88+
return await page.$eval(selector, (node: any) => [...node.classList])
89+
}
90+
91+
async function children(selector: string) {
92+
return await page.$eval(selector, (node: any) => [...node.children])
93+
}
94+
95+
async function isVisible(selector: string) {
96+
const display = await page.$eval(selector, node => {
97+
return window.getComputedStyle(node).display
98+
})
99+
return display !== 'none'
100+
}
101+
102+
async function isChecked(selector: string) {
103+
return await page.$eval(
104+
selector,
105+
node => (node as HTMLInputElement).checked,
106+
)
107+
}
108+
109+
async function isFocused(selector: string) {
110+
return await page.$eval(selector, node => node === document.activeElement)
111+
}
112+
113+
async function setValue(selector: string, value: string) {
114+
await page.$eval(
115+
selector,
116+
(node, value) => {
117+
;(node as HTMLInputElement).value = value as string
118+
node.dispatchEvent(new Event('input'))
119+
},
120+
value,
121+
)
122+
}
123+
124+
async function typeValue(selector: string, value: string) {
125+
const el = (await page.$(selector))!
126+
await el.evaluate(node => ((node as HTMLInputElement).value = ''))
127+
await el.type(value)
128+
}
129+
130+
async function enterValue(selector: string, value: string) {
131+
const el = (await page.$(selector))!
132+
await el.evaluate(node => ((node as HTMLInputElement).value = ''))
133+
await el.type(value)
134+
await el.press('Enter')
135+
}
136+
137+
async function clearValue(selector: string) {
138+
return await page.$eval(
139+
selector,
140+
node => ((node as HTMLInputElement).value = ''),
141+
)
142+
}
143+
144+
function timeout(time: number) {
145+
return page.evaluate(time => {
146+
return new Promise(r => {
147+
setTimeout(r, time)
148+
})
149+
}, time)
150+
}
151+
152+
function nextFrame() {
153+
return page.evaluate(() => {
154+
return new Promise(resolve => {
155+
requestAnimationFrame(() => {
156+
requestAnimationFrame(resolve)
157+
})
158+
})
159+
})
160+
}
161+
162+
return {
163+
page: () => page,
164+
click,
165+
count,
166+
text,
167+
value,
168+
html,
169+
classList,
170+
children,
171+
isVisible,
172+
isChecked,
173+
isFocused,
174+
setValue,
175+
typeValue,
176+
enterValue,
177+
clearValue,
178+
timeout,
179+
nextFrame,
180+
}
181+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import path from 'node:path'
2+
import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
3+
4+
describe('e2e: todomvc', () => {
5+
const {
6+
page,
7+
click,
8+
isVisible,
9+
count,
10+
text,
11+
value,
12+
isChecked,
13+
isFocused,
14+
classList,
15+
enterValue,
16+
clearValue,
17+
} = setupPuppeteer()
18+
19+
async function removeItemAt(n: number) {
20+
const item = (await page().$('.todo:nth-child(' + n + ')'))!
21+
const itemBBox = (await item.boundingBox())!
22+
await page().mouse.move(itemBBox.x + 10, itemBBox.y + 10)
23+
await click('.todo:nth-child(' + n + ') .destroy')
24+
}
25+
26+
async function testTodomvc(apiType: 'classic' | 'composition') {
27+
let baseUrl = `../../examples/${apiType}/todomvc.html`
28+
baseUrl = `file://${path.resolve(__dirname, baseUrl)}`
29+
30+
await page().goto(baseUrl)
31+
expect(await isVisible('.main')).toBe(false)
32+
expect(await isVisible('.footer')).toBe(false)
33+
expect(await count('.filters .selected')).toBe(1)
34+
expect(await text('.filters .selected')).toBe('All')
35+
expect(await count('.todo')).toBe(0)
36+
37+
await enterValue('.new-todo', 'test')
38+
expect(await count('.todo')).toBe(1)
39+
expect(await isVisible('.todo .edit')).toBe(false)
40+
expect(await text('.todo label')).toBe('test')
41+
expect(await text('.todo-count strong')).toBe('1')
42+
expect(await isChecked('.todo .toggle')).toBe(false)
43+
expect(await isVisible('.main')).toBe(true)
44+
expect(await isVisible('.footer')).toBe(true)
45+
expect(await isVisible('.clear-completed')).toBe(false)
46+
expect(await value('.new-todo')).toBe('')
47+
48+
await enterValue('.new-todo', 'test2')
49+
expect(await count('.todo')).toBe(2)
50+
expect(await text('.todo:nth-child(2) label')).toBe('test2')
51+
expect(await text('.todo-count strong')).toBe('2')
52+
53+
// toggle
54+
await click('.todo .toggle')
55+
expect(await count('.todo.completed')).toBe(1)
56+
expect(await classList('.todo:nth-child(1)')).toContain('completed')
57+
expect(await text('.todo-count strong')).toBe('1')
58+
expect(await isVisible('.clear-completed')).toBe(true)
59+
60+
await enterValue('.new-todo', 'test3')
61+
expect(await count('.todo')).toBe(3)
62+
expect(await text('.todo:nth-child(3) label')).toBe('test3')
63+
expect(await text('.todo-count strong')).toBe('2')
64+
65+
await enterValue('.new-todo', 'test4')
66+
await enterValue('.new-todo', 'test5')
67+
expect(await count('.todo')).toBe(5)
68+
expect(await text('.todo-count strong')).toBe('4')
69+
70+
// toggle more
71+
await click('.todo:nth-child(4) .toggle')
72+
await click('.todo:nth-child(5) .toggle')
73+
expect(await count('.todo.completed')).toBe(3)
74+
expect(await text('.todo-count strong')).toBe('2')
75+
76+
// remove
77+
await removeItemAt(1)
78+
expect(await count('.todo')).toBe(4)
79+
expect(await count('.todo.completed')).toBe(2)
80+
expect(await text('.todo-count strong')).toBe('2')
81+
await removeItemAt(2)
82+
expect(await count('.todo')).toBe(3)
83+
expect(await count('.todo.completed')).toBe(2)
84+
expect(await text('.todo-count strong')).toBe('1')
85+
86+
// remove all
87+
await click('.clear-completed')
88+
expect(await count('.todo')).toBe(1)
89+
expect(await text('.todo label')).toBe('test2')
90+
expect(await count('.todo.completed')).toBe(0)
91+
expect(await text('.todo-count strong')).toBe('1')
92+
expect(await isVisible('.clear-completed')).toBe(false)
93+
94+
// prepare to test filters
95+
await enterValue('.new-todo', 'test')
96+
await enterValue('.new-todo', 'test')
97+
await click('.todo:nth-child(2) .toggle')
98+
await click('.todo:nth-child(3) .toggle')
99+
100+
// active filter
101+
await click('.filters li:nth-child(2) a')
102+
expect(await count('.todo')).toBe(1)
103+
expect(await count('.todo.completed')).toBe(0)
104+
// add item with filter active
105+
await enterValue('.new-todo', 'test')
106+
expect(await count('.todo')).toBe(2)
107+
108+
// completed filter
109+
await click('.filters li:nth-child(3) a')
110+
expect(await count('.todo')).toBe(2)
111+
expect(await count('.todo.completed')).toBe(2)
112+
113+
// filter on page load
114+
await page().goto(`${baseUrl}#active`)
115+
expect(await count('.todo')).toBe(2)
116+
expect(await count('.todo.completed')).toBe(0)
117+
expect(await text('.todo-count strong')).toBe('2')
118+
119+
// completed on page load
120+
await page().goto(`${baseUrl}#completed`)
121+
expect(await count('.todo')).toBe(2)
122+
expect(await count('.todo.completed')).toBe(2)
123+
expect(await text('.todo-count strong')).toBe('2')
124+
125+
// toggling with filter active
126+
await click('.todo .toggle')
127+
expect(await count('.todo')).toBe(1)
128+
await click('.filters li:nth-child(2) a')
129+
expect(await count('.todo')).toBe(3)
130+
await click('.todo .toggle')
131+
expect(await count('.todo')).toBe(2)
132+
133+
// editing triggered by blur
134+
await click('.filters li:nth-child(1) a')
135+
await click('.todo:nth-child(1) label', { clickCount: 2 })
136+
expect(await count('.todo.editing')).toBe(1)
137+
expect(await isFocused('.todo:nth-child(1) .edit')).toBe(true)
138+
await clearValue('.todo:nth-child(1) .edit')
139+
await page().type('.todo:nth-child(1) .edit', 'edited!')
140+
await click('.new-todo') // blur
141+
expect(await count('.todo.editing')).toBe(0)
142+
expect(await text('.todo:nth-child(1) label')).toBe('edited!')
143+
144+
// editing triggered by enter
145+
await click('.todo label', { clickCount: 2 })
146+
await enterValue('.todo:nth-child(1) .edit', 'edited again!')
147+
expect(await count('.todo.editing')).toBe(0)
148+
expect(await text('.todo:nth-child(1) label')).toBe('edited again!')
149+
150+
// cancel
151+
await click('.todo label', { clickCount: 2 })
152+
await clearValue('.todo:nth-child(1) .edit')
153+
await page().type('.todo:nth-child(1) .edit', 'edited!')
154+
await page().keyboard.press('Escape')
155+
expect(await count('.todo.editing')).toBe(0)
156+
expect(await text('.todo:nth-child(1) label')).toBe('edited again!')
157+
158+
// empty value should remove
159+
await click('.todo label', { clickCount: 2 })
160+
await enterValue('.todo:nth-child(1) .edit', ' ')
161+
expect(await count('.todo')).toBe(3)
162+
163+
// toggle all
164+
await click('.toggle-all+label')
165+
expect(await count('.todo.completed')).toBe(3)
166+
await click('.toggle-all+label')
167+
expect(await count('.todo:not(.completed)')).toBe(3)
168+
}
169+
170+
test(
171+
'composition',
172+
async () => {
173+
await testTodomvc('composition')
174+
},
175+
E2E_TIMEOUT,
176+
)
177+
})

0 commit comments

Comments
 (0)