Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(vue-vapor): todomvc e2e test #115

Merged
merged 3 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
181 changes: 181 additions & 0 deletions packages/vue-vapor/__tests__/e2e/e2eUtils.ts
Original file line number Diff line number Diff line change
@@ -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: true,
}

const maxTries = 30
export const timeout = (n: number) => new Promise(r => setTimeout(r, n))

export async function expectByPolling(
poll: () => Promise<any>,
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,
}
}
177 changes: 177 additions & 0 deletions packages/vue-vapor/__tests__/e2e/todomvc.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
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') {
let baseUrl = `../../examples/${apiType}/todomvc.html`
baseUrl = `file://${path.resolve(__dirname, baseUrl)}`

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(
'composition',
async () => {
await testTodomvc('composition')
},
E2E_TIMEOUT,
)
})
Loading
Loading