mirror of https://github.com/vuejs/core.git
test(vue-vapor): todomvc e2e test (#115)
This commit is contained in:
parent
d77c7ade77
commit
2e2f3e2b96
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
})
|
|
@ -0,0 +1,224 @@
|
|||
<script src="../../dist/vue-vapor.global.js"></script>
|
||||
<link rel="stylesheet" href="../../../../node_modules/todomvc-app-css/index.css">
|
||||
|
||||
<div id="app"></div>
|
||||
|
||||
<script>
|
||||
const { createVaporApp, defineComponent, reactive, computed, watchEffect, onMounted, onUnmounted, nextTick } = VueVapor
|
||||
|
||||
const STORAGE_KEY = 'todos-vuejs-3.x'
|
||||
const todoStorage = {
|
||||
fetch () {
|
||||
const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
|
||||
todos.forEach((todo, index) => {
|
||||
todo.id = index
|
||||
})
|
||||
todoStorage.uid = todos.length
|
||||
return todos
|
||||
},
|
||||
save (todos) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
|
||||
}
|
||||
}
|
||||
|
||||
const filters = {
|
||||
all (todos) {
|
||||
return todos
|
||||
},
|
||||
active (todos) {
|
||||
return todos.filter((todo) => {
|
||||
return !todo.completed
|
||||
})
|
||||
},
|
||||
completed (todos) {
|
||||
return todos.filter(function (todo) {
|
||||
return todo.completed
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function pluralize (n) {
|
||||
return n === 1 ? 'item' : 'items'
|
||||
}
|
||||
|
||||
const _sfc_main = defineComponent({
|
||||
setup() {
|
||||
const state = reactive({
|
||||
todos: todoStorage.fetch(),
|
||||
editedTodo: null,
|
||||
newTodo: '',
|
||||
beforeEditCache: '',
|
||||
visibility: 'all',
|
||||
remaining: computed(() => {
|
||||
return filters.active(state.todos).length
|
||||
}),
|
||||
remainingText: computed(() => {
|
||||
return ` ${pluralize(state.remaining)} left`
|
||||
}),
|
||||
filteredTodos: computed(() => {
|
||||
return filters[state.visibility](state.todos)
|
||||
}),
|
||||
allDone: computed({
|
||||
get: function () {
|
||||
return state.remaining === 0
|
||||
},
|
||||
set: function (value) {
|
||||
state.todos.forEach(todo => {
|
||||
todo.completed = value
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
todoStorage.save(state.todos)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('hashchange', onHashChange)
|
||||
onHashChange()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('hashchange', onHashChange)
|
||||
})
|
||||
|
||||
function onHashChange() {
|
||||
const visibility = window.location.hash.replace(/#\/?/, '')
|
||||
if (filters[visibility]) {
|
||||
state.visibility = visibility
|
||||
} else {
|
||||
window.location.hash = ''
|
||||
state.visibility = 'all'
|
||||
}
|
||||
}
|
||||
|
||||
function addTodo() {
|
||||
const value = state.newTodo && state.newTodo.trim()
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
state.todos.push({
|
||||
id: todoStorage.uid++,
|
||||
title: value,
|
||||
completed: false,
|
||||
})
|
||||
state.newTodo = ''
|
||||
}
|
||||
|
||||
function removeTodo(todo) {
|
||||
state.todos.splice(state.todos.indexOf(todo), 1)
|
||||
}
|
||||
|
||||
async function editTodo(todo) {
|
||||
state.beforeEditCache = todo.title
|
||||
state.editedTodo = todo
|
||||
await nextTick()
|
||||
document.getElementById(`todo-${todo.id}-input`).focus()
|
||||
}
|
||||
|
||||
function doneEdit(todo) {
|
||||
if (!state.editedTodo) {
|
||||
return
|
||||
}
|
||||
state.editedTodo = null
|
||||
todo.title = todo.title.trim()
|
||||
if (!todo.title) {
|
||||
removeTodo(todo)
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEdit(todo) {
|
||||
state.editedTodo = null
|
||||
todo.title = state.beforeEditCache
|
||||
}
|
||||
|
||||
function removeCompleted() {
|
||||
state.todos = filters.active(state.todos)
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
addTodo,
|
||||
removeTodo,
|
||||
editTodo,
|
||||
doneEdit,
|
||||
cancelEdit,
|
||||
removeCompleted,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const { children: _children, vModelText: _vModelText, withDirectives: _withDirectives, vShow: _vShow, next: _next, delegate: _delegate, on: _on, setDynamicProp: _setDynamicProp, setText: _setText, setClass: _setClass, renderEffect: _renderEffect, createFor: _createFor, insert: _insert, delegateEvents: _delegateEvents, template: _template } = VueVapor
|
||||
const t0 = _template("<li><div class=\"view\"><input class=\"toggle\" type=\"checkbox\"><label></label><button class=\"destroy\"></button></div><input class=\"edit\" type=\"text\"></li>")
|
||||
const t1 = _template("<section class=\"todoapp\"><header class=\"header\"><h1>todos</h1><input class=\"new-todo\" autofocus autocomplete=\"off\" placeholder=\"What needs to be done?\"></header><section class=\"main\"><input id=\"toggle-all\" class=\"toggle-all\" type=\"checkbox\"><label for=\"toggle-all\">Mark all as complete</label><ul class=\"todo-list\"></ul></section><footer class=\"footer\"><span class=\"todo-count\"><strong></strong><span></span></span><ul class=\"filters\"><li><a href=\"#/all\">All</a></li><li><a href=\"#/active\">Active</a></li><li><a href=\"#/completed\">Completed</a></li></ul><button class=\"clear-completed\"> Clear completed </button></footer></section>")
|
||||
_delegateEvents("keyup", "dblclick", "click")
|
||||
|
||||
function _sfc_render(_ctx) {
|
||||
const n18 = t1()
|
||||
const n0 = _children(n18, 0, 1)
|
||||
_withDirectives(n0, [[_vModelText, () => _ctx.state.newTodo]])
|
||||
const n10 = _children(n18, 1)
|
||||
_withDirectives(n10, [[_vShow, () => _ctx.state.todos.length]])
|
||||
const n1 = n10.firstChild
|
||||
const n9 = _next(n1, 2)
|
||||
const n17 = n10.nextSibling
|
||||
_withDirectives(n17, [[_vShow, () => _ctx.state.todos.length]])
|
||||
const n11 = _children(n17, 0, 0)
|
||||
const n12 = n11.nextSibling
|
||||
const n13 = _children(n17, 1, 0, 0)
|
||||
const n14 = _children(n17, 1, 1, 0)
|
||||
const n15 = _children(n17, 1, 2, 0)
|
||||
const n16 = _children(n17, 2)
|
||||
_withDirectives(n16, [[_vShow, () => _ctx.state.todos.length > _ctx.state.remaining]])
|
||||
_delegate(n0, "update:modelValue", () => $event => (_ctx.state.newTodo = $event))
|
||||
_delegate(n0, "keyup", () => _ctx.addTodo, {
|
||||
keys: ["enter"]
|
||||
})
|
||||
_on(n1, "change", () => $event => (_ctx.state.allDone = $event.target.checked))
|
||||
const n2 = _createFor(() => (_ctx.state.filteredTodos), (_block) => {
|
||||
const n8 = t0()
|
||||
const n4 = _children(n8, 0, 0)
|
||||
const n5 = n4.nextSibling
|
||||
const n6 = n5.nextSibling
|
||||
const n7 = _children(n8, 1)
|
||||
_on(n4, "change", () => $event => (_block.s[0].completed = $event.target.checked))
|
||||
_delegate(n5, "dblclick", () => $event => (_ctx.editTodo(_block.s[0])))
|
||||
_delegate(n6, "click", () => $event => (_ctx.removeTodo(_block.s[0])))
|
||||
_on(n7, "input", () => $event => (_block.s[0].title = $event.target.value))
|
||||
_on(n7, "blur", () => $event => (_ctx.doneEdit(_block.s[0])))
|
||||
_delegate(n7, "keyup", () => $event => (_ctx.doneEdit(_block.s[0])), {
|
||||
keys: ["enter"]
|
||||
})
|
||||
_delegate(n7, "keyup", () => $event => (_ctx.cancelEdit(_block.s[0])), {
|
||||
keys: ["escape"]
|
||||
})
|
||||
const _updateEffect = () => {
|
||||
const [todo] = _block.s
|
||||
_setDynamicProp(n4, "checked", todo.completed)
|
||||
_setText(n5, todo.title)
|
||||
_setDynamicProp(n7, "value", todo.title)
|
||||
_setDynamicProp(n7, "id", `todo-${todo.id}-input`)
|
||||
_setClass(n8, ["todo", {
|
||||
completed: todo.completed,
|
||||
editing: todo === _ctx.state.editedTodo,
|
||||
}])
|
||||
}
|
||||
_renderEffect(_updateEffect)
|
||||
return [n8, _updateEffect]
|
||||
}, (todo) => (todo.id))
|
||||
_insert(n2, n9)
|
||||
_delegate(n16, "click", () => _ctx.removeCompleted)
|
||||
_renderEffect(() => _setDynamicProp(n1, "checked", _ctx.state.allDone))
|
||||
_renderEffect(() => _setText(n11, _ctx.state.remaining))
|
||||
_renderEffect(() => _setText(n12, _ctx.state.remainingText))
|
||||
_renderEffect(() => _setClass(n13, { selected: _ctx.state.visibility === 'all' }))
|
||||
_renderEffect(() => _setClass(n14, { selected: _ctx.state.visibility === 'active' }))
|
||||
_renderEffect(() => _setClass(n15, { selected: _ctx.state.visibility === 'completed' }))
|
||||
return n18
|
||||
}
|
||||
|
||||
_sfc_main.render = _sfc_render
|
||||
const app = createVaporApp(_sfc_main, {})
|
||||
app.mount('#app')
|
||||
</script>
|
|
@ -0,0 +1,249 @@
|
|||
<script>
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
computed,
|
||||
watchEffect,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
nextTick,
|
||||
} from 'vue/vapor'
|
||||
|
||||
import 'todomvc-app-css/index.css'
|
||||
|
||||
const STORAGE_KEY = 'todos-vuejs-3.x'
|
||||
const todoStorage = {
|
||||
fetch() {
|
||||
const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
|
||||
todos.forEach((todo, index) => {
|
||||
todo.id = index
|
||||
})
|
||||
todoStorage.uid = todos.length
|
||||
return todos
|
||||
},
|
||||
save(todos) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
|
||||
},
|
||||
}
|
||||
|
||||
const filters = {
|
||||
all(todos) {
|
||||
return todos
|
||||
},
|
||||
active(todos) {
|
||||
return todos.filter(todo => {
|
||||
return !todo.completed
|
||||
})
|
||||
},
|
||||
completed(todos) {
|
||||
return todos.filter(function (todo) {
|
||||
return todo.completed
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
function pluralize(n) {
|
||||
return n === 1 ? 'item' : 'items'
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const state = reactive({
|
||||
todos: todoStorage.fetch(),
|
||||
editedTodo: null,
|
||||
newTodo: '',
|
||||
beforeEditCache: '',
|
||||
visibility: 'all',
|
||||
remaining: computed(() => {
|
||||
return filters.active(state.todos).length
|
||||
}),
|
||||
remainingText: computed(() => {
|
||||
return ` ${pluralize(state.remaining)} left`
|
||||
}),
|
||||
filteredTodos: computed(() => {
|
||||
return filters[state.visibility](state.todos)
|
||||
}),
|
||||
allDone: computed({
|
||||
get: function () {
|
||||
return state.remaining === 0
|
||||
},
|
||||
set: function (value) {
|
||||
state.todos.forEach(todo => {
|
||||
todo.completed = value
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
todoStorage.save(state.todos)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('hashchange', onHashChange)
|
||||
onHashChange()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('hashchange', onHashChange)
|
||||
})
|
||||
|
||||
function onHashChange() {
|
||||
const visibility = window.location.hash.replace(/#\/?/, '')
|
||||
if (filters[visibility]) {
|
||||
state.visibility = visibility
|
||||
} else {
|
||||
window.location.hash = ''
|
||||
state.visibility = 'all'
|
||||
}
|
||||
}
|
||||
|
||||
function addTodo() {
|
||||
const value = state.newTodo && state.newTodo.trim()
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
state.todos.push({
|
||||
id: todoStorage.uid++,
|
||||
title: value,
|
||||
completed: false,
|
||||
})
|
||||
state.newTodo = ''
|
||||
}
|
||||
|
||||
function removeTodo(todo) {
|
||||
state.todos.splice(state.todos.indexOf(todo), 1)
|
||||
}
|
||||
|
||||
async function editTodo(todo) {
|
||||
state.beforeEditCache = todo.title
|
||||
state.editedTodo = todo
|
||||
await nextTick()
|
||||
document.getElementById(`todo-${todo.id}-input`).focus()
|
||||
}
|
||||
|
||||
function doneEdit(todo) {
|
||||
if (!state.editedTodo) {
|
||||
return
|
||||
}
|
||||
state.editedTodo = null
|
||||
todo.title = todo.title.trim()
|
||||
if (!todo.title) {
|
||||
removeTodo(todo)
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEdit(todo) {
|
||||
state.editedTodo = null
|
||||
todo.title = state.beforeEditCache
|
||||
}
|
||||
|
||||
function removeCompleted() {
|
||||
state.todos = filters.active(state.todos)
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
addTodo,
|
||||
removeTodo,
|
||||
editTodo,
|
||||
doneEdit,
|
||||
cancelEdit,
|
||||
removeCompleted,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>todos</h1>
|
||||
<input
|
||||
class="new-todo"
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
placeholder="What needs to be done?"
|
||||
v-model="state.newTodo"
|
||||
@keyup.enter="addTodo"
|
||||
/>
|
||||
</header>
|
||||
<section class="main" v-show="state.todos.length">
|
||||
<input
|
||||
id="toggle-all"
|
||||
class="toggle-all"
|
||||
type="checkbox"
|
||||
:checked="state.allDone"
|
||||
@change="state.allDone = $event.target.checked"
|
||||
/>
|
||||
<label for="toggle-all">Mark all as complete</label>
|
||||
<ul class="todo-list">
|
||||
<li
|
||||
v-for="todo in state.filteredTodos"
|
||||
class="todo"
|
||||
:key="todo.id"
|
||||
:class="{
|
||||
completed: todo.completed,
|
||||
editing: todo === state.editedTodo,
|
||||
}"
|
||||
>
|
||||
<div class="view">
|
||||
<input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
:checked="todo.completed"
|
||||
@change="todo.completed = $event.target.checked"
|
||||
/>
|
||||
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
|
||||
<button class="destroy" @click="removeTodo(todo)"></button>
|
||||
</div>
|
||||
<input
|
||||
:id="`todo-${todo.id}-input`"
|
||||
class="edit"
|
||||
type="text"
|
||||
:value="todo.title"
|
||||
@input="todo.title = $event.target.value"
|
||||
@blur="doneEdit(todo)"
|
||||
@keyup.enter="doneEdit(todo)"
|
||||
@keyup.escape="cancelEdit(todo)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" v-show="state.todos.length">
|
||||
<span class="todo-count">
|
||||
<strong>{{ state.remaining }}</strong>
|
||||
<span>{{ state.remainingText }}</span>
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li>
|
||||
<a href="#/all" :class="{ selected: state.visibility === 'all' }"
|
||||
>All</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#/active"
|
||||
:class="{ selected: state.visibility === 'active' }"
|
||||
>Active</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#/completed"
|
||||
:class="{ selected: state.visibility === 'completed' }"
|
||||
>Completed</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
class="clear-completed"
|
||||
@click="removeCompleted"
|
||||
v-show="state.todos.length > state.remaining"
|
||||
>
|
||||
Clear completed
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</template>
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue