componentEmits.spec.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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. // #3527
  79. test('trigger mixed case handlers', () => {
  80. const { render } = define({
  81. setup(_, { emit }) {
  82. emit('test-event')
  83. emit('testEvent')
  84. return []
  85. },
  86. })
  87. const fooSpy = vi.fn()
  88. const barSpy = vi.fn()
  89. render(
  90. toHandlers({
  91. 'test-event': () => fooSpy,
  92. testEvent: () => barSpy,
  93. }),
  94. )
  95. expect(fooSpy).toHaveBeenCalledTimes(1)
  96. expect(barSpy).toHaveBeenCalledTimes(1)
  97. })
  98. // for v-model:foo-bar usage in DOM templates
  99. test('trigger hyphenated events for update:xxx events', () => {
  100. const { render } = define({
  101. setup(_, { emit }) {
  102. emit('update:fooProp')
  103. emit('update:barProp')
  104. return []
  105. },
  106. })
  107. const fooSpy = vi.fn()
  108. const barSpy = vi.fn()
  109. render({
  110. ['onUpdate:fooProp']: () => fooSpy,
  111. ['onUpdate:bar-prop']: () => barSpy,
  112. })
  113. expect(fooSpy).toHaveBeenCalled()
  114. expect(barSpy).toHaveBeenCalled()
  115. })
  116. test('should trigger array of listeners', async () => {
  117. const { render } = define({
  118. setup(_, { emit }) {
  119. emit('foo', 1)
  120. return []
  121. },
  122. })
  123. const fn1 = vi.fn()
  124. const fn2 = vi.fn()
  125. render({ onFoo: () => [fn1, fn2] })
  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 { render } = define({
  133. emits: ['foo'],
  134. setup(_, { emit }) {
  135. emit('bar')
  136. return []
  137. },
  138. })
  139. render()
  140. expect(
  141. `Component emitted event "bar" but it is neither declared`,
  142. ).toHaveBeenWarned()
  143. })
  144. test('warning for undeclared event (object)', () => {
  145. const { render } = define({
  146. emits: {
  147. foo: null,
  148. },
  149. setup(_, { emit }) {
  150. emit('bar')
  151. return []
  152. },
  153. })
  154. render()
  155. expect(
  156. `Component emitted event "bar" but it is neither declared`,
  157. ).toHaveBeenWarned()
  158. })
  159. test('should not warn if has equivalent onXXX prop', () => {
  160. define({
  161. props: ['onFoo'],
  162. emits: [],
  163. setup(_, { emit }) {
  164. emit('foo')
  165. return []
  166. },
  167. }).render()
  168. expect(
  169. `Component emitted event "foo" but it is neither declared`,
  170. ).not.toHaveBeenWarned()
  171. })
  172. test.todo('validator warning', () => {
  173. // TODO: warning validator
  174. })
  175. test('.once', () => {
  176. const { render } = define({
  177. emits: {
  178. foo: null,
  179. bar: null,
  180. },
  181. setup(_, { emit }) {
  182. emit('foo')
  183. emit('foo')
  184. emit('bar')
  185. emit('bar')
  186. return []
  187. },
  188. })
  189. const fn = vi.fn()
  190. const barFn = vi.fn()
  191. render({
  192. onFooOnce: () => fn,
  193. onBarOnce: () => barFn,
  194. })
  195. expect(fn).toHaveBeenCalledTimes(1)
  196. expect(barFn).toHaveBeenCalledTimes(1)
  197. })
  198. test('.once with normal listener of the same name', () => {
  199. const { render } = define({
  200. emits: {
  201. foo: null,
  202. },
  203. setup(_, { emit }) {
  204. emit('foo')
  205. emit('foo')
  206. return []
  207. },
  208. })
  209. const onFoo = vi.fn()
  210. const onFooOnce = vi.fn()
  211. render({
  212. onFoo: () => onFoo,
  213. onFooOnce: () => onFooOnce,
  214. })
  215. expect(onFoo).toHaveBeenCalledTimes(2)
  216. expect(onFooOnce).toHaveBeenCalledTimes(1)
  217. })
  218. test('.number modifier should work with v-model on component', () => {
  219. const { render } = define({
  220. setup(_, { emit }) {
  221. emit('update:modelValue', '1')
  222. emit('update:foo', '2')
  223. return []
  224. },
  225. })
  226. const fn1 = vi.fn()
  227. const fn2 = vi.fn()
  228. render({
  229. modelValue: () => null,
  230. modelModifiers: () => ({ number: true }),
  231. ['onUpdate:modelValue']: () => fn1,
  232. foo: () => null,
  233. fooModifiers: () => ({ number: true }),
  234. ['onUpdate:foo']: () => fn2,
  235. })
  236. expect(fn1).toHaveBeenCalledTimes(1)
  237. expect(fn1).toHaveBeenCalledWith(1)
  238. expect(fn2).toHaveBeenCalledTimes(1)
  239. expect(fn2).toHaveBeenCalledWith(2)
  240. })
  241. test('.trim modifier should work with v-model on component', () => {
  242. const { render } = define({
  243. setup(_, { emit }) {
  244. emit('update:modelValue', ' one ')
  245. emit('update:foo', ' two ')
  246. return []
  247. },
  248. })
  249. const fn1 = vi.fn()
  250. const fn2 = vi.fn()
  251. render({
  252. modelValue() {
  253. return null
  254. },
  255. modelModifiers() {
  256. return { trim: true }
  257. },
  258. ['onUpdate:modelValue']() {
  259. return fn1
  260. },
  261. foo() {
  262. return null
  263. },
  264. fooModifiers() {
  265. return { trim: true }
  266. },
  267. 'onUpdate:foo'() {
  268. return fn2
  269. },
  270. })
  271. expect(fn1).toHaveBeenCalledTimes(1)
  272. expect(fn1).toHaveBeenCalledWith('one')
  273. expect(fn2).toHaveBeenCalledTimes(1)
  274. expect(fn2).toHaveBeenCalledWith('two')
  275. })
  276. test('.trim and .number modifiers should work with v-model on component', () => {
  277. const { render } = define({
  278. setup(_, { emit }) {
  279. emit('update:modelValue', ' +01.2 ')
  280. emit('update:foo', ' 1 ')
  281. return []
  282. },
  283. })
  284. const fn1 = vi.fn()
  285. const fn2 = vi.fn()
  286. render({
  287. modelValue() {
  288. return null
  289. },
  290. modelModifiers() {
  291. return { trim: true, number: true }
  292. },
  293. ['onUpdate:modelValue']() {
  294. return fn1
  295. },
  296. foo() {
  297. return null
  298. },
  299. fooModifiers() {
  300. return { trim: true, number: true }
  301. },
  302. ['onUpdate:foo']() {
  303. return fn2
  304. },
  305. })
  306. expect(fn1).toHaveBeenCalledTimes(1)
  307. expect(fn1).toHaveBeenCalledWith(1.2)
  308. expect(fn2).toHaveBeenCalledTimes(1)
  309. expect(fn2).toHaveBeenCalledWith(1)
  310. })
  311. test('only trim string parameter when work with v-model on component', () => {
  312. const { render } = define({
  313. setup(_, { emit }) {
  314. emit('update:modelValue', ' foo ', { bar: ' bar ' })
  315. return []
  316. },
  317. })
  318. const fn = vi.fn()
  319. render({
  320. modelValue() {
  321. return null
  322. },
  323. modelModifiers() {
  324. return { trim: true }
  325. },
  326. ['onUpdate:modelValue']() {
  327. return fn
  328. },
  329. })
  330. expect(fn).toHaveBeenCalledTimes(1)
  331. expect(fn).toHaveBeenCalledWith('foo', { bar: ' bar ' })
  332. })
  333. test('isEmitListener', () => {
  334. const options = {
  335. get click() {
  336. return null
  337. },
  338. get 'test-event'() {
  339. return null
  340. },
  341. get fooBar() {
  342. return null
  343. },
  344. get FooBaz() {
  345. return null
  346. },
  347. }
  348. expect(isEmitListener(options, 'onClick')).toBe(true)
  349. expect(isEmitListener(options, 'onclick')).toBe(false)
  350. expect(isEmitListener(options, 'onBlick')).toBe(false)
  351. // .once listeners
  352. expect(isEmitListener(options, 'onClickOnce')).toBe(true)
  353. expect(isEmitListener(options, 'onclickOnce')).toBe(false)
  354. // kebab-case option
  355. expect(isEmitListener(options, 'onTestEvent')).toBe(true)
  356. // camelCase option
  357. expect(isEmitListener(options, 'onFooBar')).toBe(true)
  358. // PascalCase option
  359. expect(isEmitListener(options, 'onFooBaz')).toBe(true)
  360. })
  361. test('does not emit after unmount', async () => {
  362. const fn = vi.fn()
  363. const Foo = defineVaporComponent({
  364. emits: ['closing'],
  365. setup(_, { emit }) {
  366. onBeforeUnmount(async () => {
  367. await nextTick()
  368. emit('closing', true)
  369. })
  370. return []
  371. },
  372. })
  373. const { app } = define(() =>
  374. createComponent(Foo, { onClosing: () => fn }),
  375. ).render()
  376. await nextTick()
  377. app.unmount()
  378. await nextTick()
  379. expect(fn).not.toHaveBeenCalled()
  380. })
  381. // NOTE: not supported mixins
  382. // test.todo('merge string array emits', async () => {})
  383. // test.todo('merge object emits', async () => {})
  384. })