Back to Repositories

Validating v-on Directive Compilation Workflow in dcloudio/uni-app

A comprehensive test suite that validates v-on directive transformations in uni-app’s compiler implementation. This suite ensures proper event handling, dynamic argument processing, and expression evaluation across different usage patterns.

Test Coverage Overview

The test suite provides extensive coverage of v-on directive functionality in the uni-app compiler.

Key areas tested include:
  • Basic event binding and handler transformation
  • Dynamic argument handling
  • Expression evaluation and transformation
  • Event modifier processing
  • Case conversion for kebab-case events
  • Cache handler optimization scenarios

Implementation Analysis

The testing approach utilizes Jest’s describe/test structure with custom assertion utilities. Tests validate compiler output against expected template transformations and runtime code generation.

Implementation patterns focus on:
  • Template to AST transformation verification
  • Runtime code generation accuracy
  • Expression handling with different identifier modes
  • Error condition validation

Technical Details

Testing infrastructure includes:
  • Jest as the primary test runner
  • Custom assert utility for comparing compiler output
  • Vue compiler-core integration
  • Mock functions for error handling validation
  • TypeScript type checking for compiler options

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through:

  • Comprehensive edge case coverage
  • Isolated test cases with clear assertions
  • Structured test organization by feature area
  • Error condition validation
  • Type-safe testing approaches

dcloudio/uni-app

packages/uni-mp-compiler/__tests__/vOn.spec.ts

            
import { type ElementNode, ErrorCodes } from '@vue/compiler-core'
import { compile } from '../src'
import type { CompilerOptions } from '../src/options'
import { assert } from './testUtils'

function parseWithVOn(template: string, options: CompilerOptions = {}) {
  const { ast } = compile(template, {
    generatorOpts: {
      concise: true,
    },
    ...options,
  })
  return {
    root: ast,
    node: ast.children[0] as ElementNode,
  }
}

