componentEmits.spec.ts 9.6 KB

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