test: add all tests for `v-on` (#52)

This commit is contained in:
Rizumu Ayaka 2023-12-12 15:58:07 +08:00 committed by GitHub
parent 2e25c22ddf
commit 42b913283b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 602 additions and 31 deletions

View File

@ -1,5 +1,31 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`v-on > case conversion for kebab-case events 1`] = `
"import { template as _template, children as _children, on as _on } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_on(n1, "fooBar", (...args) => (_ctx.onMount && _ctx.onMount(...args)))
return n0
}"
`;
exports[`v-on > dynamic arg 1`] = `
"import { template as _template, children as _children, effect as _effect, on as _on } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_effect(() => {
_on(n1, _ctx.event, (...args) => (_ctx.handler && _ctx.handler(...args)))
})
return n0
}"
`;
exports[`v-on > event modifier 1`] = ` exports[`v-on > event modifier 1`] = `
"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers, withKeys as _withKeys } from 'vue/vapor'; "import { template as _template, children as _children, on as _on, withModifiers as _withModifiers, withKeys as _withKeys } from 'vue/vapor';
@ -33,6 +59,131 @@ export function render(_ctx) {
}" }"
`; `;
exports[`v-on > should not wrap keys guard if no key modifier is present 1`] = `
"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_on(n1, "keyup", _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["exact"]))
return n0
}"
`;
exports[`v-on > should support multiple events and modifiers options w/ prefixIdentifiers: true 1`] = `
"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers, withKeys as _withKeys } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_on(n1, "click", _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["stop"]))
_on(n1, "keyup", _withKeys((...args) => (_ctx.test && _ctx.test(...args)), ["enter"]))
return n0
}"
`;
exports[`v-on > should support multiple modifiers and event options w/ prefixIdentifiers: true 1`] = `
"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_on(n1, "click", _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["stop", "prevent"]), { capture: true, once: true })
return n0
}"
`;
exports[`v-on > should transform click.middle 1`] = `
"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_on(n1, "mouseup", _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["middle"]))
return n0
}"
`;
exports[`v-on > should transform click.middle 2`] = `
"import { template as _template, children as _children, effect as _effect, on as _on, withModifiers as _withModifiers } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_effect(() => {
_on(n1, (_ctx.event) === "click" ? "mouseup" : (_ctx.event), _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["middle"]))
})
return n0
}"
`;
exports[`v-on > should transform click.right 1`] = `
"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_on(n1, "contextmenu", _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["right"]))
return n0
}"
`;
exports[`v-on > should transform click.right 2`] = `
"import { template as _template, children as _children, effect as _effect, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_effect(() => {
_on(n1, (_ctx.event) === "click" ? "contextmenu" : (_ctx.event), _withKeys(_withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["right"]), ["right"]))
})
return n0
}"
`;
exports[`v-on > should wrap as function if expression is inline statement 1`] = `
"import { template as _template, children as _children, on as _on } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_on(n1, "click", (...args) => (_ctx.i++ && _ctx.i++(...args)))
return n0
}"
`;
exports[`v-on > should wrap keys guard for keyboard events or dynamic events 1`] = `
"import { template as _template, children as _children, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_on(n1, "keydown", _withKeys(_withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["stop", "ctrl"]), ["a"]), { capture: true })
return n0
}"
`;
exports[`v-on > should wrap keys guard for static key event w/ left/right modifiers 1`] = `
"import { template as _template, children as _children, on as _on, withKeys as _withKeys } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_on(n1, "keyup", _withKeys((...args) => (_ctx.test && _ctx.test(...args)), ["left"]))
return n0
}"
`;
exports[`v-on > simple expression 1`] = ` exports[`v-on > simple expression 1`] = `
"import { template as _template, children as _children, on as _on } from 'vue/vapor'; "import { template as _template, children as _children, on as _on } from 'vue/vapor';

View File

