Back to Repositories

Testing Element Transformation Pipeline in dcloudio/uni-app

This test suite validates the element transformation functionality in the uni-app UTS framework, focusing on component compilation and directive handling. It ensures proper parsing and transformation of template elements with various bindings and directives.

Test Coverage Overview

The test suite provides comprehensive coverage of element transformation scenarios in uni-app UTS. It tests component resolution, prop handling, directive transformations, and special element cases like Teleport and Suspense. Key edge cases include dynamic components, runtime directives, and patch flag analysis for optimized rendering.

  • Component resolution and binding validation
  • Prop merging and directive transformation
  • Special element handling (Teleport, Suspense, KeepAlive)
  • Patch flag analysis for optimized updates

Implementation Analysis

The testing approach uses Jest framework with specialized parsing and transformation utilities. It implements modular test cases that validate the element transformation pipeline, from parsing template syntax to generating optimized render code.

The tests utilize mock functions and assertion patterns specific to Vue’s compiler architecture, validating both the transformation output and helper injection for runtime features.

Technical Details

  • Testing Framework: Jest
  • Key Libraries: @vue/compiler-core, @vue/shared
  • Custom Utilities: parseWithElementTransform, createObjectMatcher
  • Configuration: Custom directive transforms, binding metadata options

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through comprehensive coverage of transformation scenarios. It maintains clear separation of concerns between different transformation aspects while ensuring complete validation of the compiler’s output.

  • Modular test organization
  • Thorough edge case coverage
  • Clear test descriptions
  • Consistent assertion patterns

dcloudio/uni-app

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

            
import { PatchFlags } from '@vue/shared'
import {
  BASE_TRANSITION,
  BindingTypes,
  CREATE_VNODE,
  GUARD_REACTIVE_PROPS,
  KEEP_ALIVE,
  MERGE_PROPS,
  NORMALIZE_CLASS,
  NORMALIZE_PROPS,
  NORMALIZE_STYLE,
  RESOLVE_COMPONENT,
  RESOLVE_DIRECTIVE,
  RESOLVE_DYNAMIC_COMPONENT,
  SUSPENSE,
  TELEPORT,
  TO_HANDLERS,
  helperNameMap,
  baseParse as parse,
} from '@vue/compiler-core'
import {
  type DirectiveNode,
  NodeTypes,
  type RootNode,
  type VNodeCall,
  createObjectProperty,
} from '@vue/compiler-core'
import { transformElement as baseTransformElement } from '@vue/compiler-core'
import { compile as baseCompile } from '../../../src/plugins/android/uvue/compiler'
import { transformStyle } from '../../../src/plugins/android/uvue/compiler/transforms/transformStyle'
import { transformOn } from '../../../src/plugins/android/uvue/compiler/transforms/vOn'
import { transformBind } from '../../../src/plugins/android/uvue/compiler/transforms/vBind'

import { createObjectMatcher, genFlagText } from '../testUtils'
import { transformText } from '../../../src/plugins/android/uvue/compiler/transforms/transformText'
import type { TemplateCompilerOptions } from '../../../src/plugins/android/uvue/compiler/options'
import {
  type NodeTransform,
  transform,
} from '../../../src/plugins/android/uvue/compiler/transform'
import { transformExpression } from '../../../src/plugins/android/uvue/compiler/transforms/transformExpression'

const transformElement = baseTransformElement as unknown as NodeTransform

function parseWithElementTransform(
  template: string,
  options: TemplateCompilerOptions = {}
): {
  root: RootNode
  node: VNodeCall
} {
  // wrap raw template in an extra view so that it doesn't get turned into a
  // block as root node
  const ast = parse(`<view>${template}</view>`, options)
  transform(ast, {
    nodeTransforms: [transformElement, transformText],
    ...options,
  })
  const codegenNode = (ast as any).children[0].children[0]
    .codegenNode as VNodeCall
  expect(codegenNode.type).toBe(NodeTypes.VNODE_CALL)
  return {
    root: ast,
    node: codegenNode,
  }
}

function parseWithBind(template: string, options?: TemplateCompilerOptions) {
  return parseWithElementTransform(template, {
    ...options,
    directiveTransforms: {
      ...options?.directiveTransforms,
      bind: transformBind,
    },
  })
}

