componentEmits.spec.ts 11 KB

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