componentEmits.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. // Note: emits and listener fallthrough is tested in
  2. // ./rendererAttrsFallthrough.spec.ts.
  3. import { vi } from 'vitest'
  4. import {
  5. render,
  6. defineComponent,
  7. h,
  8. nodeOps,
  9. toHandlers,
  10. nextTick,
  11. ComponentPublicInstance
  12. } from '@vue/runtime-test'
  13. import { isEmitListener } from '../src/componentEmits'
  14. describe('component: emit', () => {
  15. test('trigger handlers', () => {
  16. const Foo = defineComponent({
  17. render() {},
  18. created() {
  19. // the `emit` function is bound on component instances
  20. this.$emit('foo')
  21. this.$emit('bar')
  22. this.$emit('!baz')
  23. }
  24. })
  25. const onfoo = vi.fn()
  26. const onBar = vi.fn()
  27. const onBaz = vi.fn()
  28. const Comp = () => h(Foo, { onfoo, onBar, ['on!baz']: onBaz })
  29. render(h(Comp), nodeOps.createElement('div'))
  30. expect(onfoo).not.toHaveBeenCalled()
  31. // only capitalized or special chars are considered event listeners
  32. expect(onBar).toHaveBeenCalled()
  33. expect(onBaz).toHaveBeenCalled()
  34. })
  35. test('trigger camelCase handler', () => {
  36. const Foo = defineComponent({
  37. render() {},
  38. created() {
  39. this.$emit('test-event')
  40. }
  41. })
  42. const fooSpy = vi.fn()
  43. const Comp = () =>
  44. h(Foo, {
  45. onTestEvent: fooSpy
  46. })
  47. render(h(Comp), nodeOps.createElement('div'))
  48. expect(fooSpy).toHaveBeenCalledTimes(1)
  49. })
  50. test('trigger kebab-case handler', () => {
  51. const Foo = defineComponent({
  52. render() {},
  53. created() {
  54. this.$emit('test-event')
  55. }
  56. })
  57. const fooSpy = vi.fn()
  58. const Comp = () =>
  59. h(Foo, {
  60. 'onTest-event': fooSpy
  61. })
  62. render(h(Comp), nodeOps.createElement('div'))
  63. expect(fooSpy).toHaveBeenCalledTimes(1)
  64. })
  65. // #3527
  66. test('trigger mixed case handlers', () => {
  67. const Foo = defineComponent({
  68. render() {},
  69. created() {
  70. this.$emit('test-event')
  71. this.$emit('testEvent')
  72. }
  73. })
  74. const fooSpy = vi.fn()
  75. const barSpy = vi.fn()
  76. const Comp = () =>
  77. // simulate v-on="obj" usage
  78. h(
  79. Foo,
  80. toHandlers({
  81. 'test-event': fooSpy,
  82. testEvent: barSpy
  83. })
  84. )
  85. render(h(Comp), nodeOps.createElement('div'))
  86. expect(fooSpy).toHaveBeenCalledTimes(1)
  87. expect(barSpy).toHaveBeenCalledTimes(1)
  88. })
  89. // for v-model:foo-bar usage in DOM templates
  90. test('trigger hyphenated events for update:xxx events', () => {
  91. const Foo = defineComponent({
  92. render() {},
  93. created() {
  94. this.$emit('update:fooProp')
  95. this.$emit('update:barProp')
  96. }
  97. })
  98. const fooSpy = vi.fn()
  99. const barSpy = vi.fn()
  100. const Comp = () =>
  101. h(Foo, {
  102. 'onUpdate:fooProp': fooSpy,
  103. 'onUpdate:bar-prop': barSpy
  104. })
  105. render(h(Comp), nodeOps.createElement('div'))
  106. expect(fooSpy).toHaveBeenCalled()
  107. expect(barSpy).toHaveBeenCalled()
  108. })
  109. test('should trigger array of listeners', async () => {
  110. const Child = defineComponent({
  111. setup(_, { emit }) {
  112. emit('foo', 1)
  113. return () => h('div')
  114. }
  115. })
  116. const fn1 = vi.fn()
  117. const fn2 = vi.fn()
  118. const App = {
  119. setup() {
  120. return () =>
  121. h(Child, {
  122. onFoo: [fn1, fn2]
  123. })
  124. }
  125. }
  126. render(h(App), nodeOps.createElement('div'))
  127. expect(fn1).toHaveBeenCalledTimes(1)
  128. expect(fn1).toHaveBeenCalledWith(1)
  129. expect(fn2).toHaveBeenCalledTimes(1)
  130. expect(fn2).toHaveBeenCalledWith(1)
  131. })
  132. test('warning for undeclared event (array)', () => {
  133. const Foo = defineComponent({
  134. emits: ['foo'],
  135. render() {},
  136. created() {
  137. // @ts-expect-error
  138. this.$emit('bar')
  139. }
  140. })
  141. render(h(Foo), nodeOps.createElement('div'))
  142. expect(
  143. `Component emitted event "bar" but it is neither declared`
  144. ).toHaveBeenWarned()
  145. })
  146. test('warning for undeclared event (object)', () => {
  147. const Foo = defineComponent({
  148. emits: {
  149. foo: null
  150. },
  151. render() {},
  152. created() {
  153. // @ts-expect-error
  154. this.$emit('bar')
  155. }
  156. })
  157. render(h(Foo), nodeOps.createElement('div'))
  158. expect(
  159. `Component emitted event "bar" but it is neither declared`
  160. ).toHaveBeenWarned()
  161. })
  162. test('should not warn if has equivalent onXXX prop', () => {
  163. const Foo = defineComponent({
  164. props: ['onFoo'],
  165. emits: [],
  166. render() {},
  167. created() {
  168. // @ts-expect-error
  169. this.$emit('foo')
  170. }
  171. })
  172. render(h(Foo), nodeOps.createElement('div'))
  173. expect(
  174. `Component emitted event "foo" but it is neither declared`
  175. ).not.toHaveBeenWarned()
  176. })
  177. test('validator warning', () => {
  178. const Foo = defineComponent({
  179. emits: {
  180. foo: (arg: number) => arg > 0
  181. },
  182. render() {},
  183. created() {
  184. this.$emit('foo', -1)
  185. }
  186. })
  187. render(h(Foo), nodeOps.createElement('div'))
  188. expect(`event validation failed for event "foo"`).toHaveBeenWarned()
  189. })
  190. test('merging from mixins', () => {
  191. const mixin = {
  192. emits: {
  193. foo: (arg: number) => arg > 0
  194. }
  195. }
  196. const Foo = defineComponent({
  197. mixins: [mixin],
  198. render() {},
  199. created() {
  200. this.$emit('foo', -1)
  201. }
  202. })
  203. render(h(Foo), nodeOps.createElement('div'))
  204. expect(`event validation failed for event "foo"`).toHaveBeenWarned()
  205. })
  206. // #2651
  207. test('should not attach normalized object when mixins do not contain emits', () => {
  208. const Foo = defineComponent({
  209. mixins: [{}],
  210. render() {},
  211. created() {
  212. this.$emit('foo')
  213. }
  214. })
  215. render(h(Foo), nodeOps.createElement('div'))
  216. expect(
  217. `Component emitted event "foo" but it is neither declared`
  218. ).not.toHaveBeenWarned()
  219. })
  220. test('.once', () => {
  221. const Foo = defineComponent({
  222. render() {},
  223. emits: {
  224. foo: null,
  225. bar: null
  226. },
  227. created() {
  228. this.$emit('foo')
  229. this.$emit('foo')
  230. this.$emit('bar')
  231. this.$emit('bar')
  232. }
  233. })
  234. const fn = vi.fn()
  235. const barFn = vi.fn()
  236. render(
  237. h(Foo, {
  238. onFooOnce: fn,
  239. onBarOnce: barFn
  240. }),
  241. nodeOps.createElement('div')
  242. )
  243. expect(fn).toHaveBeenCalledTimes(1)
  244. expect(barFn).toHaveBeenCalledTimes(1)
  245. })
  246. test('.once with normal listener of the same name', () => {
  247. const Foo = defineComponent({
  248. render() {},
  249. emits: {
  250. foo: null
  251. },
  252. created() {
  253. this.$emit('foo')
  254. this.$emit('foo')
  255. }
  256. })
  257. const onFoo = vi.fn()
  258. const onFooOnce = vi.fn()
  259. render(
  260. h(Foo, {
  261. onFoo,
  262. onFooOnce
  263. }),
  264. nodeOps.createElement('div')
  265. )
  266. expect(onFoo).toHaveBeenCalledTimes(2)
  267. expect(onFooOnce).toHaveBeenCalledTimes(1)
  268. })
  269. test('.number modifier should work with v-model on component', () => {
  270. const Foo = defineComponent({
  271. render() {},
  272. created() {
  273. this.$emit('update:modelValue', '1')
  274. this.$emit('update:foo', '2')
  275. }
  276. })
  277. const fn1 = vi.fn()
  278. const fn2 = vi.fn()
  279. const Comp = () =>
  280. h(Foo, {
  281. modelValue: null,
  282. modelModifiers: { number: true },
  283. 'onUpdate:modelValue': fn1,
  284. foo: null,
  285. fooModifiers: { number: true },
  286. 'onUpdate:foo': fn2
  287. })
  288. render(h(Comp), nodeOps.createElement('div'))
  289. expect(fn1).toHaveBeenCalledTimes(1)
  290. expect(fn1).toHaveBeenCalledWith(1)
  291. expect(fn2).toHaveBeenCalledTimes(1)
  292. expect(fn2).toHaveBeenCalledWith(2)
  293. })
  294. test('.trim modifier should work with v-model on component', () => {
  295. const Foo = defineComponent({
  296. render() {},
  297. created() {
  298. this.$emit('update:modelValue', ' one ')
  299. this.$emit('update:foo', ' two ')
  300. }
  301. })
  302. const fn1 = vi.fn()
  303. const fn2 = vi.fn()
  304. const Comp = () =>
  305. h(Foo, {
  306. modelValue: null,
  307. modelModifiers: { trim: true },
  308. 'onUpdate:modelValue': fn1,
  309. foo: null,
  310. fooModifiers: { trim: true },
  311. 'onUpdate:foo': fn2
  312. })
  313. render(h(Comp), nodeOps.createElement('div'))
  314. expect(fn1).toHaveBeenCalledTimes(1)
  315. expect(fn1).toHaveBeenCalledWith('one')
  316. expect(fn2).toHaveBeenCalledTimes(1)
  317. expect(fn2).toHaveBeenCalledWith('two')
  318. })
  319. test('.trim and .number modifiers should work with v-model on component', () => {
  320. const Foo = defineComponent({
  321. render() {},
  322. created() {
  323. this.$emit('update:modelValue', ' +01.2 ')
  324. this.$emit('update:foo', ' 1 ')
  325. }
  326. })
  327. const fn1 = vi.fn()
  328. const fn2 = vi.fn()
  329. const Comp = () =>
  330. h(Foo, {
  331. modelValue: null,
  332. modelModifiers: { trim: true, number: true },
  333. 'onUpdate:modelValue': fn1,
  334. foo: null,
  335. fooModifiers: { trim: true, number: true },
  336. 'onUpdate:foo': fn2
  337. })
  338. render(h(Comp), nodeOps.createElement('div'))
  339. expect(fn1).toHaveBeenCalledTimes(1)
  340. expect(fn1).toHaveBeenCalledWith(1.2)
  341. expect(fn2).toHaveBeenCalledTimes(1)
  342. expect(fn2).toHaveBeenCalledWith(1)
  343. })
  344. test('only trim string parameter when work with v-model on component', () => {
  345. const Foo = defineComponent({
  346. render() {},
  347. created() {
  348. this.$emit('update:modelValue', ' foo ', { bar: ' bar ' })
  349. }
  350. })
  351. const fn = vi.fn()
  352. const Comp = () =>
  353. h(Foo, {
  354. modelValue: null,
  355. modelModifiers: { trim: true },
  356. 'onUpdate:modelValue': fn
  357. })
  358. render(h(Comp), nodeOps.createElement('div'))
  359. expect(fn).toHaveBeenCalledTimes(1)
  360. expect(fn).toHaveBeenCalledWith('foo', { bar: ' bar ' })
  361. })
  362. test('isEmitListener', () => {
  363. const options = {
  364. click: null,
  365. 'test-event': null,
  366. fooBar: null,
  367. FooBaz: null
  368. }
  369. expect(isEmitListener(options, 'onClick')).toBe(true)
  370. expect(isEmitListener(options, 'onclick')).toBe(false)
  371. expect(isEmitListener(options, 'onBlick')).toBe(false)
  372. // .once listeners
  373. expect(isEmitListener(options, 'onClickOnce')).toBe(true)
  374. expect(isEmitListener(options, 'onclickOnce')).toBe(false)
  375. // kebab-case option
  376. expect(isEmitListener(options, 'onTestEvent')).toBe(true)
  377. // camelCase option
  378. expect(isEmitListener(options, 'onFooBar')).toBe(true)
  379. // PascalCase option
  380. expect(isEmitListener(options, 'onFooBaz')).toBe(true)
  381. })
  382. test('does not emit after unmount', async () => {
  383. const fn = vi.fn()
  384. const Foo = defineComponent({
  385. emits: ['closing'],
  386. async beforeUnmount() {
  387. await this.$nextTick()
  388. this.$emit('closing', true)
  389. },
  390. render() {
  391. return h('div')
  392. }
  393. })
  394. const Comp = () =>
  395. h(Foo, {
  396. onClosing: fn
  397. })
  398. const el = nodeOps.createElement('div')
  399. render(h(Comp), el)
  400. await nextTick()
  401. render(null, el)
  402. await nextTick()
  403. expect(fn).not.toHaveBeenCalled()
  404. })
  405. test('merge string array emits', async () => {
  406. const ComponentA = defineComponent({
  407. emits: ['one', 'two']
  408. })
  409. const ComponentB = defineComponent({
  410. emits: ['three']
  411. })
  412. const renderFn = vi.fn(function (this: ComponentPublicInstance) {
  413. expect(this.$options.emits).toEqual(['one', 'two', 'three'])
  414. return h('div')
  415. })
  416. const ComponentC = defineComponent({
  417. render: renderFn,
  418. mixins: [ComponentA, ComponentB]
  419. })
  420. const el = nodeOps.createElement('div')
  421. expect(renderFn).toHaveBeenCalledTimes(0)
  422. render(h(ComponentC), el)
  423. expect(renderFn).toHaveBeenCalledTimes(1)
  424. })
  425. test('merge object emits', async () => {
  426. const twoFn = vi.fn((v: unknown) => !v)
  427. const ComponentA = defineComponent({
  428. emits: {
  429. one: null,
  430. two: twoFn
  431. }
  432. })
  433. const ComponentB = defineComponent({
  434. emits: ['three']
  435. })
  436. const renderFn = vi.fn(function (this: ComponentPublicInstance) {
  437. expect(this.$options.emits).toEqual({
  438. one: null,
  439. two: twoFn,
  440. three: null
  441. })
  442. expect(this.$options.emits.two).toBe(twoFn)
  443. return h('div')
  444. })
  445. const ComponentC = defineComponent({
  446. render: renderFn,
  447. mixins: [ComponentA, ComponentB]
  448. })
  449. const el = nodeOps.createElement('div')
  450. expect(renderFn).toHaveBeenCalledTimes(0)
  451. render(h(ComponentC), el)
  452. expect(renderFn).toHaveBeenCalledTimes(1)
  453. })
  454. })