--- /dev/null
+import { parse } from '../../src/parse'
+import { transform } from '../../src/transform'
+import { transformIf } from '../../src/directives/vIf'
+import {
+ IfNode,
+ NodeTypes,
+ ElementNode,
+ TextNode,
+ CommentNode
+} from '../../src/ast'
+import { ErrorCodes } from '../../src/errors'
+
+describe('compiler: v-if', () => {
+ describe('transform', () => {
+ test('basic v-if', () => {
+ const ast = parse(`<div v-if="ok"/>`)
+ transform(ast, {
+ transforms: [transformIf]
+ })
+ const node = ast.children[0] as IfNode
+ expect(node.type).toBe(NodeTypes.IF)
+ expect(node.branches.length).toBe(1)
+ expect(node.branches[0].condition!.content).toBe(`ok`)
+ expect(node.branches[0].children.length).toBe(1)
+ expect(node.branches[0].children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((node.branches[0].children[0] as ElementNode).tag).toBe(`div`)
+ })
+
+ test('template v-if', () => {
+ const ast = parse(`<template v-if="ok"><div/>hello<p/></template>`)
+ transform(ast, {
+ transforms: [transformIf]
+ })
+ const node = ast.children[0] as IfNode
+ expect(node.type).toBe(NodeTypes.IF)
+ expect(node.branches.length).toBe(1)
+ expect(node.branches[0].condition!.content).toBe(`ok`)
+ expect(node.branches[0].children.length).toBe(3)
+ expect(node.branches[0].children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((node.branches[0].children[0] as ElementNode).tag).toBe(`div`)
+ expect(node.branches[0].children[1].type).toBe(NodeTypes.TEXT)
+ expect((node.branches[0].children[1] as TextNode).content).toBe(`hello`)
+ expect(node.branches[0].children[2].type).toBe(NodeTypes.ELEMENT)
+ expect((node.branches[0].children[2] as ElementNode).tag).toBe(`p`)
+ })
+
+ test('v-if + v-else', () => {
+ const ast = parse(`<div v-if="ok"/><p v-else/>`)
+ transform(ast, {
+ transforms: [transformIf]
+ })
+ // should fold branches
+ expect(ast.children.length).toBe(1)
+
+ const node = ast.children[0] as IfNode
+ expect(node.type).toBe(NodeTypes.IF)
+ expect(node.branches.length).toBe(2)
+
+ const b1 = node.branches[0]
+ expect(b1.condition!.content).toBe(`ok`)
+ expect(b1.children.length).toBe(1)
+ expect(b1.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((b1.children[0] as ElementNode).tag).toBe(`div`)
+
+ const b2 = node.branches[1]
+ expect(b2.condition).toBeUndefined()
+ expect(b2.children.length).toBe(1)
+ expect(b2.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((b2.children[0] as ElementNode).tag).toBe(`p`)
+ })
+
+ test('v-if + v-else-if', () => {
+ const ast = parse(`<div v-if="ok"/><p v-else-if="orNot"/>`)
+ transform(ast, {
+ transforms: [transformIf]
+ })
+ // should fold branches
+ expect(ast.children.length).toBe(1)
+
+ const node = ast.children[0] as IfNode
+ expect(node.type).toBe(NodeTypes.IF)
+ expect(node.branches.length).toBe(2)
+
+ const b1 = node.branches[0]
+ expect(b1.condition!.content).toBe(`ok`)
+ expect(b1.children.length).toBe(1)
+ expect(b1.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((b1.children[0] as ElementNode).tag).toBe(`div`)
+
+ const b2 = node.branches[1]
+ expect(b2.condition!.content).toBe(`orNot`)
+ expect(b2.children.length).toBe(1)
+ expect(b2.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((b2.children[0] as ElementNode).tag).toBe(`p`)
+ })
+
+ test('v-if + v-else-if + v-else', () => {
+ const ast = parse(
+ `<div v-if="ok"/><p v-else-if="orNot"/><template v-else>fine</template>`
+ )
+ transform(ast, {
+ transforms: [transformIf]
+ })
+ // should fold branches
+ expect(ast.children.length).toBe(1)
+
+ const node = ast.children[0] as IfNode
+ expect(node.type).toBe(NodeTypes.IF)
+ expect(node.branches.length).toBe(3)
+
+ const b1 = node.branches[0]
+ expect(b1.condition!.content).toBe(`ok`)
+ expect(b1.children.length).toBe(1)
+ expect(b1.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((b1.children[0] as ElementNode).tag).toBe(`div`)
+
+ const b2 = node.branches[1]
+ expect(b2.condition!.content).toBe(`orNot`)
+ expect(b2.children.length).toBe(1)
+ expect(b2.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((b2.children[0] as ElementNode).tag).toBe(`p`)
+
+ const b3 = node.branches[2]
+ expect(b3.condition).toBeUndefined()
+ expect(b3.children.length).toBe(1)
+ expect(b3.children[0].type).toBe(NodeTypes.TEXT)
+ expect((b3.children[0] as TextNode).content).toBe(`fine`)
+ })
+
+ test('comment between branches', () => {
+ const ast = parse(`
+ <div v-if="ok"/>
+ <!--foo-->
+ <p v-else-if="orNot"/>
+ <!--bar-->
+ <template v-else>fine</template>
+ `)
+ transform(ast, {
+ transforms: [transformIf]
+ })
+ // should fold branches
+ expect(ast.children.length).toBe(1)
+
+ const node = ast.children[0] as IfNode
+ expect(node.type).toBe(NodeTypes.IF)
+ expect(node.branches.length).toBe(3)
+
+ const b1 = node.branches[0]
+ expect(b1.condition!.content).toBe(`ok`)
+ expect(b1.children.length).toBe(1)
+ expect(b1.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((b1.children[0] as ElementNode).tag).toBe(`div`)
+
+ const b2 = node.branches[1]
+ expect(b2.condition!.content).toBe(`orNot`)
+ expect(b2.children.length).toBe(2)
+ expect(b2.children[0].type).toBe(NodeTypes.COMMENT)
+ expect((b2.children[0] as CommentNode).content).toBe(`foo`)
+ expect(b2.children[1].type).toBe(NodeTypes.ELEMENT)
+ expect((b2.children[1] as ElementNode).tag).toBe(`p`)
+
+ const b3 = node.branches[2]
+ expect(b3.condition).toBeUndefined()
+ expect(b3.children.length).toBe(2)
+ expect(b3.children[0].type).toBe(NodeTypes.COMMENT)
+ expect((b3.children[0] as CommentNode).content).toBe(`bar`)
+ expect(b3.children[1].type).toBe(NodeTypes.TEXT)
+ expect((b3.children[1] as TextNode).content).toBe(`fine`)
+ })
+
+ test('error on v-else missing adjacent v-if', () => {
+ const ast = parse(`<div v-else/>`)
+ const spy = jest.fn()
+ transform(ast, {
+ transforms: [transformIf],
+ onError: spy
+ })
+ expect(spy.mock.calls[0]).toMatchObject([
+ {
+ code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
+ loc: ast.children[0].loc.start
+ }
+ ])
+
+ const ast2 = parse(`<div/><div v-else/>`)
+ const spy2 = jest.fn()
+ transform(ast2, {
+ transforms: [transformIf],
+ onError: spy2
+ })
+ expect(spy2.mock.calls[0]).toMatchObject([
+ {
+ code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
+ loc: ast2.children[1].loc.start
+ }
+ ])
+
+ const ast3 = parse(`<div/>foo<div v-else/>`)
+ const spy3 = jest.fn()
+ transform(ast3, {
+ transforms: [transformIf],
+ onError: spy3
+ })
+ expect(spy3.mock.calls[0]).toMatchObject([
+ {
+ code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
+ loc: ast3.children[2].loc.start
+ }
+ ])
+ })
+
+ test('error on v-else-if missing adjacent v-if', () => {
+ const ast = parse(`<div v-else-if="foo"/>`)
+ const spy = jest.fn()
+ transform(ast, {
+ transforms: [transformIf],
+ onError: spy
+ })
+ expect(spy.mock.calls[0]).toMatchObject([
+ {
+ code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
+ loc: ast.children[0].loc.start
+ }
+ ])
+
+ const ast2 = parse(`<div/><div v-else-if="foo"/>`)
+ const spy2 = jest.fn()
+ transform(ast2, {
+ transforms: [transformIf],
+ onError: spy2
+ })
+ expect(spy2.mock.calls[0]).toMatchObject([
+ {
+ code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
+ loc: ast2.children[1].loc.start
+ }
+ ])
+
+ const ast3 = parse(`<div/>foo<div v-else-if="foo"/>`)
+ const spy3 = jest.fn()
+ transform(ast3, {
+ transforms: [transformIf],
+ onError: spy3
+ })
+ expect(spy3.mock.calls[0]).toMatchObject([
+ {
+ code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
+ loc: ast3.children[2].loc.start
+ }
+ ])
+ })
+ })
+
+ describe('codegen', () => {
+ // TODO
+ })
+})
{
parent: ast,
ancestors: [ast],
- childIndex: 0
+ currentNode: div
}
])
expect(calls[1]).toMatchObject([
{
parent: div,
ancestors: [ast, div],
- childIndex: 0
+ currentNode: div.children[0]
}
])
expect(calls[2]).toMatchObject([
{
parent: div,
ancestors: [ast, div],
- childIndex: 1
+ currentNode: div.children[1]
}
])
})
})
test('context.removeNode', () => {
- const ast = parse(`<span/><div/><span/>`)
+ const ast = parse(`<span/><div>hello</div><span/>`)
const c1 = ast.children[0]
const c2 = ast.children[2]
expect(ast.children[0]).toBe(c1)
expect(ast.children[1]).toBe(c2)
+ // should not traverse children of remove node
expect(spy).toHaveBeenCalledTimes(3)
// should traverse nodes around removed
expect(spy.mock.calls[0][0]).toBe(c1)
expect(spy.mock.calls[2][0]).toBe(c2)
})
+ test('context.removeNode (prev sibling)', () => {
+ const ast = parse(`<span/><div/><span/>`)
+ const c1 = ast.children[0]
+ const c2 = ast.children[2]
+
+ const plugin: Transform = (node, context) => {
+ if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
+ context.removeNode()
+ // remove previous sibling
+ context.removeNode(context.parent.children[0])
+ }
+ }
+ const spy = jest.fn(plugin)
+ transform(ast, {
+ transforms: [spy]
+ })
+
+ expect(ast.children.length).toBe(1)
+ expect(ast.children[0]).toBe(c2)
+
+ expect(spy).toHaveBeenCalledTimes(3)
+ // should still traverse first span before removal
+ expect(spy.mock.calls[0][0]).toBe(c1)
+ // should still traverse last span
+ expect(spy.mock.calls[2][0]).toBe(c2)
+ })
+
+ test('context.removeNode (next sibling)', () => {
+ const ast = parse(`<span/><div/><span/>`)
+ const c1 = ast.children[0]
+ const d1 = ast.children[1]
+
+ const plugin: Transform = (node, context) => {
+ if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
+ context.removeNode()
+ // remove next sibling
+ context.removeNode(context.parent.children[1])
+ }
+ }
+ const spy = jest.fn(plugin)
+ transform(ast, {
+ transforms: [spy]
+ })
+
+ expect(ast.children.length).toBe(1)
+ expect(ast.children[0]).toBe(c1)
+
+ expect(spy).toHaveBeenCalledTimes(2)
+ // should still traverse first span before removal
+ expect(spy.mock.calls[0][0]).toBe(c1)
+ // should not traverse last span
+ expect(spy.mock.calls[1][0]).toBe(d1)
+ })
+
test('onError option', () => {
const ast = parse(`<div/>`)
const loc = ast.children[0].loc.start
parent: ParentNode
ancestors: ParentNode[]
childIndex: number
+ currentNode: ChildNode | null
replaceNode(node: ChildNode): void
- removeNode(): void
- nodeRemoved: boolean
+ removeNode(node?: ChildNode): void
+ onNodeRemoved: () => void
}
export function transform(root: RootNode, options: TransformOptions) {
parent: root,
ancestors: [],
childIndex: 0,
+ currentNode: null,
replaceNode(node) {
- if (__DEV__ && context.nodeRemoved) {
- throw new Error(`node being replaced is already removed`)
+ if (__DEV__ && !context.currentNode) {
+ throw new Error(`node being replaced is already removed.`)
}
- context.parent.children[context.childIndex] = node
+ context.parent.children[context.childIndex] = context.currentNode = node
},
- removeNode() {
- context.parent.children.splice(context.childIndex, 1)
- context.nodeRemoved = true
+ removeNode(node) {
+ const list = context.parent.children
+ const removalIndex = node
+ ? list.indexOf(node)
+ : context.currentNode
+ ? context.childIndex
+ : -1
+ if (__DEV__ && removalIndex < 0) {
+ throw new Error(`node being removed is not a child of current parent`)
+ }
+ if (!node || node === context.currentNode) {
+ // current node removed
+ context.currentNode = null
+ context.onNodeRemoved()
+ } else {
+ // sibling node removed
+ if (context.childIndex > removalIndex) {
+ context.childIndex--
+ context.onNodeRemoved()
+ }
+ }
+ context.parent.children.splice(removalIndex, 1)
},
- nodeRemoved: false
+ onNodeRemoved: () => {}
}
return context
}
ancestors: ParentNode[]
) {
ancestors = ancestors.concat(parent)
- for (let i = 0; i < parent.children.length; i++) {
+ let i = 0
+ const nodeRemoved = () => {
+ i--
+ }
+ for (; i < parent.children.length; i++) {
context.parent = parent
context.ancestors = ancestors
context.childIndex = i
- traverseNode(parent.children[i], context, ancestors)
- if (context.nodeRemoved) {
- i--
- }
+ context.onNodeRemoved = nodeRemoved
+ traverseNode((context.currentNode = parent.children[i]), context, ancestors)
}
}
const transforms = context.transforms
for (let i = 0; i < transforms.length; i++) {
const plugin = transforms[i]
- context.nodeRemoved = false
plugin(node, context)
- if (context.nodeRemoved) {
+ if (!context.currentNode) {
return
} else {
// node may have been replaced
- node = context.parent.children[context.childIndex]
+ node = context.currentNode
}
}