componentEmits.spec.ts 11 KB

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