@ -80,6 +80,9 @@ describe('v-html', () => {
expect(ir.vaporHelpers).contains('setHtml') expect(ir.vaporHelpers).contains('setHtml')
expect(ir.helpers.size).toBe(0) expect(ir.helpers.size).toBe(0)
// children should have been removed
expect(ir.template).toMatchObject([{ template: '<div></div>' }])
expect(ir.operation).toEqual([]) expect(ir.operation).toEqual([])
expect(ir.effect).toMatchObject([ expect(ir.effect).toMatchObject([
{ {
@ -109,6 +112,8 @@ describe('v-html', () => {
]) ])
expect(code).matchSnapshot() expect(code).matchSnapshot()
// children should have been removed
expect(code).contains('template("<div></div>")')
}) })
test('should raise error if has no expression', () => { test('should raise error if has no expression', () => {

View File

@ -1,45 +1,72 @@
import { type RootNode, BindingTypes, ErrorCodes } from '@vue/compiler-dom' import { BindingTypes, ErrorCodes, parse, NodeTypes } from '@vue/compiler-dom'
import { type CompilerOptions, compile as _compile } from '../../src' import {
type CompilerOptions,
compile as _compile,
RootIRNode,
transform,
generate,
IRNodeTypes,
} from '../../src'
function compile(template: string | RootNode, options: CompilerOptions = {}) { import { transformVOn } from '../../src/transforms/vOn'
let { code } = _compile(template, { import { transformElement } from '../../src/transforms/transformElement'
...options,
mode: 'module', function compileWithVOn(
template: string,
options: CompilerOptions = {},
): {
ir: RootIRNode
code: string
} {
const ast = parse(template, { prefixIdentifiers: true, ...options })
const ir = transform(ast, {
nodeTransforms: [transformElement],
directiveTransforms: {
on: transformVOn,
},
prefixIdentifiers: true, prefixIdentifiers: true,
...options,
}) })
return code const { code } = generate(ir, { prefixIdentifiers: true, ...options })
return { ir, code }
} }
describe('v-on', () => { describe('v-on', () => {
test('simple expression', () => { test('simple expression', () => {
const code = compile(`<div @click="handleClick"></div>`, { const { code, ir } = compileWithVOn(`<div @click="handleClick"></div>`, {
bindingMetadata: { bindingMetadata: {
handleClick: BindingTypes.SETUP_CONST, handleClick: BindingTypes.SETUP_CONST,
}, },
}) })
expect(ir.vaporHelpers).contains('on')
expect(ir.helpers.size).toBe(0)
expect(ir.effect).toEqual([])
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
element: 1,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'click',
isStatic: true,
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'handleClick',
isStatic: false,
},
modifiers: { keys: [], nonKeys: [], options: [] },
keyOverride: undefined,
},
])
expect(code).matchSnapshot() expect(code).matchSnapshot()
}) })
test('should error if no expression AND no modifier', () => {
const onError = vi.fn()
compile(`<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('event modifier', () => { test('event modifier', () => {
const code = compile( const { code } = compileWithVOn(
`<a @click.stop="handleEvent"></a> `<a @click.stop="handleEvent"></a>
<form @submit.prevent="handleEvent"></form> <form @submit.prevent="handleEvent"></form>
<a @click.stop.prevent="handleEvent"></a> <a @click.stop.prevent="handleEvent"></a>
@ -70,4 +97,364 @@ describe('v-on', () => {
) )
expect(code).matchSnapshot() expect(code).matchSnapshot()
}) })
test('dynamic arg', () => {
const { code, ir } = compileWithVOn(`<div v-on:[event]="handler"/>`)
expect(ir.vaporHelpers).contains('on')
expect(ir.vaporHelpers).contains('effect')
expect(ir.helpers.size).toBe(0)
expect(ir.operation).toEqual([])
expect(ir.effect[0].operations[0]).toMatchObject({
type: IRNodeTypes.SET_EVENT,
element: 1,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'event',
isStatic: false,
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'handler',
isStatic: false,
},
})
expect(code).matchSnapshot()
})
test.todo('dynamic arg with prefixing')
test.todo('dynamic arg with complex exp prefixing')
test.todo('should wrap as function if expression is inline statement')
test.todo('should handle multiple inline statement')
test.todo('should handle multi-line statement')
test.todo('inline statement w/ prefixIdentifiers: true')
test.todo('multiple inline statements w/ prefixIdentifiers: true')
test.todo(
'should NOT wrap as function if expression is already function expression',
)
test.todo(
'should NOT wrap as function if expression is already function expression (with Typescript)',
)
test.todo(
'should NOT wrap as function if expression is already function expression (with newlines)',
)
test.todo(
'should NOT wrap as function if expression is already function expression (with newlines + function keyword)',
)
test.todo(
'should NOT wrap as function if expression is complex member expression',
)
test.todo('complex member expression w/ prefixIdentifiers: true')
test.todo('function expression w/ prefixIdentifiers: true')
test('should error if no expression AND no modifier', () => {
const onError = vi.fn()
compileWithVOn(`<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 = vi.fn()
compileWithVOn(`<div v-on:click.prevent />`, { onError })
expect(onError).not.toHaveBeenCalled()
})
test('case conversion for kebab-case events', () => {
const { ir, code } = compileWithVOn(`<div v-on:foo-bar="onMount"/>`)
expect(ir.vaporHelpers).contains('on')
expect(ir.helpers.size).toBe(0)
expect(ir.effect).toEqual([])
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
element: 1,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'fooBar',
isStatic: true,
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'onMount',
isStatic: false,
},
},
])
expect(code).matchSnapshot()
expect(code).contains('fooBar')
})
test('error for vnode hooks', () => {
const onError = vi.fn()
compileWithVOn(`<div v-on:vnode-mounted="onMount"/>`, { onError })
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_VNODE_HOOKS,
loc: {
start: {
line: 1,
column: 11,
},
end: {
line: 1,
column: 24,
},
},
})
})
test.todo('vue: prefixed events')
test('should support multiple modifiers and event options w/ prefixIdentifiers: true', () => {
const { code, ir } = compileWithVOn(
`<div @click.stop.prevent.capture.once="test"/>`,
{
prefixIdentifiers: true,
},
)
expect(ir.vaporHelpers).contains('on')
expect(ir.vaporHelpers).contains('withModifiers')
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'test',
isStatic: false,
},
modifiers: {
keys: [],
nonKeys: ['stop', 'prevent'],
options: ['capture', 'once'],
},
keyOverride: undefined,
},
])
expect(code).matchSnapshot()
expect(code).contains('_withModifiers')
expect(code).contains('["stop", "prevent"]')
expect(code).contains('{ capture: true, once: true }')
})
test('should support multiple events and modifiers options w/ prefixIdentifiers: true', () => {
const { code, ir } = compileWithVOn(
`<div @click.stop="test" @keyup.enter="test" />`,
{
prefixIdentifiers: true,
},
)
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'click',
isStatic: true,
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'test',
isStatic: false,
},
modifiers: {
keys: [],
nonKeys: ['stop'],
options: [],
},
},
{
type: IRNodeTypes.SET_EVENT,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'keyup',
isStatic: true,
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'test',
isStatic: false,
},
modifiers: {
keys: ['enter'],
nonKeys: [],
options: [],
},
},
])
expect(code).matchSnapshot()
expect(code).contains(
'_on(n1, "click", _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["stop"]))',
)
expect(code).contains(
'_on(n1, "keyup", _withKeys((...args) => (_ctx.test && _ctx.test(...args)), ["enter"]))',
)
})
test('should wrap keys guard for keyboard events or dynamic events', () => {
const { code, ir } = compileWithVOn(
`<div @keydown.stop.capture.ctrl.a="test"/>`,
{
prefixIdentifiers: true,
},
)
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
element: 1,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'keydown',
isStatic: true,
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'test',
isStatic: false,
},
modifiers: {
keys: ['a'],
nonKeys: ['stop', 'ctrl'],
options: ['capture'],
},
},
])
expect(code).matchSnapshot()
})
test('should not wrap keys guard if no key modifier is present', () => {
const { code, ir } = compileWithVOn(`<div @keyup.exact="test"/>`, {
prefixIdentifiers: true,
})
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
modifiers: { nonKeys: ['exact'] },
},
])
expect(code).matchSnapshot()
})
test('should wrap keys guard for static key event w/ left/right modifiers', () => {
const { code, ir } = compileWithVOn(`<div @keyup.left="test"/>`, {
prefixIdentifiers: true,
})
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
modifiers: { keys: ['left'] },
},
])
expect(code).matchSnapshot()
})
test.todo('should wrap both for dynamic key event w/ left/right modifiers')
test('should transform click.right', () => {
const { code, ir } = compileWithVOn(`<div @click.right="test"/>`)
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'contextmenu',
isStatic: true,
},
modifiers: { nonKeys: ['right'] },
keyOverride: undefined,
},
])
expect(code).matchSnapshot()
expect(code).contains('"contextmenu"')
// dynamic
const { code: code2, ir: ir2 } = compileWithVOn(
`<div @[event].right="test"/>`,
)
expect(ir2.effect[0].operations).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'event',
isStatic: false,
},
modifiers: { nonKeys: ['right'] },
keyOverride: ['click', 'contextmenu'],
},
])
expect(code2).matchSnapshot()
expect(code2).contains(
'(_ctx.event) === "click" ? "contextmenu" : (_ctx.event)',
)
})
test('should transform click.middle', () => {
const { code, ir } = compileWithVOn(`<div @click.middle="test"/>`)
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'mouseup',
isStatic: true,
},
modifiers: { nonKeys: ['middle'] },
keyOverride: undefined,
},
])
expect(code).matchSnapshot()
expect(code).contains('"mouseup"')
// dynamic
const { code: code2, ir: ir2 } = compileWithVOn(
`<div @[event].middle="test"/>`,
)
expect(ir2.effect[0].operations).toMatchObject([
{
type: IRNodeTypes.SET_EVENT,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'event',
isStatic: false,
},
modifiers: { nonKeys: ['middle'] },
keyOverride: ['click', 'mouseup'],
},
])
expect(code2).matchSnapshot()
expect(code2).contains(
'(_ctx.event) === "click" ? "mouseup" : (_ctx.event)',
)
})
}) })

