Back to Repositories

Testing v-For Directive Implementation in dcloudio/uni-app

This test suite validates v-for directive functionality in the uni-app framework, focusing on transformation and code generation for various v-for syntax patterns and use cases.

Test Coverage Overview

The test suite provides comprehensive coverage of v-for directive implementations:

  • Basic v-for syntax with value, key, and index variables
  • Template v-for usage patterns
  • Array and object iteration scenarios
  • Nested v-for implementations
  • Integration with v-if and custom directives
  • Key handling and fragment management

Implementation Analysis

The testing approach focuses on transformation and code generation aspects:

The suite uses a parseWithForTransform utility to handle AST transformation and validate the generated code structure. It verifies proper scope handling, variable aliasing, and directive compilation across different syntax patterns.

Technical Details

Testing tools and configuration:

  • Jest test framework for assertions
  • Custom AST transformation utilities
  • Vue compiler core integration
  • Code generation validation
  • Snapshot testing for output verification

Best Practices Demonstrated

The test suite demonstrates several testing best practices:

  • Comprehensive edge case coverage
  • Isolated transformation testing
  • Detailed assertion patterns
  • Snapshot-based verification
  • Clear test organization by functionality

dcloudio/uni-app

packages/uni-app-uts/__tests__/android/transforms/vFor.spec.ts

            
import { PatchFlags, extend } from '@vue/shared'
import {
  type CompilerOptions,
  ConstantTypes,
  type ElementNode,
  ErrorCodes,
  type ForCodegenNode,
  type ForNode,
  type InterpolationNode,
  NodeTypes,
  type SimpleExpressionNode,
  baseParse as parse,
  transform,
  transformElement,
} from '@vue/compiler-core'
import { transformIf } from '../../../src/plugins/android/uvue/compiler/transforms/vIf'
import { transformFor } from '../../../src/plugins/android/uvue/compiler/transforms/vFor'
import { transformSlotOutlet } from '../../../src/plugins/android/uvue/compiler/transforms/transformSlotOutlet'
import { transformExpression } from '../../../src/plugins/android/uvue/compiler/transforms/transformExpression'
import { transformBind } from '../../../src/plugins/android/uvue/compiler/transforms/vBind'
import { assert, createObjectMatcher, genFlagText } from '../testUtils'
import { generate } from '../../../src/plugins/android/uvue/compiler/codegen'
import {
  FRAGMENT,
  RENDER_LIST,
  RENDER_SLOT,
} from '../../../src/plugins/android/uvue/compiler/runtimeHelpers'

function parseWithForTransform(
  template: string,
  options: CompilerOptions = {}
) {
  const ast = parse(template, options)
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers: options.prefixIdentifiers,
      nodeTransforms: [
        transformIf,
        transformFor,
        transformExpression,
        transformSlotOutlet,
        transformElement,
        ...(options.nodeTransforms || []), // user transforms
      ],
      directiveTransforms: extend(
        {},
        { bind: transformBind },
        options.directiveTransforms || {} // user transforms
      ),
    })
  )
  return {
    root: ast,
    node: ast.children[0] as ForNode & { codegenNode: ForCodegenNode },
  }
}