describe('compiler: transform v-on', () => {
  test('basic', () => {
    assert(
      `<view v-on:click="onClick"/>`,
      `<view bindtap="{{a}}"/>`,
      `(_ctx, _cache) => {
  return { a: _o(_ctx.onClick) }
}`
    )
    assert(
      `<custom v-on:click="onClick"/>`,
      `<custom bindclick="{{a}}" u-i="2a9ec0b0-0"/>`,
      `(_ctx, _cache) => {
  return { a: _o(_ctx.onClick) }
}`
    )
  })
  test('dynamic arg', () => {
    // <view v-on:[event]="handler"/>
  })
  test('dynamic arg with prefixing', () => {
    // <view v-on:[event]="handler"/>
  })
  test('dynamic arg with complex exp prefixing', () => {
    // <view v-on:[event(foo)]="handler"/>
  })
  test('should wrap as function if expression is inline statement', () => {
    assert(
      `<view @click="i++"/>`,
      `<view bindtap="{{a}}"/>`,
      `(_ctx, _cache) => {
  return { a: _o($event => _ctx.i++) }
}`
    )
  })
  test('should handle multiple inline statement', () => {
    assert(
      `<view @click="foo();bar()"/>`,
      `<view bindtap="{{a}}"/>`,
      `(_ctx, _cache) => {
  return { a: _o($event => { _ctx.foo(); _ctx.bar(); }) }
}`
    )
  })
  test('should handle multi-line statement', () => {
    assert(
      `<view @click="\nfoo();\nbar()\n"/>`,
      `<view bindtap="{{a}}"/>`,
      `(_ctx, _cache) => {
  with (_ctx) {
    const { o: _o } = _Vue

    return { a: _o($event => { foo(); bar(); }) }
  }
}`,
      { prefixIdentifiers: false, mode: 'function' }
    )
  })
  test('inline statement w/ prefixIdentifiers: true', () => {
    assert(
      `<view @click="foo($event)"/>`,
      `<view bindtap="{{a}}"/>`,
      `(_ctx, _cache) => {
  return { a: _o($event => _ctx.foo($event)) }
}`
    )
  })
  test('multiple inline statements w/ prefixIdentifiers: true', () => {
    assert(
      `<view @click="foo($event);bar()"/>`,
      `<view bindtap="{{a}}"/>`,
      `(_ctx, _cache) => {
  return { a: _o($event => { _ctx.foo($event); _ctx.bar(); }) }
}`
    )
  })
  test('should NOT wrap as function if expression is already function expression', () => {
    assert(
      `<view @click="$event => foo($event)"/>`,
      `<view bindtap="{{a}}"/>`,
      `(_ctx, _cache) => {
  return { a: _o($event => _ctx.foo($event)) }
}`
    )
  })
  test('should NOT wrap as function if expression is already function expression (with newlines)', () => {
    assert(
      `<view @click="
  $event => {
    foo($event)
  }
"/>`,
      `<view bindtap="{{a}}"/>`,
      `(_ctx, _cache) => {
  return { a: _o($event => { _ctx.foo($event); }) }
}`
    )
  })
  test('should NOT wrap as function if expression is already function expression (with newlines + function keyword)', () => {
    assert(
      `<view @click="
  function($event) {
    foo($event)
  }
"/>`,
      `<view bindtap="{{a}}"/>`,
      `(_ctx, _cache) => {
  return { a: _o(function ($event) { _ctx.foo($event); }) }
}`
    )
  })
  test('should NOT wrap as function if expression is complex member expression', () => {
    assert(
      `<view @click="a['b' + c]"/>`,
      `<view bindtap="{{a}}"/>`,
      `(_ctx, _cache) => {
  with (_ctx) {
    const { o: _o } = _Vue

    return { a: _o(a['b' + c]) }
  }
}`,
      {
        prefixIdentifiers: false,
        mode: 'function',
      }
    )
  })
  test('complex member expression w/ prefixIdentifiers: true', () => {
    assert(
      `<view @click="a['b' + c]"/>`,
      `<view bindtap="{{a}}"/>`,
      `(_ctx, _cache) => {
  return { a: _o(_ctx.a['b' + _ctx.c]) }
}`
    )
  })
  test('function expression w/ prefixIdentifiers: true', () => {
    assert(
      `<view @click="e => foo(e)"/>`,
      `<view bindtap="{{a}}"/>`,
      `(_ctx, _cache) => {
  return { a: _o(e => _ctx.foo(e)) }
}`
    )
  })
  test('should error if no expression AND no modifier', () => {
    const onError = jest.fn()
    parseWithVOn(`<div v-on:click />`, { onError })
    expect(onError.mock.calls[0][0]).toMatchObject({
      code: ErrorCodes.X_V_ON_NO_EXPRESSION,
      loc: {
        start: {
          line: 1,
          column: 6,
        },
        end: {
          line: 1,
          column: 16,
        },
      },
    })
  })
  test('should NOT error if no expression but has modifier', () => {
    const onError = jest.fn()
    parseWithVOn(`<div v-on:click.prevent />`, { onError })
    expect(onError).not.toHaveBeenCalled()
  })

  test('case conversion for kebab-case events', () => {
    assert(
      `<view v-on:foo-bar="onMount"/>`,
      `<view bind:foo-bar="{{a}}"/>`,
      `(_ctx, _cache) => {
  return { a: _o(_ctx.onMount) }
}`
    )
  })

  test('case conversion for vnode hooks', () => {
    assert(
      `<view v-on:vnode-mounted="onMount"/>`,
      `<view bind:vnode-mounted="{{a}}"/>`,
      `(_ctx, _cache) => {
  return { a: _o(_ctx.onMount) }
}`
    )
  })

  describe('cacheHandler', () => {
    test('empty handler', () => {
      assert(
        `<view v-on:click.prevent />`,
        `<view catchtap="{{a}}"/>`,
        `(_ctx, _cache) => {
  return { a: _o(() => {}) }
}`
      )
    })

    test('member expression handler', () => {
      // <div v-on:click="foo" />
    })

    test('compound member expression handler', () => {
      // <div v-on:click="foo.bar" />
    })

    test('bail on component member expression handler', () => {
      // <comp v-on:click="foo" />
    })

    test('should not be cached inside v-once', () => {
      // <div v-once><div v-on:click="foo"/></div>
    })

    test('inline function expression handler', () => {
      // <div v-on:click="() => foo()" />
    })

    test('inline async arrow function expression handler', () => {
      // <div v-on:click="async () => await foo()" />
    })

    test('inline async function expression handler', () => {
      // <div v-on:click="async function () { await foo() } " />
    })

    test('inline statement handler', () => {
      // <div v-on:click="foo++" />
    })
  })
})