View File

@ -1,7 +1,12 @@
import { createCompilerError, ErrorCodes } from '@vue/compiler-core' import {
createCompilerError,
ElementTypes,
ErrorCodes,
} from '@vue/compiler-core'
import type { DirectiveTransform } from '../transform' import type { DirectiveTransform } from '../transform'
import { IRNodeTypes, KeyOverride } from '../ir' import { IRNodeTypes, KeyOverride, SetEventIRNode } from '../ir'
import { resolveModifiers } from '@vue/compiler-dom' import { resolveModifiers } from '@vue/compiler-dom'
import { camelize } from '@vue/shared'
export const transformVOn: DirectiveTransform = (dir, node, context) => { export const transformVOn: DirectiveTransform = (dir, node, context) => {
let { arg, exp, loc, modifiers } = dir let { arg, exp, loc, modifiers } = dir
@ -16,6 +21,23 @@ export const transformVOn: DirectiveTransform = (dir, node, context) => {
return return
} }
if (arg.isStatic) {
let rawName = arg.content
if (__DEV__ && rawName.startsWith('vnode')) {
context.options.onError(
createCompilerError(ErrorCodes.X_VNODE_HOOKS, arg.loc),
)
}
if (
node.tagType !== ElementTypes.ELEMENT ||
rawName.startsWith('vnode') ||
!/[A-Z]/.test(rawName)
) {
arg.content = camelize(arg.content)
}
}
const { keyModifiers, nonKeyModifiers, eventOptionModifiers } = const { keyModifiers, nonKeyModifiers, eventOptionModifiers } =
resolveModifiers( resolveModifiers(
arg.isStatic ? `on${arg.content}` : arg, arg.isStatic ? `on${arg.content}` : arg,
@ -48,7 +70,7 @@ export const transformVOn: DirectiveTransform = (dir, node, context) => {
} }
} }
context.registerOperation({ const operation: SetEventIRNode = {
type: IRNodeTypes.SET_EVENT, type: IRNodeTypes.SET_EVENT,
loc, loc,
element: context.reference(), element: context.reference(),
@ -60,5 +82,11 @@ export const transformVOn: DirectiveTransform = (dir, node, context) => {
options: eventOptionModifiers, options: eventOptionModifiers,
}, },
keyOverride, keyOverride,
}) }
if (arg.isStatic) {
context.registerOperation(operation)
} else {
context.registerEffect([arg], [operation])
}
} }