| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643 |
- // Note: emits and listener fallthrough is tested in
- // ./rendererAttrsFallthrough.spec.ts.
- import {
- type ComponentPublicInstance,
- createApp,
- defineComponent,
- h,
- nextTick,
- nodeOps,
- render,
- toHandlers,
- } from '@vue/runtime-test'
- import { isEmitListener } from '../src/componentEmits'
- describe('component: emit', () => {
- test('trigger handlers', () => {
- const Foo = defineComponent({
- render() {},
- created() {
- // the `emit` function is bound on component instances
- this.$emit('foo')
- this.$emit('bar')
- this.$emit('!baz')
- },
- })
- const onfoo = vi.fn()
- const onBar = vi.fn()
- const onBaz = vi.fn()
- const Comp = () => h(Foo, { onfoo, onBar, ['on!baz']: onBaz })
- render(h(Comp), nodeOps.createElement('div'))
- expect(onfoo).not.toHaveBeenCalled()
- // only capitalized or special chars are considered event listeners
- expect(onBar).toHaveBeenCalled()
- expect(onBaz).toHaveBeenCalled()
- })
- test('trigger camelCase handler', () => {
- const Foo = defineComponent({
- render() {},
- created() {
- this.$emit('test-event')
- },
- })
- const fooSpy = vi.fn()
- const Comp = () =>
- h(Foo, {
- onTestEvent: fooSpy,
- })
- render(h(Comp), nodeOps.createElement('div'))
- expect(fooSpy).toHaveBeenCalledTimes(1)
- })
- test('trigger kebab-case handler', () => {
- const Foo = defineComponent({
- render() {},
- created() {
- this.$emit('test-event')
- },
- })
- const fooSpy = vi.fn()
- const Comp = () =>
- h(Foo, {
- 'onTest-event': fooSpy,
- })
- render(h(Comp), nodeOps.createElement('div'))
- expect(fooSpy).toHaveBeenCalledTimes(1)
- })
- // #3527
- test('trigger mixed case handlers', () => {
- const Foo = defineComponent({
- render() {},
- created() {
- this.$emit('test-event')
- this.$emit('testEvent')
- },
- })
- const fooSpy = vi.fn()
- const barSpy = vi.fn()
- const Comp = () =>
- // simulate v-on="obj" usage
- h(
- Foo,
- toHandlers({
- 'test-event': fooSpy,
- testEvent: barSpy,
- }),
- )
- render(h(Comp), nodeOps.createElement('div'))
- expect(fooSpy).toHaveBeenCalledTimes(1)
- expect(barSpy).toHaveBeenCalledTimes(1)
- })
- // for v-model:foo-bar usage in DOM templates
- test('trigger hyphenated events for update:xxx events', () => {
- const Foo = defineComponent({
- render() {},
- created() {
- this.$emit('update:fooProp')
- this.$emit('update:barProp')
- },
- })
- const fooSpy = vi.fn()
- const barSpy = vi.fn()
- const Comp = () =>
- h(Foo, {
- 'onUpdate:fooProp': fooSpy,
- 'onUpdate:bar-prop': barSpy,
- })
- render(h(Comp), nodeOps.createElement('div'))
- expect(fooSpy).toHaveBeenCalled()
- expect(barSpy).toHaveBeenCalled()
- })
- test('should trigger array of listeners', async () => {
- const Child = defineComponent({
- setup(_, { emit }) {
- emit('foo', 1)
- return () => h('div')
- },
- })
- const fn1 = vi.fn()
- const fn2 = vi.fn()
- const App = {
- setup() {
- return () =>
- h(Child, {
- onFoo: [fn1, fn2],
- })
- },
- }
- render(h(App), nodeOps.createElement('div'))
- expect(fn1).toHaveBeenCalledTimes(1)
- expect(fn1).toHaveBeenCalledWith(1)
- expect(fn2).toHaveBeenCalledTimes(1)
- expect(fn2).toHaveBeenCalledWith(1)
- })
- test('warning for undeclared event (array)', () => {
- const Foo = defineComponent({
- emits: ['foo'],
- render() {},
- created() {
- // @ts-expect-error
- this.$emit('bar-baz')
- },
- })
- render(h(Foo), nodeOps.createElement('div'))
- expect(
- `Component emitted event "bar-baz" but it is neither declared in the emits option nor as an "onBarBaz" prop`,
- ).toHaveBeenWarned()
- })
- test('warning for undeclared event (object)', () => {
- const Foo = defineComponent({
- emits: {
- foo: null,
- },
- render() {},
- created() {
- // @ts-expect-error
- this.$emit('bar-baz')
- },
- })
- render(h(Foo), nodeOps.createElement('div'))
- expect(
- `Component emitted event "bar-baz" but it is neither declared in the emits option nor as an "onBarBaz" prop`,
- ).toHaveBeenWarned()
- })
- test('should not warn if has equivalent onXXX prop', () => {
- const Foo = defineComponent({
- props: ['onFoo'],
- emits: [],
- render() {},
- created() {
- // @ts-expect-error
- this.$emit('foo')
- },
- })
- render(h(Foo), nodeOps.createElement('div'))
- expect(
- `Component emitted event "foo" but it is neither declared`,
- ).not.toHaveBeenWarned()
- })
- test('should not warn if has equivalent onXXX prop with kebab-cased event', () => {
- const Foo = defineComponent({
- props: ['onFooBar'],
- emits: [],
- render() {},
- created() {
- // @ts-expect-error
- this.$emit('foo-bar')
- },
- })
- render(h(Foo), nodeOps.createElement('div'))
- expect(
- `Component emitted event "foo-bar" but it is neither declared`,
- ).not.toHaveBeenWarned()
- })
- test('validator warning', () => {
- const Foo = defineComponent({
- emits: {
- foo: (arg: number) => arg > 0,
- },
- render() {},
- created() {
- this.$emit('foo', -1)
- },
- })
- render(h(Foo), nodeOps.createElement('div'))
- expect(`event validation failed for event "foo"`).toHaveBeenWarned()
- })
- test('merging from mixins', () => {
- const mixin = {
- emits: {
- foo: (arg: number) => arg > 0,
- },
- }
- const Foo = defineComponent({
- mixins: [mixin],
- render() {},
- created() {
- this.$emit('foo', -1)
- },
- })
- render(h(Foo), nodeOps.createElement('div'))
- expect(`event validation failed for event "foo"`).toHaveBeenWarned()
- })
- // #2651
- test('should not attach normalized object when mixins do not contain emits', () => {
- const Foo = defineComponent({
- mixins: [{}],
- render() {},
- created() {
- this.$emit('foo')
- },
- })
- render(h(Foo), nodeOps.createElement('div'))
- expect(
- `Component emitted event "foo" but it is neither declared`,
- ).not.toHaveBeenWarned()
- })
- test('.once', () => {
- const Foo = defineComponent({
- render() {},
- emits: {
- foo: null,
- bar: null,
- },
- created() {
- this.$emit('foo')
- this.$emit('foo')
- this.$emit('bar')
- this.$emit('bar')
- },
- })
- const fn = vi.fn()
- const barFn = vi.fn()
- render(
- h(Foo, {
- onFooOnce: fn,
- onBarOnce: barFn,
- }),
- nodeOps.createElement('div'),
- )
- expect(fn).toHaveBeenCalledTimes(1)
- expect(barFn).toHaveBeenCalledTimes(1)
- })
- test('.once with normal listener of the same name', () => {
- const Foo = defineComponent({
- render() {},
- emits: {
- foo: null,
- },
- created() {
- this.$emit('foo')
- this.$emit('foo')
- },
- })
- const onFoo = vi.fn()
- const onFooOnce = vi.fn()
- render(
- h(Foo, {
- onFoo,
- onFooOnce,
- }),
- nodeOps.createElement('div'),
- )
- expect(onFoo).toHaveBeenCalledTimes(2)
- expect(onFooOnce).toHaveBeenCalledTimes(1)
- })
- test('.number modifier should work with v-model on component', () => {
- const Foo = defineComponent({
- render() {},
- created() {
- this.$emit('update:modelValue', '1')
- this.$emit('update:foo', '2')
- },
- })
- const fn1 = vi.fn()
- const fn2 = vi.fn()
- const Comp = () =>
- h(Foo, {
- modelValue: null,
- modelModifiers: { number: true },
- 'onUpdate:modelValue': fn1,
- foo: null,
- fooModifiers: { number: true },
- 'onUpdate:foo': fn2,
- })
- render(h(Comp), nodeOps.createElement('div'))
- expect(fn1).toHaveBeenCalledTimes(1)
- expect(fn1).toHaveBeenCalledWith(1)
- expect(fn2).toHaveBeenCalledTimes(1)
- expect(fn2).toHaveBeenCalledWith(2)
- })
- test('.trim modifier should work with v-model on component', () => {
- const Foo = defineComponent({
- render() {},
- created() {
- this.$emit('update:modelValue', ' one ')
- this.$emit('update:foo', ' two ')
- },
- })
- const fn1 = vi.fn()
- const fn2 = vi.fn()
- const Comp = () =>
- h(Foo, {
- modelValue: null,
- modelModifiers: { trim: true },
- 'onUpdate:modelValue': fn1,
- foo: null,
- fooModifiers: { trim: true },
- 'onUpdate:foo': fn2,
- })
- render(h(Comp), nodeOps.createElement('div'))
- expect(fn1).toHaveBeenCalledTimes(1)
- expect(fn1).toHaveBeenCalledWith('one')
- expect(fn2).toHaveBeenCalledTimes(1)
- expect(fn2).toHaveBeenCalledWith('two')
- })
- test('.trim modifier should work with v-model on component for kebab-cased props and camelCased emit', () => {
- const Foo = defineComponent({
- render() {},
- created() {
- this.$emit('update:firstName', ' one ')
- },
- })
- const fn1 = vi.fn()
- const Comp = () =>
- h(Foo, {
- 'first-name': null,
- 'first-nameModifiers': { trim: true },
- 'onUpdate:first-name': fn1,
- })
- render(h(Comp), nodeOps.createElement('div'))
- expect(fn1).toHaveBeenCalledTimes(1)
- expect(fn1).toHaveBeenCalledWith('one')
- })
- test('.trim modifier should work with v-model on component for camelCased props and kebab-cased emit', () => {
- const Foo = defineComponent({
- render() {},
- created() {
- this.$emit('update:model-value', ' one ')
- this.$emit('update:first-name', ' two ')
- },
- })
- const fn1 = vi.fn()
- const fn2 = vi.fn()
- const Comp = () =>
- h(Foo, {
- modelValue: null,
- modelModifiers: { trim: true },
- 'onUpdate:modelValue': fn1,
- firstName: null,
- firstNameModifiers: { trim: true },
- 'onUpdate:firstName': fn2,
- })
- render(h(Comp), nodeOps.createElement('div'))
- expect(fn1).toHaveBeenCalledTimes(1)
- expect(fn1).toHaveBeenCalledWith('one')
- expect(fn2).toHaveBeenCalledTimes(1)
- expect(fn2).toHaveBeenCalledWith('two')
- })
- test('.trim modifier should work with v-model on component for mixed cased props and emit', () => {
- const Foo = defineComponent({
- render() {},
- created() {
- this.$emit('update:base-URL', ' one ')
- },
- })
- const fn1 = vi.fn()
- const Comp = () =>
- h(Foo, {
- 'base-URL': null,
- 'base-URLModifiers': { trim: true },
- 'onUpdate:base-URL': fn1,
- })
- render(h(Comp), nodeOps.createElement('div'))
- expect(fn1).toHaveBeenCalledTimes(1)
- expect(fn1).toHaveBeenCalledWith('one')
- })
- test('.trim and .number modifiers should work with v-model on component', () => {
- const Foo = defineComponent({
- render() {},
- created() {
- this.$emit('update:modelValue', ' +01.2 ')
- this.$emit('update:foo', ' 1 ')
- },
- })
- const fn1 = vi.fn()
- const fn2 = vi.fn()
- const Comp = () =>
- h(Foo, {
- modelValue: null,
- modelModifiers: { trim: true, number: true },
- 'onUpdate:modelValue': fn1,
- foo: null,
- fooModifiers: { trim: true, number: true },
- 'onUpdate:foo': fn2,
- })
- render(h(Comp), nodeOps.createElement('div'))
- expect(fn1).toHaveBeenCalledTimes(1)
- expect(fn1).toHaveBeenCalledWith(1.2)
- expect(fn2).toHaveBeenCalledTimes(1)
- expect(fn2).toHaveBeenCalledWith(1)
- })
- test('only trim string parameter when work with v-model on component', () => {
- const Foo = defineComponent({
- render() {},
- created() {
- this.$emit('update:modelValue', ' foo ', { bar: ' bar ' })
- },
- })
- const fn = vi.fn()
- const Comp = () =>
- h(Foo, {
- modelValue: null,
- modelModifiers: { trim: true },
- 'onUpdate:modelValue': fn,
- })
- render(h(Comp), nodeOps.createElement('div'))
- expect(fn).toHaveBeenCalledTimes(1)
- expect(fn).toHaveBeenCalledWith('foo', { bar: ' bar ' })
- })
- test('isEmitListener', () => {
- const options = {
- click: null,
- 'test-event': null,
- fooBar: null,
- FooBaz: null,
- }
- expect(isEmitListener(options, 'onClick')).toBe(true)
- expect(isEmitListener(options, 'onclick')).toBe(false)
- expect(isEmitListener(options, 'onBlick')).toBe(false)
- // .once listeners
- expect(isEmitListener(options, 'onClickOnce')).toBe(true)
- expect(isEmitListener(options, 'onclickOnce')).toBe(false)
- // kebab-case option
- expect(isEmitListener(options, 'onTestEvent')).toBe(true)
- // camelCase option
- expect(isEmitListener(options, 'onFooBar')).toBe(true)
- // PascalCase option
- expect(isEmitListener(options, 'onFooBaz')).toBe(true)
- })
- test('does not emit after unmount', async () => {
- const fn = vi.fn()
- const Foo = defineComponent({
- emits: ['closing'],
- async beforeUnmount() {
- await this.$nextTick()
- this.$emit('closing', true)
- },
- render() {
- return h('div')
- },
- })
- const Comp = () =>
- h(Foo, {
- onClosing: fn,
- })
- const el = nodeOps.createElement('div')
- render(h(Comp), el)
- await nextTick()
- render(null, el)
- await nextTick()
- expect(fn).not.toHaveBeenCalled()
- })
- test('merge string array emits', async () => {
- const ComponentA = defineComponent({
- emits: ['one', 'two'],
- })
- const ComponentB = defineComponent({
- emits: ['three'],
- })
- const renderFn = vi.fn(function (this: ComponentPublicInstance) {
- expect(this.$options.emits).toEqual(['one', 'two', 'three'])
- return h('div')
- })
- const ComponentC = defineComponent({
- render: renderFn,
- mixins: [ComponentA, ComponentB],
- })
- const el = nodeOps.createElement('div')
- expect(renderFn).toHaveBeenCalledTimes(0)
- render(h(ComponentC), el)
- expect(renderFn).toHaveBeenCalledTimes(1)
- })
- test('merge object emits', async () => {
- const twoFn = vi.fn((v: unknown) => !v)
- const ComponentA = defineComponent({
- emits: {
- one: null,
- two: twoFn,
- },
- })
- const ComponentB = defineComponent({
- emits: ['three'],
- })
- const renderFn = vi.fn(function (this: ComponentPublicInstance) {
- expect(this.$options.emits).toEqual({
- one: null,
- two: twoFn,
- three: null,
- })
- expect(this.$options.emits.two).toBe(twoFn)
- return h('div')
- })
- const ComponentC = defineComponent({
- render: renderFn,
- mixins: [ComponentA, ComponentB],
- })
- const el = nodeOps.createElement('div')
- expect(renderFn).toHaveBeenCalledTimes(0)
- render(h(ComponentC), el)
- expect(renderFn).toHaveBeenCalledTimes(1)
- })
- test('merging emits for a component that is also used as a mixin', () => {
- const render = () => h('div')
- const CompA = {
- render,
- }
- const validateByMixin = vi.fn(() => true)
- const validateByGlobalMixin = vi.fn(() => true)
- const mixin = {
- emits: {
- one: validateByMixin,
- },
- }
- const CompB = defineComponent({
- mixins: [mixin, CompA],
- created(this) {
- this.$emit('one', 1)
- },
- render,
- })
- const app = createApp({
- render() {
- return [h(CompA), h(CompB)]
- },
- })
- app.mixin({
- emits: {
- one: validateByGlobalMixin,
- two: null,
- },
- })
- const root = nodeOps.createElement('div')
- app.mount(root)
- expect(validateByMixin).toHaveBeenCalledTimes(1)
- expect(validateByGlobalMixin).not.toHaveBeenCalled()
- })
- })
|