describe('compiler: v-for', () => {
  test('number expression', () => {
    assert(
      `<text v-for="item in 10" :key="item" />`,
      `createElementVNode(Fragment, null, RenderHelpers.renderList(10, (item, __key, __index, _cached): any => {
  return createElementVNode("text", utsMapOf({ key: item }))
}), 64 /* STABLE_FRAGMENT */)`
    )
  })
  test('value', () => {
    assert(
      `<text v-for="(item) in items" />`,
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (item, __key, __index, _cached): any => {
  return createElementVNode("text")
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('value and key', () => {
    assert(
      `<text v-for="(item, index) in [1,2,3]" :key="index" />`,
      `createElementVNode(Fragment, null, RenderHelpers.renderList([1,2,3], (item, index, __index, _cached): any => {
  return createElementVNode("text", utsMapOf({ key: index }))
}), 64 /* STABLE_FRAGMENT */)`
    )
  })
  test('value, key and index', () => {
    assert(
      `<text v-for="(item, key, index) in {a:'a',b:'b'}" :key="index" />`,
      `createElementVNode(Fragment, null, RenderHelpers.renderList({a:'a',b:'b'}, (item, key, index, _cached): any => {
  return createElementVNode("text", utsMapOf({ key: index }))
}), 64 /* STABLE_FRAGMENT */)`
    )
  })
  test('array de-structured value', () => {
    assert(
      '<text v-for="([ id, value ]) in items" />',
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, ([ id, value ], __key, __index, _cached): any => {
  return createElementVNode(\"text\")
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('object de-structured value', () => {
    assert(
      '<text v-for="({ id, value }) in items" />',
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, ({ id, value }, __key, __index, _cached): any => {
  return createElementVNode("text")
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('skipped value', () => {
    assert(
      '<text v-for="(,key,index) in items" />',
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (__value, key, index, _cached): any => {
  return createElementVNode("text")
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('skipped key', () => {
    assert(
      '<text v-for="(value,,index) in items" />',
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (value, __key, index, _cached): any => {
  return createElementVNode("text")
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('skipped value & key', () => {
    assert(
      '<text v-for="(,,index) in items" />',
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (__value, __key, index, _cached): any => {
  return createElementVNode("text")
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('skipped value and key', () => {
    assert(
      '<text v-for="(,,index) in items" />',
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (__value, __key, index, _cached): any => {
  return createElementVNode("text")
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('unbracketed value', () => {
    assert(
      '<text v-for="item in items" />',
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (item, __key, __index, _cached): any => {
  return createElementVNode(\"text\")
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('unbracketed value and key', () => {
    assert(
      '<text v-for="item, key in items" />',
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (item, key, __index, _cached): any => {
  return createElementVNode(\"text\")
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('unbracketed value and key', () => {
    assert(
      '<text v-for="value, key, index in items" />',
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (value, key, index, _cached): any => {
  return createElementVNode(\"text\")
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('unbracketed skipped key', () => {
    assert(
      '<text v-for="value, , index in items" />',
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (value, __key, index, _cached): any => {
  return createElementVNode(\"text\")
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('unbracketed skipped value and key', () => {
    assert(
      '<text v-for=", , index in items" />',
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (__value, __key, index, _cached): any => {
  return createElementVNode(\"text\")
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('source complex expression', () => {
    assert(
      '<text v-for="i in list.concat([foo])" />',
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.list.concat([_ctx.foo]), (i, __key, __index, _cached): any => {
  return createElementVNode(\"text\")
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('should not prefix v-for alias', () => {
    assert(
      '<text v-for="i in list">{{ i }}{{ j }}</text>',
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.list, (i, __key, __index, _cached): any => {
  return createElementVNode("text", null, toDisplayString(i) + toDisplayString(_ctx.j), 1 /* TEXT */)
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('should not prefix v-for aliases (multiple)', () => {
    assert(
      '<text v-for="(i, j, k) in list">{{ i + j + k }}{{ l }}</text>',
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.list, (i, j, k, _cached): any => {
  return createElementVNode("text", null, toDisplayString(i + j + k) + toDisplayString(_ctx.l), 1 /* TEXT */)
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('should prefix id outside of v-for', () => {
    assert(
      '<text><text v-for="i in list" />{{ i }}</text>',
      `createElementVNode("text", null, [
  createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.list, (i, __key, __index, _cached): any => {
    return createElementVNode("text")
  }), 256 /* UNKEYED_FRAGMENT */),
  toDisplayString(_ctx.i)
])`
    )
  })
  test('nested v-for', () => {
    assert(
      `<view v-for="i in list">
  <text v-for="i in list">{{ i + j }}</text>{{ i }}
</view>`,
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.list, (i, __key, __index, _cached): any => {
  return createElementVNode(\"view\", null, [
    createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.list, (i, __key, __index, _cached): any => {
      return createElementVNode(\"text\", null, toDisplayString(i + _ctx.j), 1 /* TEXT */)
    }), 256 /* UNKEYED_FRAGMENT */),
    toDisplayString(i)
  ])
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('v-for aliases w/ complex expressions', () => {
    assert(
      `<text v-for="({ foo = bar, baz: [qux = quux] }) in list">
  {{ foo + bar + baz + qux + quux }}
</text>`,
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.list, ({ foo = _ctx.bar, baz: [qux = _ctx.quux] }, __key, __index, _cached): any => {
  return createElementVNode(\"text\", null, toDisplayString(foo + _ctx.bar + _ctx.baz + qux + _ctx.quux), 1 /* TEXT */)
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('element v-for key expression prefixing', () => {
    assert(
      `<text v-for="item in items" :key="itemKey(item)">test</text>`,
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (item, __key, __index, _cached): any => {
  return createElementVNode(\"text\", utsMapOf({
    key: _ctx.itemKey(item)
  }), \"test\")
}), 128 /* KEYED_FRAGMENT */)`
    )
  })
  test('template v-for key no prefixing on attribute key', () => {
    assert(
      `<template v-for="item in items" key="key">test</template>`,
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (item, __key, __index, _cached): any => {
  return createElementVNode(Fragment, utsMapOf({ key: "key" }), [\"test\"], 64 /* STABLE_FRAGMENT */)
}), 128 /* KEYED_FRAGMENT */)`
    )
  })
  test('template v-for key injection with single child', () => {
    assert(
      `<template v-for="item in items" :key="item.id"><text :id="item.id" /></template>`,
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (item, __key, __index, _cached): any => {
  return createElementVNode(\"text\", utsMapOf({
    key: item.id,
    id: item.id
  }), null, 8 /* PROPS */, [\"id\"])
}), 128 /* KEYED_FRAGMENT */)`
    )
  })
  test('v-for on <slot/>', () => {
    assert(
      `<slot v-for="item in items"></slot>`,
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (item, __key, __index, _cached): any => {
  return renderSlot(_ctx.$slots, \"default\")
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  test('keyed v-for', () => {
    assert(
      `<text v-for="(item) in items" :key="item" />`,
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (item, __key, __index, _cached): any => {
  return createElementVNode(\"text\", utsMapOf({ key: item }))
}), 128 /* KEYED_FRAGMENT */)`
    )
  })
  test('keyed template v-for', () => {
    assert(
      `<template v-for="(item) in items" :key="item"><text/></template>`,
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.items, (item, __key, __index, _cached): any => {
  return createElementVNode(\"text\", utsMapOf({ key: item }))
}), 128 /* KEYED_FRAGMENT */)`
    )
  })
  test('v-if + v-for', () => {
    assert(
      `<view v-if="ok" v-for="i in list"/>`,
      `isTrue(_ctx.ok)
  ? createElementVNode(Fragment, utsMapOf({ key: 0 }), RenderHelpers.renderList(_ctx.list, (i, __key, __index, _cached): any => {
      return createElementVNode(\"view\")
    }), 256 /* UNKEYED_FRAGMENT */)
  : createCommentVNode(\"v-if\", true)`
    )
  })
  test('v-if + v-for on <template>', () => {
    assert(
      `<template v-if="ok" v-for="i in list"/>`,
      `isTrue(_ctx.ok)
  ? createElementVNode(Fragment, utsMapOf({ key: 0 }), RenderHelpers.renderList(_ctx.list, (i, __key, __index, _cached): any => {
      return createElementVNode(Fragment, null, [], 64 /* STABLE_FRAGMENT */)
    }), 256 /* UNKEYED_FRAGMENT */)
  : createCommentVNode(\"v-if\", true)`
    )
  })
  test('v-for on element with custom directive', () => {
    assert(
      `<view v-for="i in list" v-foo/>`,
      `createElementVNode(Fragment, null, RenderHelpers.renderList(_ctx.list, (i, __key, __index, _cached): any => {
  return withDirectives(createElementVNode(\"view\", null, null, 512 /* NEED_PATCH */), [
    [_directive_foo]
  ])
}), 256 /* UNKEYED_FRAGMENT */)`
    )
  })
  describe('transform', () => {
    test('number expression', () => {
      const { node: forNode } = parseWithForTransform(
        '<text v-for="index in 5" />'
      )
      expect(forNode.keyAlias).toBeUndefined()
      expect(forNode.objectIndexAlias).toBeUndefined()
      expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('index')
      expect((forNode.source as SimpleExpressionNode).content).toBe('5')
    })

    test('value', () => {
      const { node: forNode } = parseWithForTransform(
        '<text v-for="(item) in items" />'
      )
      expect(forNode.keyAlias).toBeUndefined()
      expect(forNode.objectIndexAlias).toBeUndefined()
      expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('item')
      expect((forNode.source as SimpleExpressionNode).content).toBe('items')
    })

    test('object de-structured value', () => {
      const { node: forNode } = parseWithForTransform(
        '<text v-for="({ id, value }) in items" />'
      )
      expect(forNode.keyAlias).toBeUndefined()
      expect(forNode.objectIndexAlias).toBeUndefined()
      expect((forNode.valueAlias as SimpleExpressionNode).content).toBe(
        '{ id, value }'
      )
      expect((forNode.source as SimpleExpressionNode).content).toBe('items')
    })

    test('array de-structured value', () => {
      const { node: forNode } = parseWithForTransform(
        '<text v-for="([ id, value ]) in items" />'
      )
      expect(forNode.keyAlias).toBeUndefined()
      expect(forNode.objectIndexAlias).toBeUndefined()
      expect((forNode.valueAlias as SimpleExpressionNode).content).toBe(
        '[ id, value ]'
      )
      expect((forNode.source as SimpleExpressionNode).content).toBe('items')
    })

    test('value and key', () => {
      const { node: forNode } = parseWithForTransform(
        '<text v-for="(item, key) in items" />'
      )
      expect(forNode.keyAlias).not.toBeUndefined()
      expect((forNode.keyAlias as SimpleExpressionNode).content).toBe('key')
      expect(forNode.objectIndexAlias).toBeUndefined()
      expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('item')
      expect((forNode.source as SimpleExpressionNode).content).toBe('items')
    })

    test('value, key and index', () => {
      const { node: forNode } = parseWithForTransform(
        '<text v-for="(value, key, index) in items" />'
      )
      expect(forNode.keyAlias).not.toBeUndefined()
      expect((forNode.keyAlias as SimpleExpressionNode).content).toBe('key')
      expect(forNode.objectIndexAlias).not.toBeUndefined()
      expect((forNode.objectIndexAlias as SimpleExpressionNode).content).toBe(
        'index'
      )
      expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('value')
      expect((forNode.source as SimpleExpressionNode).content).toBe('items')
    })

    test('skipped key', () => {
      const { node: forNode } = parseWithForTransform(
        '<text v-for="(value,,index) in items" />'
      )
      expect(forNode.keyAlias).toBeUndefined()
      expect(forNode.objectIndexAlias).not.toBeUndefined()
      expect((forNode.objectIndexAlias as SimpleExpressionNode).content).toBe(
        'index'
      )
      expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('value')
      expect((forNode.source as SimpleExpressionNode).content).toBe('items')
    })

    test('skipped value and key', () => {
      const { node: forNode } = parseWithForTransform(
        '<text v-for="(,,index) in items" />'
      )
      expect(forNode.keyAlias).toBeUndefined()
      expect(forNode.objectIndexAlias).not.toBeUndefined()
      expect((forNode.objectIndexAlias as SimpleExpressionNode).content).toBe(
        'index'
      )
      expect(forNode.valueAlias).toBeUndefined()
      expect((forNode.source as SimpleExpressionNode).content).toBe('items')
    })

    test('unbracketed value', () => {
      const { node: forNode } = parseWithForTransform(
        '<text v-for="item in items" />'
      )
      expect(forNode.keyAlias).toBeUndefined()
      expect(forNode.objectIndexAlias).toBeUndefined()
      expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('item')
      expect((forNode.source as SimpleExpressionNode).content).toBe('items')
    })

    test('unbracketed value and key', () => {
      const { node: forNode } = parseWithForTransform(
        '<text v-for="item, key in items" />'
      )
      expect(forNode.keyAlias).not.toBeUndefined()
      expect((forNode.keyAlias as SimpleExpressionNode).content).toBe('key')
      expect(forNode.objectIndexAlias).toBeUndefined()
      expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('item')
      expect((forNode.source as SimpleExpressionNode).content).toBe('items')
    })

    test('unbracketed value, key and index', () => {
      const { node: forNode } = parseWithForTransform(
        '<text v-for="value, key, index in items" />'
      )
      expect(forNode.keyAlias).not.toBeUndefined()
      expect((forNode.keyAlias as SimpleExpressionNode).content).toBe('key')
      expect(forNode.objectIndexAlias).not.toBeUndefined()
      expect((forNode.objectIndexAlias as SimpleExpressionNode).content).toBe(
        'index'
      )
      expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('value')
      expect((forNode.source as SimpleExpressionNode).content).toBe('items')
    })

    test('unbracketed skipped key', () => {
      const { node: forNode } = parseWithForTransform(
        '<text v-for="value, , index in items" />'
      )
      expect(forNode.keyAlias).toBeUndefined()
      expect(forNode.objectIndexAlias).not.toBeUndefined()
      expect((forNode.objectIndexAlias as SimpleExpressionNode).content).toBe(
        'index'
      )
      expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('value')
      expect((forNode.source as SimpleExpressionNode).content).toBe('items')
    })

    test('unbracketed skipped value and key', () => {
      const { node: forNode } = parseWithForTransform(
        '<text v-for=", , index in items" />'
      )
      expect(forNode.keyAlias).toBeUndefined()
      expect(forNode.objectIndexAlias).not.toBeUndefined()
      expect((forNode.objectIndexAlias as SimpleExpressionNode).content).toBe(
        'index'
      )
      expect(forNode.valueAlias).toBeUndefined()
      expect((forNode.source as SimpleExpressionNode).content).toBe('items')
    })
  })

  describe('errors', () => {
    test('missing expression', () => {
      const onError = jest.fn()
      parseWithForTransform('<text v-for />', { 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('<text v-for="" />', { 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('<text v-for="items" />', { 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('<text v-for="item in" />', { 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('<text v-for="in items" />', { onError })

      expect(onError).toHaveBeenCalledTimes(1)
      expect(onError).toHaveBeenCalledWith(
        expect.objectContaining({
          code: ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION,
        })
      )
    })

    test('<template v-for> key placement', () => {
      const onError = jest.fn()
      parseWithForTransform(
        `
      <template v-for="item in items">
        <view :key="item.id"/>
      </template>`,
        { 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(
        `
      <template v-for="item in items">
        <view v-for="c in item.children" :key="c.id"/>
      </template>`,
        { onError }
      )
      expect(onError).toHaveBeenCalledTimes(1)
    })
  })

  describe('source location', () => {
    test('value & source', () => {
      const source = '<text v-for="item in items" />'
      const { node: forNode } = parseWithForTransform(source)

      const itemOffset = source.indexOf('item')
      const value = forNode.valueAlias as SimpleExpressionNode
      expect((forNode.valueAlias as SimpleExpressionNode).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('bracketed value', () => {
      const source = '<text v-for="( item ) in items" />'
      const { node: forNode } = parseWithForTransform(source)

      const itemOffset = source.indexOf('item')
      const value = forNode.valueAlias 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 = '<text v-for="(  { id, key }) in items" />'
      const { node: forNode } = parseWithForTransform(source)

      const value = forNode.valueAlias 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 = '<text v-for="( item, key, index ) in items" />'
      const { node: forNode } = parseWithForTransform(source)

      const itemOffset = source.indexOf('item')
      const value = forNode.valueAlias 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.keyAlias 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.objectIndexAlias 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 = '<text v-for="( item,, index ) in items" />'
      const { node: forNode } = parseWithForTransform(source)

      const itemOffset = source.indexOf('item')
      const value = forNode.valueAlias 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.objectIndexAlias 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(`<view v-for="i in list"/>`, {
        prefixIdentifiers: true,
      })
      expect(node.source).toMatchObject({
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: `_ctx.list`,
      })
    })

    test('should prefix v-for source w/ complex expression', () => {
      const { node } = parseWithForTransform(
        `<view v-for="i in list.concat([foo])"/>`,
        { prefixIdentifiers: true }
      )
      expect(node.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(
        `<view v-for="i in list">{{ i }}{{ j }}</view>`,
        { prefixIdentifiers: true }
      )
      const view = node.children[0] 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(
        `<view v-for="(i, j, k) in list">{{ i + j + k }}{{ l }}</view>`,
        { prefixIdentifiers: true }
      )
      const view = node.children[0] 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(
        `<view><view v-for="i in list" />{{ i }}</view>`,
        { prefixIdentifiers: true }
      )
      expect((node.children[1] as InterpolationNode).content).toMatchObject({
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: `_ctx.i`,
      })
    })

    test('nested v-for', () => {
      const { node } = parseWithForTransform(
        `<view v-for="i in list">
              <view v-for="i in list">{{ i + j }}</view>{{ i }}
            </view>`,
        { prefixIdentifiers: true }
      )
      const outerView = node.children[0] as ElementNode
      const innerFor = outerView.children[0] as ForNode
      const innerExp = (innerFor.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 = outerView.children[1] as InterpolationNode
      expect(outerExp.content).toMatchObject({
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: `i`,
      })
    })

    test('v-for aliases w/ complex expressions', () => {
      const { node } = parseWithForTransform(
        `<view v-for="({ foo = bar, baz: [qux = quux] }) in list">
              {{ foo + bar + baz + qux + quux }}
            </view>`,
        { prefixIdentifiers: true }
      )
      expect(node.valueAlias!).toMatchObject({
        type: NodeTypes.COMPOUND_EXPRESSION,
        children: [
          `{ `,
          { content: `foo` },
          ` = `,
          { content: `_ctx.bar` },
          `, baz: [`,
          { content: `qux` },
          ` = `,
          { content: `_ctx.quux` },
          `] }`,
        ],
      })
      const view = node.children[0] 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', () => {
      const {
        node: { codegenNode },
      } = parseWithForTransform(
        '<view v-for="item in items" :key="itemKey(item)">test</view>',
        { prefixIdentifiers: true }
      )
      const innerBlock = codegenNode.children.arguments[1].returns
      expect(innerBlock).toMatchObject({
        type: NodeTypes.VNODE_CALL,
        tag: `"view"`,
        props: createObjectMatcher({
          key: {
            type: NodeTypes.COMPOUND_EXPRESSION,
            children: [
              // should prefix outer scope references
              { content: `_ctx.itemKey` },
              `(`,
              // should NOT prefix in scope variables
              { content: `item` },
              `)`,
            ],
          },
        }),
      })
    })

    // #2085
    test('template v-for key expression prefixing', () => {
      const {
        node: { codegenNode },
      } = parseWithForTransform(
        '<template v-for="item in items" :key="itemKey(item)">test</template>',
        { prefixIdentifiers: true }
      )
      const innerBlock = codegenNode.children.arguments[1].returns
      expect(innerBlock).toMatchObject({
        type: NodeTypes.VNODE_CALL,
        tag: FRAGMENT,
        props: createObjectMatcher({
          key: {
            type: NodeTypes.COMPOUND_EXPRESSION,
            children: [
              // should prefix outer scope references
              { content: `_ctx.itemKey` },
              `(`,
              // should NOT prefix in scope variables
              { content: `item` },
              `)`,
            ],
          },
        }),
      })
    })

    test('template v-for key no prefixing on attribute key', () => {
      const {
        node: { codegenNode },
      } = parseWithForTransform(
        '<template v-for="item in items" key="key">test</template>',
        { prefixIdentifiers: true }
      )
      const innerBlock = codegenNode.children.arguments[1].returns
      expect(innerBlock).toMatchObject({
        type: NodeTypes.VNODE_CALL,
        tag: FRAGMENT,
        props: createObjectMatcher({
          key: {
            type: NodeTypes.SIMPLE_EXPRESSION,
            content: 'key',
          },
        }),
      })
    })
  })

  describe('codegen', () => {
    function assertSharedCodegen(
      node: ForCodegenNode,
      keyed: boolean = false,
      customReturn: boolean = false,
      disableTracking: boolean = true
    ) {
      expect(node).toMatchObject({
        type: NodeTypes.VNODE_CALL,
        tag: FRAGMENT,
        disableTracking,
        patchFlag: !disableTracking
          ? genFlagText(PatchFlags.STABLE_FRAGMENT)
          : keyed
          ? genFlagText(PatchFlags.KEYED_FRAGMENT)
          : genFlagText(PatchFlags.UNKEYED_FRAGMENT),
        children: {
          type: NodeTypes.JS_CALL_EXPRESSION,
          callee: RENDER_LIST,
          arguments: [
            {}, // to be asserted by each test
            {
              type: NodeTypes.JS_FUNCTION_EXPRESSION,
              returns: customReturn
                ? {}
                : {
                    type: NodeTypes.VNODE_CALL,
                    isBlock: disableTracking,
                  },
            },
          ],
        },
      })
      const renderListArgs = node.children.arguments
      return {
        source: renderListArgs[0] as SimpleExpressionNode,
        params: (renderListArgs[1] as any).params,
        returns: (renderListArgs[1] as any).returns,
        innerVNodeCall: customReturn
          ? null
          : (renderListArgs[1] as any).returns,
      }
    }

    test('basic v-for', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform('<text v-for="(item) in items" />')
      expect(assertSharedCodegen(codegenNode)).toMatchObject({
        source: { content: `items` },
        params: [
          { content: `item` },
          { content: `__key` },
          { content: `__index` },
          { content: `_cached` },
        ],
        innerVNodeCall: {
          tag: `"text"`,
        },
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })

    test('value + key + index', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform('<text v-for="(item, key, index) in items" />')
      expect(assertSharedCodegen(codegenNode)).toMatchObject({
        source: { content: `items` },
        params: [
          { content: `item` },
          { content: `key` },
          { content: `index` },
          { content: `_cached` },
        ],
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })

    test('skipped value', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform('<text v-for="(, key, index) in items" />')
      expect(assertSharedCodegen(codegenNode)).toMatchObject({
        source: { content: `items` },
        params: [
          { content: `__value` },
          { content: `key` },
          { content: `index` },
          { content: `_cached` },
        ],
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })

    test('skipped key', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform('<text v-for="(item,,index) in items" />')
      expect(assertSharedCodegen(codegenNode)).toMatchObject({
        source: { content: `items` },
        params: [
          { content: `item` },
          { content: `__key` },
          { content: `index` },
          { content: `_cached` },
        ],
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })

    test('skipped value & key', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform('<text v-for="(,,index) in items" />')
      expect(assertSharedCodegen(codegenNode)).toMatchObject({
        source: { content: `items` },
        params: [
          { content: `__value` },
          { content: `__key` },
          { content: `index` },
          { content: `_cached` },
        ],
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })

    test('v-for with constant expression', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform('<p v-for="item in 10">{{item}}</p>', {
        prefixIdentifiers: true,
      })

      expect(
        assertSharedCodegen(
          codegenNode,
          false /* keyed */,
          false /* customReturn */,
          false /* disableTracking */
        )
      ).toMatchObject({
        source: { content: `10`, constType: ConstantTypes.CAN_STRINGIFY },
        params: [
          { content: `item` },
          { content: `__key` },
          { content: `__index` },
          { content: `_cached` },
        ],
        innerVNodeCall: {
          tag: `"p"`,
          props: undefined,
          isBlock: false,
          children: {
            type: NodeTypes.INTERPOLATION,
            content: {
              type: NodeTypes.SIMPLE_EXPRESSION,
              content: 'item',
              isStatic: false,
              constType: ConstantTypes.NOT_CONSTANT,
            },
          },
          patchFlag: genFlagText(PatchFlags.TEXT),
        },
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })

    test('template v-for', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform(
        '<template v-for="item in items">hello<text/></template>'
      )
      expect(assertSharedCodegen(codegenNode)).toMatchObject({
        source: { content: `items` },
        params: [
          { content: `item` },
          { content: `__key` },
          { content: `__index` },
          { content: `_cached` },
        ],
        innerVNodeCall: {
          tag: FRAGMENT,
          props: undefined,
          isBlock: true,
          children: [
            { type: NodeTypes.TEXT, content: `hello` },
            { type: NodeTypes.ELEMENT, tag: `text` },
          ],
          patchFlag: genFlagText(PatchFlags.STABLE_FRAGMENT),
        },
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })

    test('template v-for w/ <slot/>', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform(
        '<template v-for="item in items"><slot/></template>'
      )
      expect(
        assertSharedCodegen(codegenNode, false, true /* custom return */)
      ).toMatchObject({
        source: { content: `items` },
        params: [
          { content: `item` },
          { content: `__key` },
          { content: `__index` },
          { content: `_cached` },
        ],
        returns: {
          type: NodeTypes.JS_CALL_EXPRESSION,
          callee: RENDER_SLOT,
        },
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })

    // #1907
    test('template v-for key injection with single child', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform(
        '<template v-for="item in items" :key="item.id"><text :id="item.id" /></template>'
      )
      expect(assertSharedCodegen(codegenNode, true)).toMatchObject({
        source: { content: `items` },
        params: [
          { content: `item` },
          { content: `__key` },
          { content: `__index` },
          { content: `_cached` },
        ],
        innerVNodeCall: {
          type: NodeTypes.VNODE_CALL,
          tag: `"text"`,
          props: createObjectMatcher({
            key: '[item.id]',
            id: '[item.id]',
          }),
        },
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })

    test('v-for on <slot/>', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform('<slot v-for="item in items"></slot>')
      expect(
        assertSharedCodegen(codegenNode, false, true /* custom return */)
      ).toMatchObject({
        source: { content: `items` },
        params: [
          { content: `item` },
          { content: `__key` },
          { content: `__index` },
          { content: `_cached` },
        ],
        returns: {
          type: NodeTypes.JS_CALL_EXPRESSION,
          callee: RENDER_SLOT,
        },
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })

    test('keyed v-for', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform('<text v-for="(item) in items" :key="item" />')
      expect(assertSharedCodegen(codegenNode, true)).toMatchObject({
        source: { content: `items` },
        params: [
          { content: `item` },
          { content: `__key` },
          { content: `__index` },
          { content: `_cached` },
        ],
        innerVNodeCall: {
          tag: `"text"`,
          props: createObjectMatcher({
            key: `[item]`,
          }),
        },
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })

    test('keyed template v-for', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform(
        '<template v-for="item in items" :key="item">hello<text/></template>'
      )
      expect(assertSharedCodegen(codegenNode, true)).toMatchObject({
        source: { content: `items` },
        params: [
          { content: `item` },
          { content: `__key` },
          { content: `__index` },
          { content: `_cached` },
        ],
        innerVNodeCall: {
          tag: FRAGMENT,
          props: createObjectMatcher({
            key: `[item]`,
          }),
          children: [
            { type: NodeTypes.TEXT, content: `hello` },
            { type: NodeTypes.ELEMENT, tag: `text` },
          ],
          patchFlag: genFlagText(PatchFlags.STABLE_FRAGMENT),
        },
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })

    test('v-if + v-for', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform(`<view v-if="ok" v-for="i in list"/>`)
      expect(codegenNode).toMatchObject({
        type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
        test: { content: `ok` },
        consequent: {
          type: NodeTypes.VNODE_CALL,
          props: createObjectMatcher({
            key: `[0]`,
          }),
          isBlock: true,
          disableTracking: true,
          patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
          children: {
            type: NodeTypes.JS_CALL_EXPRESSION,
            callee: RENDER_LIST,
            arguments: [
              { content: `list` },
              {
                type: NodeTypes.JS_FUNCTION_EXPRESSION,
                params: [
                  { content: `i` },
                  { content: `__key` },
                  { content: `__index` },
                  { content: `_cached` },
                ],
                returns: {
                  type: NodeTypes.VNODE_CALL,
                  tag: `"view"`,
                  isBlock: true,
                },
              },
            ],
          },
        },
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })

    // 1637
    test('v-if + v-for on <template>', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform(`<template v-if="ok" v-for="i in list"/>`)
      expect(codegenNode).toMatchObject({
        type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
        test: { content: `ok` },
        consequent: {
          type: NodeTypes.VNODE_CALL,
          props: createObjectMatcher({
            key: `[0]`,
          }),
          isBlock: true,
          disableTracking: true,
          patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
          children: {
            type: NodeTypes.JS_CALL_EXPRESSION,
            callee: RENDER_LIST,
            arguments: [
              { content: `list` },
              {
                type: NodeTypes.JS_FUNCTION_EXPRESSION,
                params: [
                  { content: `i` },
                  { content: `__key` },
                  { content: `__index` },
                  { content: `_cached` },
                ],
                returns: {
                  type: NodeTypes.VNODE_CALL,
                  tag: FRAGMENT,
                  isBlock: true,
                },
              },
            ],
          },
        },
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })

    test('v-for on element with custom directive', () => {
      const {
        root,
        node: { codegenNode },
      } = parseWithForTransform('<view v-for="i in list" v-foo/>')
      const { returns } = assertSharedCodegen(codegenNode, false, true)
      expect(returns).toMatchObject({
        type: NodeTypes.VNODE_CALL,
        directives: { type: NodeTypes.JS_ARRAY_EXPRESSION },
      })
      expect(generate(root, {} as any).code).toMatchSnapshot()
    })
  })
})