Back to Repositories

Testing v-Bind Directive Transformations in dcloudio/uni-app

This test suite validates v-bind directive functionality in the uni-app-uts framework, focusing on various binding scenarios including basic bindings, modifiers, and dynamic arguments. The tests ensure proper transformation and compilation of v-bind expressions.

Test Coverage Overview

Comprehensive coverage of v-bind directive implementations including:
  • Basic attribute binding scenarios
  • Object expressions and dynamic arguments
  • Modifier handling (.camel, .prop, .attr)
  • Complex binding scenarios with nested objects and arrays
  • Error handling for invalid bindings

Implementation Analysis

The testing approach utilizes Jest framework with custom assertion utilities to verify v-bind transformations. Tests validate the compiler output against expected AST structures and generated code, ensuring correct handling of binding expressions and modifiers.

Implements specialized parsing logic through parseWithVBind helper that combines multiple transform phases.

Technical Details

Testing infrastructure includes:
  • @vue/compiler-core for AST parsing and transformation
  • Custom transform plugins for v-bind handling
  • Jest test runner and assertion framework
  • Mock functions for error handling validation
  • TypeScript type checking for compiler interfaces

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through:
  • Granular test cases covering distinct functionality
  • Thorough validation of AST structure and generated code
  • Error case handling and validation
  • Type-safe testing with TypeScript
  • Modular test organization with clear descriptions

dcloudio/uni-app

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

            
import { extend } from '@vue/shared'
import {
  CAMELIZE,
  type CallExpression,
  type CompilerOptions,
  type ElementNode,
  ErrorCodes,
  NORMALIZE_PROPS,
  NodeTypes,
  type ObjectExpression,
  type VNodeCall,
  helperNameMap,
  baseParse as parse,
  transform,
  transformElement,
} from '@vue/compiler-core'
import { transformBind } from '../../../src/plugins/android/uvue/compiler/transforms/vBind'
import { transformExpression } from '../../../src/plugins/android/uvue/compiler/transforms/transformExpression'
import { assert } from '../testUtils'

function parseWithVBind(
  template: string,
  options: CompilerOptions = {}
): ElementNode {
  const ast = parse(template)
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers: options.prefixIdentifiers,
      nodeTransforms: [
        transformExpression,
        transformElement,
        ...(options.nodeTransforms || []), // user transforms
      ],
      directiveTransforms: extend(
        {},
        { bind: transformBind },
        options.directiveTransforms || {} // user transforms
      ),
    })
  )
  return ast.children[0] as ElementNode
}