describe('compiler: element transform', () => {
  test('import + resolve component', () => {
    const { root } = parseWithElementTransform(`<Foo/>`)
    expect(root.helpers).toContain(RESOLVE_COMPONENT)
    expect(root.components).toContain(`Foo`)
  })

  test('resolve implicitly self-referencing component', () => {
    const { root } = parseWithElementTransform(`<Example/>`, {
      filename: `/foo/bar/Example.vue?vue&type=template`,
    })
    expect(root.helpers).toContain(RESOLVE_COMPONENT)
    expect(root.components).toContain(`Example__self`)
  })

  test('resolve component from setup bindings', () => {
    const { root, node } = parseWithElementTransform(`<Example/>`, {
      bindingMetadata: {
        Example: BindingTypes.SETUP_MAYBE_REF,
      },
    })
    expect(root.helpers).not.toContain(RESOLVE_COMPONENT)
    expect(node.tag).toBe(`$setup["Example"]`)
  })

  // test('resolve component from setup bindings (inline)', () => {
  //   const { root, node } = parseWithElementTransform(`<Example/>`, {
  //     inline: true,
  //     bindingMetadata: {
  //       Example: BindingTypes.SETUP_MAYBE_REF,
  //     },
  //   })
  //   expect(root.helpers).not.toContain(RESOLVE_COMPONENT)
  //   expect(node.tag).toBe(`_unref(Example)`)
  // })

  // test('resolve component from setup bindings (inline const)', () => {
  //   const { root, node } = parseWithElementTransform(`<Example/>`, {
  //     inline: true,
  //     bindingMetadata: {
  //       Example: BindingTypes.SETUP_CONST,
  //     },
  //   })
  //   expect(root.helpers).not.toContain(RESOLVE_COMPONENT)
  //   expect(node.tag).toBe(`Example`)
  // })

  test('resolve namespaced component from setup bindings', () => {
    const { root, node } = parseWithElementTransform(`<Foo.Example/>`, {
      bindingMetadata: {
        Foo: BindingTypes.SETUP_MAYBE_REF,
      },
    })
    expect(root.helpers).not.toContain(RESOLVE_COMPONENT)
    expect(node.tag).toBe(`$setup["Foo"].Example`)
  })

  // test('resolve namespaced component from setup bindings (inline)', () => {
  //   const { root, node } = parseWithElementTransform(`<Foo.Example/>`, {
  //     inline: true,
  //     bindingMetadata: {
  //       Foo: BindingTypes.SETUP_MAYBE_REF,
  //     },
  //   })
  //   expect(root.helpers).not.toContain(RESOLVE_COMPONENT)
  //   expect(node.tag).toBe(`_unref(Foo).Example`)
  // })

  // test('resolve namespaced component from setup bindings (inline const)', () => {
  //   const { root, node } = parseWithElementTransform(`<Foo.Example/>`, {
  //     inline: true,
  //     bindingMetadata: {
  //       Foo: BindingTypes.SETUP_CONST,
  //     },
  //   })
  //   expect(root.helpers).not.toContain(RESOLVE_COMPONENT)
  //   expect(node.tag).toBe(`Foo.Example`)
  // })

  test('do not resolve component from non-script-setup bindings', () => {
    const bindingMetadata = {
      Example: BindingTypes.SETUP_MAYBE_REF,
    }
    Object.defineProperty(bindingMetadata, '__isScriptSetup', { value: false })
    const { root } = parseWithElementTransform(`<Example/>`, {
      bindingMetadata,
    })
    expect(root.helpers).toContain(RESOLVE_COMPONENT)
    expect(root.components).toContain(`Example`)
  })

  test('static props', () => {
    const { node } = parseWithElementTransform(`<view id="foo" class="bar" />`)
    expect(node).toMatchObject({
      tag: `"view"`,
      props: createObjectMatcher({
        id: 'foo',
        class: 'bar',
      }),
      children: undefined,
    })
  })

  test('props + children', () => {
    const { node } = parseWithElementTransform(`<view id="foo"><text/></view>`)

    expect(node).toMatchObject({
      tag: `"view"`,
      props: createObjectMatcher({
        id: 'foo',
      }),
      children: [
        {
          type: NodeTypes.ELEMENT,
          tag: 'text',
          codegenNode: {
            type: NodeTypes.VNODE_CALL,
            tag: `"text"`,
          },
        },
      ],
    })
  })

  test('0 placeholder for children with no props', () => {
    const { node } = parseWithElementTransform(`<view><text/></view>`)

    expect(node).toMatchObject({
      tag: `"view"`,
      props: undefined,
      children: [
        {
          type: NodeTypes.ELEMENT,
          tag: 'text',
          codegenNode: {
            type: NodeTypes.VNODE_CALL,
            tag: `"text"`,
          },
        },
      ],
    })
  })

  test('v-bind="obj"', () => {
    const { root, node } = parseWithElementTransform(`<view v-bind="obj" />`)
    // single v-bind doesn't need mergeProps
    expect(root.helpers).not.toContain(MERGE_PROPS)
    expect(root.helpers).toContain(NORMALIZE_PROPS)
    expect(root.helpers).toContain(GUARD_REACTIVE_PROPS)

    // should directly use `obj` in props position
    expect(node.props).toMatchObject({
      type: NodeTypes.JS_CALL_EXPRESSION,
      callee: NORMALIZE_PROPS,
      arguments: [
        {
          type: NodeTypes.JS_CALL_EXPRESSION,
          callee: GUARD_REACTIVE_PROPS,
          arguments: [
            {
              type: NodeTypes.SIMPLE_EXPRESSION,
              content: `obj`,
            },
          ],
        },
      ],
    })
  })

  test('v-bind="obj" after static prop', () => {
    const { root, node } = parseWithElementTransform(
      `<view id="foo" v-bind="obj" />`
    )
    expect(root.helpers).toContain(MERGE_PROPS)

    expect(node.props).toMatchObject({
      type: NodeTypes.JS_CALL_EXPRESSION,
      callee: MERGE_PROPS,
      arguments: [
        createObjectMatcher({
          id: 'foo',
        }),
        {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: `obj`,
        },
      ],
    })
  })

  test('v-bind="obj" before static prop', () => {
    const { root, node } = parseWithElementTransform(
      `<view v-bind="obj" id="foo" />`
    )
    expect(root.helpers).toContain(MERGE_PROPS)

    expect(node.props).toMatchObject({
      type: NodeTypes.JS_CALL_EXPRESSION,
      callee: MERGE_PROPS,
      arguments: [
        {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: `obj`,
        },
        createObjectMatcher({
          id: 'foo',
        }),
      ],
    })
  })

  test('v-bind="obj" between static props', () => {
    const { root, node } = parseWithElementTransform(
      `<view id="foo" v-bind="obj" class="bar" />`
    )
    expect(root.helpers).toContain(MERGE_PROPS)

    expect(node.props).toMatchObject({
      type: NodeTypes.JS_CALL_EXPRESSION,
      callee: MERGE_PROPS,
      arguments: [
        createObjectMatcher({
          id: 'foo',
        }),
        {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: `obj`,
        },
        createObjectMatcher({
          class: 'bar',
        }),
      ],
    })
  })

  test('v-on="obj"', () => {
    const { root, node } = parseWithElementTransform(
      `<view id="foo" v-on="obj" class="bar" />`
    )
    expect(root.helpers).toContain(MERGE_PROPS)

    expect(node.props).toMatchObject({
      type: NodeTypes.JS_CALL_EXPRESSION,
      callee: MERGE_PROPS,
      arguments: [
        createObjectMatcher({
          id: 'foo',
        }),
        {
          type: NodeTypes.JS_CALL_EXPRESSION,
          callee: TO_HANDLERS,
          arguments: [
            {
              type: NodeTypes.SIMPLE_EXPRESSION,
              content: `obj`,
            },
            `true`,
          ],
        },
        createObjectMatcher({
          class: 'bar',
        }),
      ],
    })
  })

  test('v-on="obj" on component', () => {
    const { root, node } = parseWithElementTransform(
      `<Foo id="foo" v-on="obj" class="bar" />`
    )
    expect(root.helpers).toContain(MERGE_PROPS)

    expect(node.props).toMatchObject({
      type: NodeTypes.JS_CALL_EXPRESSION,
      callee: MERGE_PROPS,
      arguments: [
        createObjectMatcher({
          id: 'foo',
        }),
        {
          type: NodeTypes.JS_CALL_EXPRESSION,
          callee: TO_HANDLERS,
          arguments: [
            {
              type: NodeTypes.SIMPLE_EXPRESSION,
              content: `obj`,
            },
          ],
        },
        createObjectMatcher({
          class: 'bar',
        }),
      ],
    })
  })

  test('v-on="obj" + v-bind="obj"', () => {
    const { root, node } = parseWithElementTransform(
      `<view id="foo" v-on="handlers" v-bind="obj" />`
    )
    expect(root.helpers).toContain(MERGE_PROPS)

    expect(node.props).toMatchObject({
      type: NodeTypes.JS_CALL_EXPRESSION,
      callee: MERGE_PROPS,
      arguments: [
        createObjectMatcher({
          id: 'foo',
        }),
        {
          type: NodeTypes.JS_CALL_EXPRESSION,
          callee: TO_HANDLERS,
          arguments: [
            {
              type: NodeTypes.SIMPLE_EXPRESSION,
              content: `handlers`,
            },
            `true`,
          ],
        },
        {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: `obj`,
        },
      ],
    })
  })

  test('should handle plain <template> as normal element', () => {
    const { node } = parseWithElementTransform(`<template id="foo" />`)

    expect(node).toMatchObject({
      tag: `"template"`,
      props: createObjectMatcher({
        id: 'foo',
      }),
    })
  })

  test('should handle <Teleport> with normal children', () => {
    function assert(tag: string) {
      const { root, node } = parseWithElementTransform(
        `<${tag} target="#foo"><text /></${tag}>`
      )
      expect(root.components.length).toBe(0)
      expect(root.helpers).toContain(TELEPORT)

      expect(node).toMatchObject({
        tag: TELEPORT,
        props: createObjectMatcher({
          target: '#foo',
        }),
        children: [
          {
            type: NodeTypes.ELEMENT,
            tag: 'text',
            codegenNode: {
              type: NodeTypes.VNODE_CALL,
              tag: `"text"`,
            },
          },
        ],
      })
    }

    assert(`teleport`)
    assert(`Teleport`)
  })

  test('should handle <Suspense>', () => {
    function assert(tag: string, content: string, hasFallback?: boolean) {
      const { root, node } = parseWithElementTransform(
        `<${tag}>${content}</${tag}>`
      )
      expect(root.components.length).toBe(0)
      expect(root.helpers).toContain(SUSPENSE)

      expect(node).toMatchObject({
        tag: SUSPENSE,
        props: undefined,
        children: hasFallback
          ? createObjectMatcher({
              default: {
                type: NodeTypes.JS_FUNCTION_EXPRESSION,
              },
              fallback: {
                type: NodeTypes.JS_FUNCTION_EXPRESSION,
              },
              _: `[1 /* STABLE */]`,
            })
          : createObjectMatcher({
              default: {
                type: NodeTypes.JS_FUNCTION_EXPRESSION,
              },
              _: `[1 /* STABLE */]`,
            }),
      })
    }

    assert(`suspense`, `foo`)
    assert(`suspense`, `<template #default>foo</template>`)
    assert(
      `suspense`,
      `<template #default>foo</template><template #fallback>fallback</template>`,
      true
    )
  })

  test('should handle <KeepAlive>', () => {
    function assert(tag: string) {
      const root = parse(`<view><${tag}><text /></${tag}></view>`)
      transform(root, {
        nodeTransforms: [transformElement, transformText],
      })
      expect(root.components.length).toBe(0)
      expect(root.helpers).toContain(KEEP_ALIVE)
      const node = (root.children[0] as any).children[0].codegenNode
      expect(node).toMatchObject({
        type: NodeTypes.VNODE_CALL,
        tag: KEEP_ALIVE,
        isBlock: true, // should be forced into a block
        props: undefined,
        // keep-alive should not compile content to slots
        children: [{ type: NodeTypes.ELEMENT, tag: 'text' }],
        // should get a dynamic slots flag to force updates
        patchFlag: genFlagText(PatchFlags.DYNAMIC_SLOTS),
      })
    }

    assert(`keep-alive`)
    assert(`KeepAlive`)
  })

  test('should handle <BaseTransition>', () => {
    function assert(tag: string) {
      const { root, node } = parseWithElementTransform(
        `<${tag}><text /></${tag}>`
      )
      expect(root.components.length).toBe(0)
      expect(root.helpers).toContain(BASE_TRANSITION)

      expect(node).toMatchObject({
        tag: BASE_TRANSITION,
        props: undefined,
        children: createObjectMatcher({
          default: {
            type: NodeTypes.JS_FUNCTION_EXPRESSION,
          },
          _: `[1 /* STABLE */]`,
        }),
      })
    }

    assert(`base-transition`)
    assert(`BaseTransition`)
  })

  // test('error on v-bind with no argument', async () => {
  //   const onError = vi.fn()
  //   parseWithElementTransform(`<view v-bind/>`, { onError })
  //   expect(onError.mock.calls[0]).toMatchObject([
  //     {
  //       code: ErrorCodes.X_V_BIND_NO_EXPRESSION,
  //     },
  //   ])
  // })

  test('directiveTransforms', () => {
    let _dir: DirectiveNode
    const { node } = parseWithElementTransform(`<view v-foo:bar="hello" />`, {
      directiveTransforms: {
        foo(dir) {
          _dir = dir
          return {
            props: [createObjectProperty(dir.arg!, dir.exp!)],
          }
        },
      },
    })

    expect(node.props).toMatchObject({
      type: NodeTypes.JS_OBJECT_EXPRESSION,
      properties: [
        {
          type: NodeTypes.JS_PROPERTY,
          key: _dir!.arg,
          value: _dir!.exp,
        },
      ],
    })
    // should factor in props returned by custom directive transforms
    // in patchFlag analysis
    expect(node.patchFlag).toMatch(PatchFlags.PROPS + '')
    expect(node.dynamicProps).toMatch(`"bar"`)
  })

  test('directiveTransform with needRuntime: true', () => {
    const { root, node } = parseWithElementTransform(
      `<view v-foo:bar="hello" />`,
      {
        directiveTransforms: {
          foo() {
            return {
              props: [],
              needRuntime: true,
            }
          },
        },
      }
    )
    expect(root.helpers).toContain(RESOLVE_DIRECTIVE)
    expect(root.directives).toContain(`foo`)
    expect(node).toMatchObject({
      tag: `"view"`,
      props: undefined,
      children: undefined,
      patchFlag: genFlagText(PatchFlags.NEED_PATCH), // should generate appropriate flag
      directives: {
        type: NodeTypes.JS_ARRAY_EXPRESSION,
        elements: [
          {
            type: NodeTypes.JS_ARRAY_EXPRESSION,
            elements: [
              `_directive_foo`,
              // exp
              {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: `hello`,
                isStatic: false,
              },
              // arg
              {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: `bar`,
                isStatic: true,
              },
            ],
          },
        ],
      },
    })
  })

  test('directiveTransform with needRuntime: Symbol', () => {
    const { root, node } = parseWithElementTransform(
      `<view v-foo:bar="hello" />`,
      {
        directiveTransforms: {
          foo() {
            return {
              props: [],
              needRuntime: CREATE_VNODE,
            }
          },
        },
      }
    )

    expect(root.helpers).toContain(CREATE_VNODE)
    expect(root.helpers).not.toContain(RESOLVE_DIRECTIVE)
    expect(root.directives.length).toBe(0)
    expect(node.directives!.elements[0].elements[0]).toBe(
      `${helperNameMap[CREATE_VNODE]}`
    )
  })

  test('runtime directives', () => {
    const { root, node } = parseWithElementTransform(
      `<view v-foo v-bar="x" v-baz:[arg].mod.mad="y" />`
    )
    expect(root.helpers).toContain(RESOLVE_DIRECTIVE)
    expect(root.directives).toContain(`foo`)
    expect(root.directives).toContain(`bar`)
    expect(root.directives).toContain(`baz`)

    expect(node).toMatchObject({
      directives: {
        type: NodeTypes.JS_ARRAY_EXPRESSION,
        elements: [
          {
            type: NodeTypes.JS_ARRAY_EXPRESSION,
            elements: [`_directive_foo`],
          },
          {
            type: NodeTypes.JS_ARRAY_EXPRESSION,
            elements: [
              `_directive_bar`,
              // exp
              {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: `x`,
              },
            ],
          },
          {
            type: NodeTypes.JS_ARRAY_EXPRESSION,
            elements: [
              `_directive_baz`,
              // exp
              {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: `y`,
                isStatic: false,
              },
              // arg
              {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: `arg`,
                isStatic: false,
              },
              // modifiers
              {
                type: NodeTypes.JS_OBJECT_EXPRESSION,
                properties: [
                  {
                    type: NodeTypes.JS_PROPERTY,
                    key: {
                      type: NodeTypes.SIMPLE_EXPRESSION,
                      content: `mod`,
                      isStatic: true,
                    },
                    value: {
                      type: NodeTypes.SIMPLE_EXPRESSION,
                      content: `true`,
                      isStatic: false,
                    },
                  },
                  {
                    type: NodeTypes.JS_PROPERTY,
                    key: {
                      type: NodeTypes.SIMPLE_EXPRESSION,
                      content: `mad`,
                      isStatic: true,
                    },
                    value: {
                      type: NodeTypes.SIMPLE_EXPRESSION,
                      content: `true`,
                      isStatic: false,
                    },
                  },
                ],
              },
            ],
          },
        ],
      },
    })
  })

  test(`props merging: event handlers`, () => {
    const { node } = parseWithElementTransform(
      `<view @click.foo="a" @click.bar="b" />`,
      {
        directiveTransforms: {
          on: transformOn,
        },
      }
    )
    expect(node.props).toMatchObject({
      type: NodeTypes.JS_OBJECT_EXPRESSION,
      properties: [
        {
          type: NodeTypes.JS_PROPERTY,
          key: {
            type: NodeTypes.SIMPLE_EXPRESSION,
            content: `onClick`,
            isStatic: true,
          },
          value: {
            type: NodeTypes.JS_ARRAY_EXPRESSION,
            elements: [
              {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: `a`,
                isStatic: false,
              },
              {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: `b`,
                isStatic: false,
              },
            ],
          },
        },
      ],
    })
  })

  test(`props merging: style`, () => {
    const { node, root } = parseWithElementTransform(
      `<view style="color: green" :style="{ color: 'red' }" />`,
      {
        nodeTransforms: [transformStyle, transformElement],
        directiveTransforms: {
          bind: transformBind,
        },
      }
    )
    expect(root.helpers).toContain(NORMALIZE_STYLE)
    expect(node.props).toMatchObject({
      type: NodeTypes.JS_OBJECT_EXPRESSION,
      properties: [
        {
          type: NodeTypes.JS_PROPERTY,
          key: {
            type: NodeTypes.SIMPLE_EXPRESSION,
            content: `style`,
            isStatic: true,
          },
          value: {
            type: NodeTypes.JS_CALL_EXPRESSION,
            callee: NORMALIZE_STYLE,
            arguments: [
              {
                type: NodeTypes.JS_ARRAY_EXPRESSION,
                elements: [
                  {
                    type: NodeTypes.SIMPLE_EXPRESSION,
                    content: `{"color":"green"}`,
                    isStatic: false,
                  },
                  {
                    type: NodeTypes.SIMPLE_EXPRESSION,
                    content: `{ color: 'red' }`,
                    isStatic: false,
                  },
                ],
              },
            ],
          },
        },
      ],
    })
  })

  test(`props merging: style w/ transformExpression`, () => {
    const { node, root } = parseWithElementTransform(
      `<view style="color: green" :style="{ color: 'red' }" />`,
      {
        nodeTransforms: [transformExpression, transformStyle, transformElement],
        directiveTransforms: {
          bind: transformBind,
        },
        prefixIdentifiers: true,
      }
    )
    expect(root.helpers).toContain(NORMALIZE_STYLE)
    expect(node.props).toMatchObject({
      type: NodeTypes.JS_OBJECT_EXPRESSION,
      properties: [
        {
          type: NodeTypes.JS_PROPERTY,
          key: {
            type: NodeTypes.SIMPLE_EXPRESSION,
            content: `style`,
            isStatic: true,
          },
          value: {
            type: NodeTypes.JS_CALL_EXPRESSION,
            callee: NORMALIZE_STYLE,
          },
        },
      ],
    })
  })

  test(':style with array literal', () => {
    const { node, root } = parseWithElementTransform(
      `<view :style="[{ color: 'red' }]" />`,
      {
        nodeTransforms: [transformExpression, transformStyle, transformElement],
        directiveTransforms: {
          bind: transformBind,
        },
        prefixIdentifiers: true,
      }
    )
    expect(root.helpers).toContain(NORMALIZE_STYLE)
    expect(node.props).toMatchObject({
      type: NodeTypes.JS_OBJECT_EXPRESSION,
      properties: [
        {
          type: NodeTypes.JS_PROPERTY,
          key: {
            type: NodeTypes.SIMPLE_EXPRESSION,
            content: `style`,
            isStatic: true,
          },
          value: {
            type: NodeTypes.JS_CALL_EXPRESSION,
            callee: NORMALIZE_STYLE,
          },
        },
      ],
    })
  })

  test(`props merging: class`, () => {
    const { node, root } = parseWithElementTransform(
      `<view class="foo" :class="{ bar: isBar }" />`,
      {
        directiveTransforms: {
          bind: transformBind,
        },
      }
    )
    expect(root.helpers).toContain(NORMALIZE_CLASS)
    expect(node.props).toMatchObject({
      type: NodeTypes.JS_OBJECT_EXPRESSION,
      properties: [
        {
          type: NodeTypes.JS_PROPERTY,
          key: {
            type: NodeTypes.SIMPLE_EXPRESSION,
            content: `class`,
            isStatic: true,
          },
          value: {
            type: NodeTypes.JS_CALL_EXPRESSION,
            callee: NORMALIZE_CLASS,
            arguments: [
              {
                type: NodeTypes.JS_ARRAY_EXPRESSION,
                elements: [
                  {
                    type: NodeTypes.SIMPLE_EXPRESSION,
                    content: `foo`,
                    isStatic: true,
                  },
                  {
                    type: NodeTypes.SIMPLE_EXPRESSION,
                    content: `{ bar: isBar }`,
                    isStatic: false,
                  },
                ],
              },
            ],
          },
        },
      ],
    })
  })

  describe('patchFlag analysis', () => {
    test('TEXT', () => {
      const { node } = parseWithBind(`<view>foo</view>`)
      expect(node.patchFlag).toBeUndefined()

      const { node: node2 } = parseWithBind(`<view>{{ foo }}</view>`)
      expect(node2.patchFlag).toBe(genFlagText(PatchFlags.TEXT))

      // multiple nodes, merged with optimize text
      // const { node: node3 } = parseWithBind(`<view>foo {{ bar }} baz</view>`)
      // expect(node3.patchFlag).toBe(genFlagText(PatchFlags.TEXT))
    })

    test('CLASS', () => {
      const { node } = parseWithBind(`<view :class="foo" />`)
      expect(node.patchFlag).toBe(genFlagText(PatchFlags.CLASS))
    })

    test('STYLE', () => {
      const { node } = parseWithBind(`<view :style="foo" />`)
      expect(node.patchFlag).toBe(genFlagText(PatchFlags.STYLE))
    })

    test('PROPS', () => {
      const { node } = parseWithBind(`<view id="foo" :foo="bar" :baz="qux" />`)
      expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))
      expect(node.dynamicProps).toBe(`["foo", "baz"]`)
    })

    test('CLASS + STYLE + PROPS', () => {
      const { node } = parseWithBind(
        `<view id="foo" :class="cls" :style="styl" :foo="bar" :baz="qux"/>`
      )
      expect(node.patchFlag).toBe(
        genFlagText([PatchFlags.CLASS, PatchFlags.STYLE, PatchFlags.PROPS])
      )
      expect(node.dynamicProps).toBe(`["foo", "baz"]`)
    })

    // should treat `class` and `style` as PROPS
    test('PROPS on component', () => {
      const { node } = parseWithBind(
        `<Foo :id="foo" :class="cls" :style="styl" />`
      )
      expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))
      expect(node.dynamicProps).toBe(`["id", "class", "style"]`)
    })

    test('FULL_PROPS (v-bind)', () => {
      const { node } = parseWithBind(`<view v-bind="foo" />`)
      expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
    })

    test('FULL_PROPS (dynamic key)', () => {
      const { node } = parseWithBind(`<view :[foo]="bar" />`)
      expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
    })

    test('FULL_PROPS (w/ others)', () => {
      const { node } = parseWithBind(
        `<view id="foo" v-bind="bar" :class="cls" />`
      )
      expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
    })

    test('NEED_PATCH (static ref)', () => {
      const { node } = parseWithBind(`<view ref="foo" />`)
      expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
    })

    test('NEED_PATCH (dynamic ref)', () => {
      const { node } = parseWithBind(`<view :ref="foo" />`)
      expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
    })

    test('NEED_PATCH (custom directives)', () => {
      const { node } = parseWithBind(`<view v-foo />`)
      expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
    })

    test('NEED_PATCH (vnode hooks)', () => {
      const root = baseCompile(`<view @vnodeUpdated="foo" />`, {
        prefixIdentifiers: true,
        // cacheHandlers: true, 暂不支持
      }).ast
      const node = (root as any).children[0].codegenNode
      expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))
      // expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
    })

    // test('script setup inline mode template ref (binding exists)', () => {
    //   const { node } = parseWithElementTransform(`<input ref="input"/>`, {
    //     inline: true,
    //     bindingMetadata: {
    //       input: BindingTypes.SETUP_REF,
    //     },
    //   })
    //   expect(node.props).toMatchObject({
    //     type: NodeTypes.JS_OBJECT_EXPRESSION,
    //     properties: [
    //       {
    //         type: NodeTypes.JS_PROPERTY,
    //         key: {
    //           content: 'ref_key',
    //           isStatic: true,
    //         },
    //         value: {
    //           content: 'input',
    //           isStatic: true,
    //         },
    //       },
    //       {
    //         type: NodeTypes.JS_PROPERTY,
    //         key: {
    //           content: 'ref',
    //           isStatic: true,
    //         },
    //         value: {
    //           content: 'input',
    //           isStatic: false,
    //         },
    //       },
    //     ],
    //   })
    // })

    // test('script setup inline mode template ref (binding does not exist)', () => {
    //   const { node } = parseWithElementTransform(`<input ref="input"/>`, {
    //     inline: true,
    //   })
    //   expect(node.props).toMatchObject({
    //     type: NodeTypes.JS_OBJECT_EXPRESSION,
    //     properties: [
    //       {
    //         type: NodeTypes.JS_PROPERTY,
    //         key: {
    //           content: 'ref',
    //           isStatic: true,
    //         },
    //         value: {
    //           content: 'input',
    //           isStatic: true,
    //         },
    //       },
    //     ],
    //   })
    // })

    // test('script setup inline mode template ref (binding does not exist but props with the same name exist)', () => {
    //   const { node } = parseWithElementTransform(`<input ref="msg"/>`, {
    //     inline: true,
    //     bindingMetadata: {
    //       msg: BindingTypes.PROPS,
    //       ref: BindingTypes.SETUP_CONST,
    //     },
    //   })
    //   expect(node.props).toMatchObject({
    //     type: NodeTypes.JS_OBJECT_EXPRESSION,
    //     properties: [
    //       {
    //         type: NodeTypes.JS_PROPERTY,
    //         key: {
    //           content: 'ref',
    //           isStatic: true,
    //         },
    //         value: {
    //           content: 'msg',
    //           isStatic: true,
    //         },
    //       },
    //     ],
    //   })
    // })

    test('HYDRATE_EVENTS', () => {
      // ignore click events (has dedicated fast path)
      const { node } = parseWithElementTransform(`<view @click="foo" />`, {
        directiveTransforms: {
          on: transformOn,
        },
      })
      // should only have props flag
      expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))

      const { node: node2 } = parseWithElementTransform(
        `<view @keyup="foo" />`,
        {
          directiveTransforms: {
            on: transformOn,
          },
        }
      )
      expect(node2.patchFlag).toBe(
        genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION])
      )
    })

    // #5870
    test('HYDRATE_EVENTS on dynamic component', () => {
      const { node } = parseWithElementTransform(
        `<component :is="foo" @input="foo" />`,
        {
          directiveTransforms: {
            on: transformOn,
          },
        }
      )
      expect(node.patchFlag).toBe(
        genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION])
      )
    })
  })

  describe('dynamic component', () => {
    test('static binding', () => {
      const { node, root } = parseWithBind(`<component is="foo" />`)
      expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
      expect(node).toMatchObject({
        isBlock: true,
        tag: {
          callee: RESOLVE_DYNAMIC_COMPONENT,
          arguments: [
            {
              type: NodeTypes.SIMPLE_EXPRESSION,
              content: 'foo',
              isStatic: true,
            },
          ],
        },
      })
    })

    test('capitalized version w/ static binding', () => {
      const { node, root } = parseWithBind(`<Component is="foo" />`)
      expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
      expect(node).toMatchObject({
        isBlock: true,
        tag: {
          callee: RESOLVE_DYNAMIC_COMPONENT,
          arguments: [
            {
              type: NodeTypes.SIMPLE_EXPRESSION,
              content: 'foo',
              isStatic: true,
            },
          ],
        },
      })
    })

    test('dynamic binding', () => {
      const { node, root } = parseWithBind(`<component :is="foo" />`)
      expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
      expect(node).toMatchObject({
        isBlock: true,
        tag: {
          callee: RESOLVE_DYNAMIC_COMPONENT,
          arguments: [
            {
              type: NodeTypes.SIMPLE_EXPRESSION,
              content: 'foo',
              isStatic: false,
            },
          ],
        },
      })
    })

    // 已废弃,交由 :is="vue:my-row-component" 实现
    // test('v-is', () => {
    //   const { node, root } = parseWithBind(`<view v-is="'foo'" />`)
    //   expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
    //   expect(node).toMatchObject({
    //     tag: {
    //       callee: RESOLVE_DYNAMIC_COMPONENT,
    //       arguments: [
    //         {
    //           type: NodeTypes.SIMPLE_EXPRESSION,
    //           content: `'foo'`,
    //           isStatic: false,
    //         },
    //       ],
    //     },
    //     // should skip v-is runtime check
    //     directives: undefined,
    //   })
    // })

    // #3934
    test('normal component with is prop', () => {
      const { node, root } = parseWithBind(`<custom-input is="foo" />`, {
        isNativeTag: () => false,
      })
      expect(root.helpers).toContain(RESOLVE_COMPONENT)
      expect(root.helpers).not.toContain(RESOLVE_DYNAMIC_COMPONENT)
      expect(node).toMatchObject({
        tag: '_component_custom_input',
      })
    })
  })

  test('<svg> should be forced into blocks', () => {
    const ast = parse(`<view><svg/></view>`)
    transform(ast, {
      nodeTransforms: [transformElement],
    })
    expect((ast as any).children[0].children[0].codegenNode).toMatchObject({
      type: NodeTypes.VNODE_CALL,
      tag: `"svg"`,
      isBlock: true,
    })
  })

  test('force block for runtime custom directive w/ children', () => {
    const { node } = parseWithElementTransform(`<view v-foo>hello</view>`)
    expect(node.isBlock).toBe(true)
  })

  test('force block for inline before-update handlers w/ children', () => {
    expect(
      parseWithElementTransform(`<view @vue:before-update>hello</view>`).node
        .isBlock
    ).toBe(true)
  })

  // #938
  test('element with dynamic keys should be forced into blocks', () => {
    const ast = parse(`<view><view :key="foo" /></view>`)
    transform(ast, {
      nodeTransforms: [transformElement],
    })
    expect((ast as any).children[0].children[0].codegenNode).toMatchObject({
      type: NodeTypes.VNODE_CALL,
      tag: `"view"`,
      isBlock: true,
    })
  })

  test('should process node when node has been replaced', () => {
    // a NodeTransform that swaps out <view id="foo" /> with <text id="foo" />
    const customNodeTransform: NodeTransform = (node, context) => {
      if (
        node.type === NodeTypes.ELEMENT &&
        node.tag === 'view' &&
        node.props.some(
          (prop) =>
            prop.type === NodeTypes.ATTRIBUTE &&
            prop.name === 'id' &&
            prop.value &&
            prop.value.content === 'foo'
        )
      ) {
        context.replaceNode({
          ...node,
          tag: 'text',
        })
      }
    }
    const ast = parse(`<view><view id="foo" /></view>`)
    transform(ast, {
      nodeTransforms: [transformElement, transformText, customNodeTransform],
    })
    expect((ast as any).children[0].children[0].codegenNode).toMatchObject({
      type: NodeTypes.VNODE_CALL,
      tag: '"text"',
      isBlock: false,
    })
  })
})