componentEmits.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. // Note: emits and listener fallthrough is tested in
  2. // ./rendererAttrsFallthrough.spec.ts.
  3. import {
  4. type ComponentPublicInstance,
  5. defineComponent,
  6. h,
  7. nextTick,
  8. nodeOps,
  9. render,
  10. toHandlers,
  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. test('merge string array emits', async () => {
  405. const ComponentA = defineComponent({
  406. emits: ['one', 'two'],
  407. })
  408. const ComponentB = defineComponent({
  409. emits: ['three'],
  410. })
  411. const renderFn = vi.fn(function (this: ComponentPublicInstance) {
  412. expect(this.$options.emits).toEqual(['one', 'two', 'three'])
  413. return h('div')
  414. })
  415. const ComponentC = defineComponent({
  416. render: renderFn,
  417. mixins: [ComponentA, ComponentB],
  418. })
  419. const el = nodeOps.createElement('div')
  420. expect(renderFn).toHaveBeenCalledTimes(0)
  421. render(h(ComponentC), el)
  422. expect(renderFn).toHaveBeenCalledTimes(1)
  423. })
  424. test('merge object emits', async () => {
  425. const twoFn = vi.fn((v: unknown) => !v)
  426. const ComponentA = defineComponent({
  427. emits: {
  428. one: null,
  429. two: twoFn,
  430. },
  431. })
  432. const ComponentB = defineComponent({
  433. emits: ['three'],
  434. })
  435. const renderFn = vi.fn(function (this: ComponentPublicInstance) {
  436. expect(this.$options.emits).toEqual({
  437. one: null,
  438. two: twoFn,
  439. three: null,
  440. })
  441. expect(this.$options.emits.two).toBe(twoFn)
  442. return h('div')
  443. })
  444. const ComponentC = defineComponent({
  445. render: renderFn,
  446. mixins: [ComponentA, ComponentB],
  447. })
  448. const el = nodeOps.createElement('div')
  449. expect(renderFn).toHaveBeenCalledTimes(0)
  450. render(h(ComponentC), el)
  451. expect(renderFn).toHaveBeenCalledTimes(1)
  452. })
  453. })