import {
ElementNode,
ErrorCodes,
InterpolationNode,
NodeTypes,
SimpleExpressionNode,
} from '@vue/compiler-core'
import { compile } from '../src'
import { CompilerOptions } from '../src/options'
import { ForElementNode } from '../src/transforms/vFor'
import { assert } from './testUtils'
function parseWithForTransform(
template: string,
options: CompilerOptions = {}
) {
const { ast } = compile(template, options)
return {
root: ast,
node: ast.children[0] as ForElementNode,
}
}
describe(`compiler: v-for`, () => {
describe(`codegen`, () => {
test(`number expression`, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(5, (index, k0, i0) => { return {}; }) }
}`
)
})
test(`value`, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (item, k0, i0) => { return {}; }) }
}`
)
})
test('object de-structured value', () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, ({ id, value }, k0, i0) => { return {}; }) }
}`
)
})
test('array de-structured value', () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, ([id, value], k0, i0) => { return {}; }) }
}`
)
})
test(`value and key`, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (item, key, i0) => { return {}; }) }
}`
)
})
test(`value, key and index`, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (item, key, index) => { return {}; }) }
}`
)
})
test(`skipped key`, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (value, k0, index) => { return {}; }) }
}`
)
})
test(`skipped value and key`, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (v0, k0, index) => { return {}; }) }
}`
)
})
test(`unbracketed value`, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (item, k0, i0) => { return {}; }) }
}`
)
})
test(`unbracketed value and key`, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (item, key, i0) => { return {}; }) }
}`
)
})
test(`unbracketed value, key and index`, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (value, key, index) => { return {}; }) }
}`
)
})
test(`unbracketed skipped key`, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (value, k0, index) => { return {}; }) }
}`
)
})
test(`unbracketed skipped value and key`, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (v0, k0, index) => { return {}; }) }
}`
)
})
test(`template v-for`, () => {
assert(
`hello`,
`hello`,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (item, k0, i0) => { return {}; }) }
}`
)
})
test(`template v-for w/ `, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (item, k0, i0) => { return {}; }) }
}`
)
})
// #1907 TODO 待优化
test(`template v-for key injection with single child`, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (item, k0, i0) => { return { a: item.id, b: item.id }; }) }
}`
)
})
test(`v-for on `, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (item, k0, i0) => { return {}; }) }
}`
)
})
test(`keyed v-for`, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (item, k0, i0) => { return {}; }) }
}`
)
})
test(`keyed template v-for`, () => {
assert(
`hello`,
`hello`,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (item, k0, i0) => { return {}; }) }
}`
)
})
test(`v-if + v-for`, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return _extend({ a: _ctx.ok }, _ctx.ok ? { b: _vFor(_ctx.list, (i, k0, i0) => { return {}; }) } : {})
}`
)
})
// 1637
test(`v-if + v-for on `, () => {
assert(
``,
``,
`(_ctx, _cache) => {
return _extend({ a: _ctx.ok }, _ctx.ok ? { b: _vFor(_ctx.list, (i, k0, i0) => { return {}; }) } : {})
}`
)
})
test(`v-for on element with custom directive`, () => {
//
})
})
describe('errors', () => {
test('missing expression', () => {
const onError = jest.fn()
parseWithForTransform('', { onError })
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
code: ErrorCodes.X_V_FOR_NO_EXPRESSION,
})
)
})
test('empty expression', () => {
const onError = jest.fn()
parseWithForTransform('', { onError })
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
code: ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION,
})
)
})
test('invalid expression', () => {
const onError = jest.fn()
parseWithForTransform('', { onError })
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
code: ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION,
})
)
})
test('missing source', () => {
const onError = jest.fn()
parseWithForTransform('', { onError })
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
code: ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION,
})
)
})
test('missing value', () => {
const onError = jest.fn()
parseWithForTransform('', { onError })
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
code: ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION,
})
)
})
test(' key placement', () => {
const onError = jest.fn()
parseWithForTransform(
`
`,
{ onError }
)
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
code: ErrorCodes.X_V_FOR_TEMPLATE_KEY_PLACEMENT,
})
)
// should not warn on nested v-for keys
parseWithForTransform(
`
`,
{ onError }
)
expect(onError).toHaveBeenCalledTimes(1)
})
})
describe('source location', () => {
test('value & source', () => {
const source = ''
const {
node: { vFor: forNode },
} = parseWithForTransform(source)
const itemOffset = source.indexOf('item')
const value = forNode.value as SimpleExpressionNode
expect(forNode.valueAlias).toBe('item')
expect(value.loc.start.offset).toBe(itemOffset)
expect(value.loc.start.line).toBe(1)
expect(value.loc.start.column).toBe(itemOffset + 1)
expect(value.loc.end.line).toBe(1)
expect(value.loc.end.column).toBe(itemOffset + 1 + `item`.length)
const itemsOffset = source.indexOf('items')
expect((forNode.source as SimpleExpressionNode).content).toBe('items')
expect(forNode.source.loc.start.offset).toBe(itemsOffset)
expect(forNode.source.loc.start.line).toBe(1)
expect(forNode.source.loc.start.column).toBe(itemsOffset + 1)
expect(forNode.source.loc.end.line).toBe(1)
expect(forNode.source.loc.end.column).toBe(
itemsOffset + 1 + `items`.length
)
})
test('bracketed value', () => {
const source = ''
const {
node: { vFor: forNode },
} = parseWithForTransform(source)
const itemOffset = source.indexOf('item')
const value = forNode.value as SimpleExpressionNode
expect(value.content).toBe('item')
expect(value.loc.start.offset).toBe(itemOffset)
expect(value.loc.start.line).toBe(1)
expect(value.loc.start.column).toBe(itemOffset + 1)
expect(value.loc.end.line).toBe(1)
expect(value.loc.end.column).toBe(itemOffset + 1 + `item`.length)
const itemsOffset = source.indexOf('items')
expect((forNode.source as SimpleExpressionNode).content).toBe('items')
expect(forNode.source.loc.start.offset).toBe(itemsOffset)
expect(forNode.source.loc.start.line).toBe(1)
expect(forNode.source.loc.start.column).toBe(itemsOffset + 1)
expect(forNode.source.loc.end.line).toBe(1)
expect(forNode.source.loc.end.column).toBe(
itemsOffset + 1 + `items`.length
)
})
test('de-structured value', () => {
const source = ''
const {
node: { vFor: forNode },
} = parseWithForTransform(source)
const value = forNode.value as SimpleExpressionNode
const valueIndex = source.indexOf('{ id, key }')
expect(value.content).toBe('{ id, key }')
expect(value.loc.start.offset).toBe(valueIndex)
expect(value.loc.start.line).toBe(1)
expect(value.loc.start.column).toBe(valueIndex + 1)
expect(value.loc.end.line).toBe(1)
expect(value.loc.end.column).toBe(valueIndex + 1 + '{ id, key }'.length)
const itemsOffset = source.indexOf('items')
expect((forNode.source as SimpleExpressionNode).content).toBe('items')
expect(forNode.source.loc.start.offset).toBe(itemsOffset)
expect(forNode.source.loc.start.line).toBe(1)
expect(forNode.source.loc.start.column).toBe(itemsOffset + 1)
expect(forNode.source.loc.end.line).toBe(1)
expect(forNode.source.loc.end.column).toBe(
itemsOffset + 1 + `items`.length
)
})
test('bracketed value, key, index', () => {
const source = ''
const {
node: { vFor: forNode },
} = parseWithForTransform(source)
const itemOffset = source.indexOf('item')
const value = forNode.value as SimpleExpressionNode
expect(value.content).toBe('item')
expect(value.loc.start.offset).toBe(itemOffset)
expect(value.loc.start.line).toBe(1)
expect(value.loc.start.column).toBe(itemOffset + 1)
expect(value.loc.end.line).toBe(1)
expect(value.loc.end.column).toBe(itemOffset + 1 + `item`.length)
const keyOffset = source.indexOf('key')
const key = forNode.key as SimpleExpressionNode
expect(key.content).toBe('key')
expect(key.loc.start.offset).toBe(keyOffset)
expect(key.loc.start.line).toBe(1)
expect(key.loc.start.column).toBe(keyOffset + 1)
expect(key.loc.end.line).toBe(1)
expect(key.loc.end.column).toBe(keyOffset + 1 + `key`.length)
const indexOffset = source.indexOf('index')
const index = forNode.index as SimpleExpressionNode
expect(index.content).toBe('index')
expect(index.loc.start.offset).toBe(indexOffset)
expect(index.loc.start.line).toBe(1)
expect(index.loc.start.column).toBe(indexOffset + 1)
expect(index.loc.end.line).toBe(1)
expect(index.loc.end.column).toBe(indexOffset + 1 + `index`.length)
const itemsOffset = source.indexOf('items')
expect((forNode.source as SimpleExpressionNode).content).toBe('items')
expect(forNode.source.loc.start.offset).toBe(itemsOffset)
expect(forNode.source.loc.start.line).toBe(1)
expect(forNode.source.loc.start.column).toBe(itemsOffset + 1)
expect(forNode.source.loc.end.line).toBe(1)
expect(forNode.source.loc.end.column).toBe(
itemsOffset + 1 + `items`.length
)
})
test('skipped key', () => {
const source = ''
const {
node: { vFor: forNode },
} = parseWithForTransform(source)
const itemOffset = source.indexOf('item')
const value = forNode.value as SimpleExpressionNode
expect(value.content).toBe('item')
expect(value.loc.start.offset).toBe(itemOffset)
expect(value.loc.start.line).toBe(1)
expect(value.loc.start.column).toBe(itemOffset + 1)
expect(value.loc.end.line).toBe(1)
expect(value.loc.end.column).toBe(itemOffset + 1 + `item`.length)
const indexOffset = source.indexOf('index')
const index = forNode.index as SimpleExpressionNode
expect(index.content).toBe('index')
expect(index.loc.start.offset).toBe(indexOffset)
expect(index.loc.start.line).toBe(1)
expect(index.loc.start.column).toBe(indexOffset + 1)
expect(index.loc.end.line).toBe(1)
expect(index.loc.end.column).toBe(indexOffset + 1 + `index`.length)
const itemsOffset = source.indexOf('items')
expect((forNode.source as SimpleExpressionNode).content).toBe('items')
expect(forNode.source.loc.start.offset).toBe(itemsOffset)
expect(forNode.source.loc.start.line).toBe(1)
expect(forNode.source.loc.start.column).toBe(itemsOffset + 1)
expect(forNode.source.loc.end.line).toBe(1)
expect(forNode.source.loc.end.column).toBe(
itemsOffset + 1 + `items`.length
)
})
})
describe('prefixIdentifiers: true', () => {
test('should prefix v-for source', () => {
const { node } = parseWithForTransform(``, {
prefixIdentifiers: true,
skipTransformIdentifier: true,
})
expect(node.vFor.source).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.list`,
})
})
test('should prefix v-for source w/ complex expression', () => {
const { node } = parseWithForTransform(
``,
{
prefixIdentifiers: true,
skipTransformIdentifier: true,
}
)
expect(node.vFor.source).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `_ctx.list` },
`.`,
{ content: `concat` },
`([`,
{ content: `_ctx.foo` },
`])`,
],
})
})
test('should not prefix v-for alias', () => {
const { node } = parseWithForTransform(
`{{ i }}{{ j }}`,
{ prefixIdentifiers: true, skipTransformIdentifier: true }
)
const view = node as ElementNode
expect((view.children[0] as InterpolationNode).content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `i`,
})
expect((view.children[1] as InterpolationNode).content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.j`,
})
})
test('should not prefix v-for aliases (multiple)', () => {
const { node } = parseWithForTransform(
`{{ i + j + k }}{{ l }}`,
{ prefixIdentifiers: true, skipTransformIdentifier: true }
)
const view = node as ElementNode
expect((view.children[0] as InterpolationNode).content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `i` },
` + `,
{ content: `j` },
` + `,
{ content: `k` },
],
})
expect((view.children[1] as InterpolationNode).content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.l`,
})
})
test('should prefix id outside of v-for', () => {
const { node } = parseWithForTransform(
`{{ i }}`,
{ prefixIdentifiers: true, skipTransformIdentifier: true }
)
expect((node.children[1] as InterpolationNode).content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.i`,
})
})
test('nested v-for', () => {
const { node } = parseWithForTransform(
`
{{ i + j }}{{ i }}
`,
{ prefixIdentifiers: true, skipTransformIdentifier: true }
)
const outerDiv = node as ElementNode
const innerExp = (outerDiv.children[0] as ElementNode)
.children[0] as InterpolationNode
expect(innerExp.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [{ content: 'i' }, ` + `, { content: `_ctx.j` }],
})
// when an inner v-for shadows a variable of an outer v-for and exit,
// it should not cause the outer v-for's alias to be removed from known ids
const outerExp = outerDiv.children[1] as InterpolationNode
expect(outerExp.content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `i`,
})
})
test('v-for aliases w/ complex expressions', () => {
const { node } = parseWithForTransform(
`
{{ foo + bar + baz + qux + quux }}
`,
{ prefixIdentifiers: true, skipTransformIdentifier: true }
)
expect(node.vFor.value).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`{ `,
{ content: `foo` },
` = `,
{ content: `_ctx.bar` },
`, baz: [`,
{ content: `qux` },
` = `,
{ content: `_ctx.quux` },
`] }`,
],
})
const view = node as ElementNode
expect((view.children[0] as InterpolationNode).content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `foo` },
` + `,
{ content: `_ctx.bar` },
` + `,
{ content: `_ctx.baz` },
` + `,
{ content: `qux` },
` + `,
{ content: `_ctx.quux` },
],
})
})
test('element v-for key expression prefixing', () => {
assert(
`test`,
`test`,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (item, k0, i0) => { return { a: _ctx.itemKey(item) }; }) }
}`
)
})
// #2085
test('template v-for key expression prefixing', () => {
assert(
`test`,
`test`,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (item, k0, i0) => { return { a: _ctx.itemKey(item) }; }) }
}`
)
})
test('template v-for key no prefixing on attribute key', () => {
assert(
`test`,
`test`,
`(_ctx, _cache) => {
return { a: _vFor(_ctx.items, (item, k0, i0) => { return {}; }) }
}`
)
})
})
})