From fd1fef55020036b4989329d66d356fcd14f9a42e Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 23 Aug 2019 15:27:17 -0400 Subject: [PATCH] test: update fragment tests --- packages/runtime-core/__tests__/h.spec.ts | 0 .../__tests__/vdomAttrsFallthrough.spec.ts | 2 +- .../__tests__/vdomFragment.spec.ts | 458 +++++++++++------- packages/runtime-core/src/createRenderer.ts | 4 +- packages/runtime-core/src/h.ts | 49 ++ packages/runtime-core/src/index.ts | 1 + packages/runtime-core/src/vnode.ts | 2 +- .../__tests__/testRuntime.spec.ts | 4 +- packages/runtime-test/src/nodeOps.ts | 17 +- 9 files changed, 343 insertions(+), 194 deletions(-) create mode 100644 packages/runtime-core/__tests__/h.spec.ts create mode 100644 packages/runtime-core/src/h.ts diff --git a/packages/runtime-core/__tests__/h.spec.ts b/packages/runtime-core/__tests__/h.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/runtime-core/__tests__/vdomAttrsFallthrough.spec.ts b/packages/runtime-core/__tests__/vdomAttrsFallthrough.spec.ts index 711e8109e..8ddaa644d 100644 --- a/packages/runtime-core/__tests__/vdomAttrsFallthrough.spec.ts +++ b/packages/runtime-core/__tests__/vdomAttrsFallthrough.spec.ts @@ -1,6 +1,6 @@ // using DOM renderer because this case is mostly DOM-specific import { - createVNode as h, + h, render, nextTick, mergeProps, diff --git a/packages/runtime-core/__tests__/vdomFragment.spec.ts b/packages/runtime-core/__tests__/vdomFragment.spec.ts index f9732864a..c5caf83a0 100644 --- a/packages/runtime-core/__tests__/vdomFragment.spec.ts +++ b/packages/runtime-core/__tests__/vdomFragment.spec.ts @@ -1,201 +1,295 @@ -// These tests are outdated. +import { + h, + createVNode, + render, + nodeOps, + NodeTypes, + TestElement, + serialize, + Fragment, + reactive, + nextTick, + PatchFlags, + resetOps, + dumpOps, + NodeOpTypes +} from '@vue/runtime-test' -// import { -// createVNode as h, -// render, -// nodeOps, -// NodeTypes, -// TestElement, -// Fragment, -// reactive, -// serialize, -// nextTick, -// resetOps, -// dumpOps, -// NodeOpTypes -// } from '@vue/runtime-test' +describe('vdom: fragment', () => { + it('should allow returning multiple component root nodes', () => { + const App = { + render() { + return [h('div', 'one'), 'two'] + } + } -// describe('vdom: fragment', () => { -// it('should allow returning multiple component root nodes', async () => { -// class App extends Component { -// render() { -// return [h('div', 'one'), 'two'] -// } -// } -// const root = nodeOps.createElement('div') -// await render(h(App), root) -// expect(serialize(root)).toBe(`
one
two
`) -// expect(root.children.length).toBe(2) -// expect(root.children[0]).toMatchObject({ -// type: NodeTypes.ELEMENT, -// tag: 'div' -// }) -// expect((root.children[0] as TestElement).children[0]).toMatchObject({ -// type: NodeTypes.TEXT, -// text: 'one' -// }) -// expect(root.children[1]).toMatchObject({ -// type: NodeTypes.TEXT, -// text: 'two' -// }) -// }) + const root = nodeOps.createElement('div') + render(h(App), root) -// it('should be able to explicitly create fragments', async () => { -// class App extends Component { -// render() { -// return h('div', [h(Fragment, [h('div', 'one'), 'two'])]) -// } -// } -// const root = nodeOps.createElement('div') -// await render(h(App), root) -// const parent = root.children[0] as TestElement -// expect(serialize(parent)).toBe(`
one
two
`) -// }) + expect(serialize(root)).toBe(`
one
two
`) + expect(root.children.length).toBe(4) + expect(root.children[0]).toMatchObject({ + type: NodeTypes.COMMENT + }) + expect(root.children[1]).toMatchObject({ + type: NodeTypes.ELEMENT, + tag: 'div' + }) + expect((root.children[1] as TestElement).children[0]).toMatchObject({ + type: NodeTypes.TEXT, + text: 'one' + }) + expect(root.children[2]).toMatchObject({ + type: NodeTypes.TEXT, + text: 'two' + }) + expect(root.children[3]).toMatchObject({ + type: NodeTypes.COMMENT + }) + }) -// it('should be able to patch fragment children (unkeyed)', async () => { -// const state = observable({ ok: true }) -// class App extends Component { -// render() { -// return state.ok -// ? createFragment( -// [h('div', 'one'), createTextVNode('two')], -// ChildrenFlags.NONE_KEYED_VNODES -// ) -// : createFragment( -// [h('div', 'foo'), createTextVNode('bar'), createTextVNode('baz')], -// ChildrenFlags.NONE_KEYED_VNODES -// ) -// } -// } -// const root = nodeOps.createElement('div') -// await render(h(App), root) + it('explicitly create fragments', () => { + const App = { + render() { + return h('div', [h(Fragment, [h('div', 'one'), 'two'])]) + } + } + const root = nodeOps.createElement('div') + render(h(App), root) + const parent = root.children[0] as TestElement + expect(serialize(parent)).toBe(`
one
two
`) + }) -// expect(serialize(root)).toBe(`
one
two
`) + it('patch fragment children (manual, keyed)', async () => { + const state = reactive({ ok: true }) + const App = { + render() { + return state.ok + ? [h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')] + : [h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')] + } + } + const root = nodeOps.createElement('div') + render(h(App), root) -// state.ok = false -// await nextTick() -// expect(serialize(root)).toBe(`
foo
barbaz
`) -// }) + expect(serialize(root)).toBe( + `
one
two
` + ) -// it('should be able to patch fragment children (implicitly keyed)', async () => { -// const state = observable({ ok: true }) -// class App extends Component { -// render() { -// return state.ok -// ? [h('div', 'one'), 'two'] -// : [h('pre', 'foo'), 'bar', 'baz'] -// } -// } -// const root = nodeOps.createElement('div') -// await await render(h(App), root) + resetOps() + state.ok = false + await nextTick() + expect(serialize(root)).toBe( + `
two
one
` + ) + const ops = dumpOps() + // should be moving nodes instead of re-creating or patching them + expect(ops).toMatchObject([ + { + type: NodeOpTypes.INSERT + } + ]) + }) -// expect(serialize(root)).toBe(`
one
two
`) + it('patch fragment children (manual, unkeyed)', async () => { + const state = reactive({ ok: true }) + const App = { + render() { + return state.ok + ? [h('div', 'one'), h('div', 'two')] + : [h('div', 'two'), h('div', 'one')] + } + } + const root = nodeOps.createElement('div') + render(h(App), root) -// state.ok = false -// await nextTick() -// expect(serialize(root)).toBe(`
foo
barbaz
`) -// }) + expect(serialize(root)).toBe( + `
one
two
` + ) -// it('should be able to patch fragment children (explcitly keyed)', async () => { -// const state = observable({ ok: true }) -// class App extends Component { -// render() { -// return state.ok -// ? [h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')] -// : [h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')] -// } -// } -// const root = nodeOps.createElement('div') -// await render(h(App), root) + resetOps() + state.ok = false + await nextTick() + expect(serialize(root)).toBe( + `
two
one
` + ) + const ops = dumpOps() + // should be patching nodes instead of moving or re-creating them + expect(ops).toMatchObject([ + { + type: NodeOpTypes.SET_ELEMENT_TEXT + }, + { + type: NodeOpTypes.SET_ELEMENT_TEXT + } + ]) + }) -// expect(serialize(root)).toBe(`
one
two
`) + it('patch fragment children (compiler generated, unkeyed)', async () => { + const state = reactive({ ok: true }) + const App = { + render() { + return state.ok + ? createVNode( + Fragment, + 0, + [h('div', 'one'), 'two'], + PatchFlags.UNKEYED + ) + : createVNode( + Fragment, + 0, + [h('div', 'foo'), 'bar', 'baz'], + PatchFlags.UNKEYED + ) + } + } + const root = nodeOps.createElement('div') + render(h(App), root) -// resetOps() -// state.ok = false -// await nextTick() -// expect(serialize(root)).toBe(`
two
one
`) -// const ops = dumpOps() -// // should be moving nodes instead of re-creating them -// expect(ops.some(op => op.type === NodeOpTypes.CREATE)).toBe(false) -// }) + expect(serialize(root)).toBe(`
one
two
`) -// it('should be able to move fragment', async () => { -// const state = observable({ ok: true }) -// class App extends Component { -// render() { -// return state.ok -// ? h('div', [ -// h('div', { key: 1 }, 'outer'), -// h(Fragment, { key: 2 }, [ -// h('div', { key: 1 }, 'one'), -// h('div', { key: 2 }, 'two') -// ]) -// ]) -// : h('div', [ -// h(Fragment, { key: 2 }, [ -// h('div', { key: 2 }, 'two'), -// h('div', { key: 1 }, 'one') -// ]), -// h('div', { key: 1 }, 'outer') -// ]) -// } -// } -// const root = nodeOps.createElement('div') -// await render(h(App), root) -// const parent = root.children[0] as TestElement + state.ok = false + await nextTick() + expect(serialize(root)).toBe( + `
foo
barbaz
` + ) + }) -// expect(serialize(parent)).toBe( -// `
outer
one
two
` -// ) + it('patch fragment children (compiler generated, keyed)', async () => { + const state = reactive({ ok: true }) + const App = { + render() { + return state.ok + ? createVNode( + Fragment, + 0, + [h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')], + PatchFlags.KEYED + ) + : createVNode( + Fragment, + 0, + [h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')], + PatchFlags.KEYED + ) + } + } + const root = nodeOps.createElement('div') + render(h(App), root) -// resetOps() -// state.ok = false -// await nextTick() -// expect(serialize(parent)).toBe( -// `
two
one
outer
` -// ) -// const ops = dumpOps() -// // should be moving nodes instead of re-creating them -// expect(ops.some(op => op.type === NodeOpTypes.CREATE)).toBe(false) -// }) + expect(serialize(root)).toBe( + `
one
two
` + ) -// it('should be able to handle nested fragments', async () => { -// const state = observable({ ok: true }) -// class App extends Component { -// render() { -// return state.ok -// ? [ -// h('div', { key: 1 }, 'outer'), -// h(Fragment, { key: 2 }, [ -// h('div', { key: 1 }, 'one'), -// h('div', { key: 2 }, 'two') -// ]) -// ] -// : [ -// h(Fragment, { key: 2 }, [ -// h('div', { key: 2 }, 'two'), -// h('div', { key: 1 }, 'one') -// ]), -// h('div', { key: 1 }, 'outer') -// ] -// } -// } + resetOps() + state.ok = false + await nextTick() + expect(serialize(root)).toBe( + `
two
one
` + ) + const ops = dumpOps() + // should be moving nodes instead of re-creating or patching them + expect(ops).toMatchObject([ + { + type: NodeOpTypes.INSERT + } + ]) + }) -// const root = nodeOps.createElement('div') -// await render(h(App), root) + it('move fragment', async () => { + const state = reactive({ ok: true }) + const App = { + render() { + return state.ok + ? h('div', [ + h('div', { key: 1 }, 'outer'), + h(Fragment, { key: 2 }, [ + h('div', { key: 1 }, 'one'), + h('div', { key: 2 }, 'two') + ]) + ]) + : h('div', [ + h(Fragment, { key: 2 }, [ + h('div', { key: 2 }, 'two'), + h('div', { key: 1 }, 'one') + ]), + h('div', { key: 1 }, 'outer') + ]) + } + } + const root = nodeOps.createElement('div') + render(h(App), root) + const parent = root.children[0] as TestElement -// expect(serialize(root)).toBe( -// `
outer
one
two
` -// ) + expect(serialize(parent)).toBe( + `
outer
one
two
` + ) -// resetOps() -// state.ok = false -// await nextTick() -// expect(serialize(root)).toBe( -// `
two
one
outer
` -// ) -// const ops = dumpOps() -// // should be moving nodes instead of re-creating them -// expect(ops.some(op => op.type === NodeOpTypes.CREATE)).toBe(false) -// }) -// }) + resetOps() + state.ok = false + await nextTick() + expect(serialize(parent)).toBe( + `
two
one
outer
` + ) + const ops = dumpOps() + // should be moving nodes instead of re-creating them + expect(ops).toMatchObject([ + // 1. re-order inside the fragment + { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } }, + // 2. move entire fragment, including anchors + // not the most efficient move, but this case is super rare + // and optimizing for this special case complicates the algo quite a bit + { type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } }, + { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } }, + { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } }, + { type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } } + ]) + }) + + it('handle nested fragments', async () => { + const state = reactive({ ok: true }) + const App = { + render() { + return state.ok + ? [ + h('div', { key: 1 }, 'outer'), + h(Fragment, { key: 2 }, [ + h('div', { key: 1 }, 'one'), + h('div', { key: 2 }, 'two') + ]) + ] + : [ + h(Fragment, { key: 2 }, [ + h('div', { key: 2 }, 'two'), + h('div', { key: 1 }, 'one') + ]), + h('div', { key: 1 }, 'outer') + ] + } + } + + const root = nodeOps.createElement('div') + render(h(App), root) + + expect(serialize(root)).toBe( + `
outer
one
two
` + ) + + resetOps() + state.ok = false + await nextTick() + expect(serialize(root)).toBe( + `
two
one
outer
` + ) + const ops = dumpOps() + // should be moving nodes instead of re-creating them + expect(ops).toMatchObject([ + { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } }, + { type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } }, + { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } }, + { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } }, + { type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } } + ]) + }) +}) diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 864148291..2dd6c6abe 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -694,7 +694,9 @@ export function createRenderer(options: RendererOptions) { if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { unmountChildren(c1 as VNode[], parentComponent) } - hostSetElementText(container, c2 as string) + if (c2 !== c1) { + hostSetElementText(container, c2 as string) + } } else { if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { hostSetElementText(container, '') diff --git a/packages/runtime-core/src/h.ts b/packages/runtime-core/src/h.ts new file mode 100644 index 000000000..df16598cd --- /dev/null +++ b/packages/runtime-core/src/h.ts @@ -0,0 +1,49 @@ +import { VNodeTypes, VNode, createVNode } from './vnode' +import { isObject, isArray } from '@vue/shared' + +// `h` is a more user-friendly version of `createVNode` that allows omitting the +// props when possible. It is intended for manually written render functions. +// Compiler-generated code uses `createVNode` because +// 1. it is monomorphic and avoids the extra call overhead +// 2. it allows specifying patchFlags for optimization + +/* +// type only +h('div') + +// type + props +h('div', {}) + +// type + omit props + children +// Omit props does NOT support named slots +h('div', []) // array +h('div', () => {}) // default slot +h('div', 'foo') // text + +// type + props + children +h('div', {}, []) // array +h('div', {}, () => {}) // default slot +h('div', {}, {}) // named slots +h('div', {}, 'foo') // text + +// named slots without props requires explicit `null` to avoid ambiguity +h('div', null, {}) +**/ + +export function h( + type: VNodeTypes, + propsOrChildren?: any, + children?: any +): VNode { + if (arguments.length === 2) { + if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { + // props without children + return createVNode(type, propsOrChildren) + } else { + // omit props + return createVNode(type, null, propsOrChildren) + } + } else { + return createVNode(type, propsOrChildren, children) + } +} diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 70efa4883..f2a36c984 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -10,6 +10,7 @@ export * from './apiInject' // Advanced API ---------------------------------------------------------------- // For raw render function users +export { h } from './h' export { createVNode, cloneVNode, diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 13021e783..d2f77f661 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -18,7 +18,7 @@ export const Text = Symbol('Text') export const Empty = Symbol('Empty') export const Portal = Symbol('Portal') -type VNodeTypes = +export type VNodeTypes = | string | Function | Object diff --git a/packages/runtime-test/__tests__/testRuntime.spec.ts b/packages/runtime-test/__tests__/testRuntime.spec.ts index 630ce5203..a7fe97fd9 100644 --- a/packages/runtime-test/__tests__/testRuntime.spec.ts +++ b/packages/runtime-test/__tests__/testRuntime.spec.ts @@ -1,5 +1,5 @@ import { - createVNode as h, + h, render, nodeOps, NodeTypes, @@ -125,7 +125,7 @@ describe('test renderer', () => { { id: 'test' }, - [h('span', 0, 'foo'), 'hello'] + [h('span', 'foo'), 'hello'] ) } } diff --git a/packages/runtime-test/src/nodeOps.ts b/packages/runtime-test/src/nodeOps.ts index b7fa91e72..7de1ab774 100644 --- a/packages/runtime-test/src/nodeOps.ts +++ b/packages/runtime-test/src/nodeOps.ts @@ -145,7 +145,8 @@ function insert(child: TestNode, parent: TestElement, ref?: TestNode | null) { parentNode: parent, refNode: ref }) - remove(child) + // remove the node first, but don't log it as a REMOVE op + remove(child, false) if (refIndex === undefined) { parent.children.push(child) child.parentNode = parent @@ -155,14 +156,16 @@ function insert(child: TestNode, parent: TestElement, ref?: TestNode | null) { } } -function remove(child: TestNode) { +function remove(child: TestNode, logOp: boolean = true) { const parent = child.parentNode if (parent != null) { - logNodeOp({ - type: NodeOpTypes.REMOVE, - targetNode: child, - parentNode: parent - }) + if (logOp) { + logNodeOp({ + type: NodeOpTypes.REMOVE, + targetNode: child, + parentNode: parent + }) + } const i = parent.children.indexOf(child) if (i > -1) { parent.children.splice(i, 1)