componentEmits.spec.ts 10 KB

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