describe('compiler: v-bind', () => {
  test('basic', () => {
    assert(
      `<view v-bind:id="id"/>`,
      `createElementVNode("view", utsMapOf({ id: _ctx.id }), null, 8 /* PROPS */, ["id"])`
    )
  })
  test('object expression', () => {
    assert(
      `<view v-bind="{id}"/>`,
      `createElementVNode("view", normalizeProps(guardReactiveProps(utsMapOf({id: _ctx.id}))), null, 16 /* FULL_PROPS */)`
    )
  })
  test('dynamic arg', () => {
    assert(
      `<view v-bind:[id]="id"/>`,
      `createElementVNode("view", normalizeProps(utsMapOf({ [_ctx.id !== null ? _ctx.id : ""]: _ctx.id })), null, 16 /* FULL_PROPS */)`
    )
  })
  test('.camel modifier', () => {
    assert(
      `<view v-bind:foo-bar.camel="id"/>`,
      `createElementVNode("view", utsMapOf({ fooBar: _ctx.id }), null, 8 /* PROPS */, ["fooBar"])`
    )
  })
  test('.camel modifier w/ dynamic arg', () => {
    assert(
      `<view v-bind:[foo].camel="id"/>`,
      `createElementVNode("view", normalizeProps(utsMapOf({ [camelize(_ctx.foo !== null ? _ctx.foo : "")]: _ctx.id })), null, 16 /* FULL_PROPS */)`
    )
  })
  test('.prop modifier', () => {
    assert(
      `<view v-bind:className.prop="className"/>`,
      `createElementVNode("view", utsMapOf({ ".className": _ctx.className }), null, 40 /* PROPS, NEED_HYDRATION */, [".className"])`
    )
  })
  test('.prop modifier w/ dynamic arg', () => {
    assert(
      `<view v-bind:[fooBar].prop="className"/>`,
      'createElementVNode("view", normalizeProps(utsMapOf({ [`.${_ctx.fooBar !== null ? _ctx.fooBar : ""}`]: _ctx.className })), null, 48 /* FULL_PROPS, NEED_HYDRATION */)'
    )
  })
  test('.prop modifier (shorthand)', () => {
    assert(
      `<view .className="className"/>`,
      'createElementVNode("view", utsMapOf({ ".className": _ctx.className }), null, 40 /* PROPS, NEED_HYDRATION */, [".className"])'
    )
  })
  test('.attr modifier', () => {
    assert(
      `<view v-bind:foo-bar.attr="id"/>`,
      'createElementVNode("view", utsMapOf({ "^foo-bar": _ctx.id }), null, 8 /* PROPS */, ["^foo-bar"])'
    )
  })
  test('simple expression', () => {
    assert(
      `<view v-bind:class="{'box': true}"></view>`,
      `createElementVNode("view", utsMapOf({
  class: normalizeClass(utsMapOf({'box': true}))
}), null, 2 /* CLASS */)`
    )
  })
  test('simple expression with array', () => {
    assert(
      `<view v-bind:class="[classA, {classB: true, classC: false}]"></view>`,
      `createElementVNode("view", utsMapOf({
  class: normalizeClass([_ctx.classA, utsMapOf({classB: true, classC: false})])
}), null, 2 /* CLASS */)`
    )
  })
  test('simple expression with array with variable', () => {
    assert(
      `<view v-bind:class="[classA, {[classB]: true, [classC]: false}]"></view>`,
      `createElementVNode("view", utsMapOf({
  class: normalizeClass([_ctx.classA, utsMapOf({[_ctx.classB]: true, [_ctx.classC]: false})])
}), null, 2 /* CLASS */)`
    )
  })
  test('simple expression with object', () => {
    assert(
      `<view :style="{color: true ? 'blue' : 'red'}"></view>`,
      `createElementVNode("view", utsMapOf({
  style: normalizeStyle(utsMapOf({color: true ? 'blue' : 'red'}))
}), null, 4 /* STYLE */)`
    )
  })
  test('complex expression', () => {
    assert(
      `<rich-text :nodes="[{'name':'div','attrs':{'class':'div-class','style':'line-height: 60px; color: red; text-align:center;'},'children':[{'type':'text','text':'this is text'}]}]" />`,
      `createElementVNode("rich-text", utsMapOf({ nodes: [utsMapOf({'name':'div','attrs':utsMapOf({'class':'div-class','style':'line-height: 60px; color: red; text-align:center;'}),'children':[utsMapOf({'type':'text','text':'this is text'})]})] }), null, 8 /* PROPS */, ["nodes"])`
    )
  })
  test('empty object syntax with \n', () => {
    assert(
      `<rich-text
  :nodes="[
    {
      'name': 'div',
      'attrs': {
        'class': 'div-class',
        'style': 'line-height: 60px; color: red; text-align:center;'
      },
      'children': [
        { 'type': 'text', 'text': 'this is text' },
        { 'type': 'text', 'text': 'this is text' },
      ]
    }
  ]"
/>`,
      `createElementVNode("rich-text", utsMapOf({ nodes: [
    utsMapOf({
      'name': 'div',
      'attrs': utsMapOf({
        'class': 'div-class',
        'style': 'line-height: 60px; color: red; text-align:center;'
      }),
      'children': [
        utsMapOf({ 'type': 'text', 'text': 'this is text' }),
        utsMapOf({ 'type': 'text', 'text': 'this is text' }),
      ]
    })
  ] }), null, 8 /* PROPS */, ["nodes"])`
    )
  })
  test('style with empty {\n }', () => {
    assert(
      `<text :style="{
    }" />`,
      `createElementVNode("text", utsMapOf({
  style: normalizeStyle(utsMapOf<string, any | null>({
    }))
}), null, 4 /* STYLE */)`
    )
  })
  test('object value width expression', () => {
    assert(
      `<view class="search" @click="toSearchPage" :style="{'width':700 +'rpx',
      'top':0 +'px'}" />`,
      `createElementVNode("view", utsMapOf({
  class: "search",
  onClick: _ctx.toSearchPage,
  style: normalizeStyle(utsMapOf({'width':700 +'rpx',
      'top':0 +'px'}))
}), null, 12 /* STYLE, PROPS */, ["onClick"])`
    )
  })
  test('object value width all number expression', () => {
    assert(
      `<view class="search" @click="toSearchPage" :style="{'opacity': 1 + 1}" />`,
      `createElementVNode("view", utsMapOf({
  class: "search",
  onClick: _ctx.toSearchPage,
  style: normalizeStyle(utsMapOf({'opacity': 1 + 1}))
}), null, 12 /* STYLE, PROPS */, ["onClick"])`
    )
  })
  test('object value width expression (with data)', () => {
    assert(
      `<view :style="{'opacity': count > 0.3 ? 1 : count * 3, 'color': 'red'}" />`,
      `createElementVNode("view", utsMapOf({
  style: normalizeStyle(utsMapOf({'opacity': _ctx.count > 0.3 ? 1 : _ctx.count * 3, 'color': 'red'}))
}), null, 4 /* STYLE */)`
    )
  })
  test('prop with type object', () => {
    assert(
      `<Foo :obj="{name: 'name'}" />`,
      `createVNode(_component_Foo, utsMapOf({ obj: {name: 'name'} }))`
    )
  })
  test('v-bind all props', () => {
    assert(
      `<Foo v-bind="{name: 'name', age: 18, obj: {key: 'value'}}" />`,
      `createVNode(_component_Foo, normalizeProps(guardReactiveProps(utsMapOf({name: 'name', age: 18, obj: {key: 'value'}}))), null, 16 /* FULL_PROPS */)`
    )
  })
  test('v-bind all props with variable', () => {
    assert(
      `<Foo v-bind="allProps" />`,
      `createVNode(_component_Foo, normalizeProps(guardReactiveProps(_ctx.allProps)), null, 16 /* FULL_PROPS */)`
    )
  })

  test('basic', () => {
    const node = parseWithVBind(`<view v-bind:id="id"/>`)
    const props = (node.codegenNode as VNodeCall).props as ObjectExpression
    expect(props.properties[0]).toMatchObject({
      key: {
        content: `id`,
        isStatic: true,
        loc: {
          start: {
            line: 1,
            column: 14,
          },
          end: {
            line: 1,
            column: 16,
          },
        },
      },
      value: {
        content: `id`,
        isStatic: false,
        loc: {
          start: {
            line: 1,
            column: 18,
          },
          end: {
            line: 1,
            column: 20,
          },
        },
      },
    })
  })

  test('dynamic arg', () => {
    const node = parseWithVBind(`<view v-bind:[id]="id"/>`)
    const props = (node.codegenNode as VNodeCall).props as CallExpression
    expect(props).toMatchObject({
      type: NodeTypes.JS_CALL_EXPRESSION,
      callee: NORMALIZE_PROPS,
      arguments: [
        {
          type: NodeTypes.JS_OBJECT_EXPRESSION,
          properties: [
            {
              key: {
                content: `id !== null ? id : ""`,
                isStatic: false,
              },
              value: {
                content: `id`,
                isStatic: false,
              },
            },
          ],
        },
      ],
    })
  })

  test('should error if no expression', () => {
    const onError = jest.fn()
    const node = parseWithVBind(`<view v-bind:arg />`, { onError })
    const props = (node.codegenNode as VNodeCall).props as ObjectExpression
    expect(onError.mock.calls[0][0]).toMatchObject({
      code: ErrorCodes.X_V_BIND_NO_EXPRESSION,
      loc: {
        start: {
          line: 1,
          column: 7,
        },
        end: {
          line: 1,
          column: 17,
        },
      },
    })
    expect(props.properties[0]).toMatchObject({
      key: {
        content: `arg`,
        isStatic: true,
      },
      value: {
        content: ``,
        isStatic: true,
      },
    })
  })

  test('.camel modifier', () => {
    const node = parseWithVBind(`<view v-bind:foo-bar.camel="id"/>`)
    const props = (node.codegenNode as VNodeCall).props as ObjectExpression
    expect(props.properties[0]).toMatchObject({
      key: {
        content: `fooBar`,
        isStatic: true,
      },
      value: {
        content: `id`,
        isStatic: false,
      },
    })
  })

  test('.camel modifier w/ dynamic arg', () => {
    const node = parseWithVBind(`<view v-bind:[foo].camel="id"/>`)
    const props = (node.codegenNode as VNodeCall).props as CallExpression
    expect(props).toMatchObject({
      type: NodeTypes.JS_CALL_EXPRESSION,
      callee: NORMALIZE_PROPS,
      arguments: [
        {
          type: NodeTypes.JS_OBJECT_EXPRESSION,
          properties: [
            {
              key: {
                content: `_${helperNameMap[CAMELIZE]}(foo !== null ? foo : "")`,
                isStatic: false,
              },
              value: {
                content: `id`,
                isStatic: false,
              },
            },
          ],
        },
      ],
    })
  })

  test('.camel modifier w/ dynamic arg + prefixIdentifiers', () => {
    const node = parseWithVBind(`<view v-bind:[foo(bar)].camel="id"/>`, {
      prefixIdentifiers: true,
    })
    const props = (node.codegenNode as VNodeCall).props as CallExpression
    expect(props).toMatchObject({
      type: NodeTypes.JS_CALL_EXPRESSION,
      callee: NORMALIZE_PROPS,
      arguments: [
        {
          type: NodeTypes.JS_OBJECT_EXPRESSION,
          properties: [
            {
              key: {
                children: [
                  `_${helperNameMap[CAMELIZE]}(`,
                  `(`,
                  { content: `_ctx.foo` },
                  `(`,
                  { content: `_ctx.bar` },
                  `)`,
                  `) || ""`,
                  `)`,
                ],
              },
              value: {
                content: `_ctx.id`,
                isStatic: false,
              },
            },
          ],
        },
      ],
    })
  })

  test('.prop modifier', () => {
    const node = parseWithVBind(`<view v-bind:fooBar.prop="id"/>`)
    const props = (node.codegenNode as VNodeCall).props as ObjectExpression
    expect(props.properties[0]).toMatchObject({
      key: {
        content: `.fooBar`,
        isStatic: true,
      },
      value: {
        content: `id`,
        isStatic: false,
      },
    })
  })

  test('.prop modifier w/ dynamic arg', () => {
    const node = parseWithVBind(`<view v-bind:[fooBar].prop="id"/>`)
    const props = (node.codegenNode as VNodeCall).props as CallExpression
    expect(props).toMatchObject({
      type: NodeTypes.JS_CALL_EXPRESSION,
      callee: NORMALIZE_PROPS,
      arguments: [
        {
          type: NodeTypes.JS_OBJECT_EXPRESSION,
          properties: [
            {
              key: {
                content: '`.${fooBar !== null ? fooBar : ""}`',
                isStatic: false,
              },
              value: {
                content: `id`,
                isStatic: false,
              },
            },
          ],
        },
      ],
    })
  })

  test('.prop modifier w/ dynamic arg + prefixIdentifiers', () => {
    const node = parseWithVBind(`<view v-bind:[foo(bar)].prop="id"/>`, {
      prefixIdentifiers: true,
    })
    const props = (node.codegenNode as VNodeCall).props as CallExpression
    expect(props).toMatchObject({
      type: NodeTypes.JS_CALL_EXPRESSION,
      callee: NORMALIZE_PROPS,
      arguments: [
        {
          type: NodeTypes.JS_OBJECT_EXPRESSION,
          properties: [
            {
              key: {
                children: [
                  `'.' + (`,
                  `(`,
                  { content: `_ctx.foo` },
                  `(`,
                  { content: `_ctx.bar` },
                  `)`,
                  `) || ""`,
                  `)`,
                ],
              },
              value: {
                content: `_ctx.id`,
                isStatic: false,
              },
            },
          ],
        },
      ],
    })
  })

  test('.prop modifier (shorthand)', () => {
    const node = parseWithVBind(`<view .fooBar="id"/>`)
    const props = (node.codegenNode as VNodeCall).props as ObjectExpression
    expect(props.properties[0]).toMatchObject({
      key: {
        content: `.fooBar`,
        isStatic: true,
      },
      value: {
        content: `id`,
        isStatic: false,
      },
    })
  })

  test('.attr modifier', () => {
    const node = parseWithVBind(`<view v-bind:foo-bar.attr="id"/>`)
    const props = (node.codegenNode as VNodeCall).props as ObjectExpression
    expect(props.properties[0]).toMatchObject({
      key: {
        content: `^foo-bar`,
        isStatic: true,
      },
      value: {
        content: `id`,
        isStatic: false,
      },
    })
  })
})