componentEmits.spec.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  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('validator warning', () => {
  173. define({
  174. emits: {
  175. foo: (arg: number) => arg > 0,
  176. },
  177. setup(_, { emit }) {
  178. emit('foo', -1)
  179. return []
  180. },
  181. }).render()
  182. expect(`event validation failed for event "foo"`).toHaveBeenWarned()
  183. })
  184. test('.once', () => {
  185. const { render } = define({
  186. emits: {
  187. foo: null,
  188. bar: null,
  189. },
  190. setup(_, { emit }) {
  191. emit('foo')
  192. emit('foo')
  193. emit('bar')
  194. emit('bar')
  195. return []
  196. },
  197. })
  198. const fn = vi.fn()
  199. const barFn = vi.fn()
  200. render({
  201. onFooOnce: () => fn,
  202. onBarOnce: () => barFn,
  203. })
  204. expect(fn).toHaveBeenCalledTimes(1)
  205. expect(barFn).toHaveBeenCalledTimes(1)
  206. })
  207. test('.once with normal listener of the same name', () => {
  208. const { render } = define({
  209. emits: {
  210. foo: null,
  211. },
  212. setup(_, { emit }) {
  213. emit('foo')
  214. emit('foo')
  215. return []
  216. },
  217. })
  218. const onFoo = vi.fn()
  219. const onFooOnce = vi.fn()
  220. render({
  221. onFoo: () => onFoo,
  222. onFooOnce: () => onFooOnce,
  223. })
  224. expect(onFoo).toHaveBeenCalledTimes(2)
  225. expect(onFooOnce).toHaveBeenCalledTimes(1)
  226. })
  227. test('.number modifier should work with v-model on component', () => {
  228. const { render } = define({
  229. setup(_, { emit }) {
  230. emit('update:modelValue', '1')
  231. emit('update:foo', '2')
  232. return []
  233. },
  234. })
  235. const fn1 = vi.fn()
  236. const fn2 = vi.fn()
  237. render({
  238. modelValue: () => null,
  239. modelModifiers: () => ({ number: true }),
  240. ['onUpdate:modelValue']: () => fn1,
  241. foo: () => null,
  242. fooModifiers: () => ({ number: true }),
  243. ['onUpdate:foo']: () => fn2,
  244. })
  245. expect(fn1).toHaveBeenCalledTimes(1)
  246. expect(fn1).toHaveBeenCalledWith(1)
  247. expect(fn2).toHaveBeenCalledTimes(1)
  248. expect(fn2).toHaveBeenCalledWith(2)
  249. })
  250. test('.trim modifier should work with v-model on component', () => {
  251. const { render } = define({
  252. setup(_, { emit }) {
  253. emit('update:modelValue', ' one ')
  254. emit('update:foo', ' two ')
  255. return []
  256. },
  257. })
  258. const fn1 = vi.fn()
  259. const fn2 = vi.fn()
  260. render({
  261. modelValue() {
  262. return null
  263. },
  264. modelModifiers() {
  265. return { trim: true }
  266. },
  267. ['onUpdate:modelValue']() {
  268. return fn1
  269. },
  270. foo() {
  271. return null
  272. },
  273. fooModifiers() {
  274. return { trim: true }
  275. },
  276. 'onUpdate:foo'() {
  277. return fn2
  278. },
  279. })
  280. expect(fn1).toHaveBeenCalledTimes(1)
  281. expect(fn1).toHaveBeenCalledWith('one')
  282. expect(fn2).toHaveBeenCalledTimes(1)
  283. expect(fn2).toHaveBeenCalledWith('two')
  284. })
  285. test('.trim and .number modifiers should work with v-model on component', () => {
  286. const { render } = define({
  287. setup(_, { emit }) {
  288. emit('update:modelValue', ' +01.2 ')
  289. emit('update:foo', ' 1 ')
  290. return []
  291. },
  292. })
  293. const fn1 = vi.fn()
  294. const fn2 = vi.fn()
  295. render({
  296. modelValue() {
  297. return null
  298. },
  299. modelModifiers() {
  300. return { trim: true, number: true }
  301. },
  302. ['onUpdate:modelValue']() {
  303. return fn1
  304. },
  305. foo() {
  306. return null
  307. },
  308. fooModifiers() {
  309. return { trim: true, number: true }
  310. },
  311. ['onUpdate:foo']() {
  312. return fn2
  313. },
  314. })
  315. expect(fn1).toHaveBeenCalledTimes(1)
  316. expect(fn1).toHaveBeenCalledWith(1.2)
  317. expect(fn2).toHaveBeenCalledTimes(1)
  318. expect(fn2).toHaveBeenCalledWith(1)
  319. })
  320. test('only trim string parameter when work with v-model on component', () => {
  321. const { render } = define({
  322. setup(_, { emit }) {
  323. emit('update:modelValue', ' foo ', { bar: ' bar ' })
  324. return []
  325. },
  326. })
  327. const fn = vi.fn()
  328. render({
  329. modelValue() {
  330. return null
  331. },
  332. modelModifiers() {
  333. return { trim: true }
  334. },
  335. ['onUpdate:modelValue']() {
  336. return fn
  337. },
  338. })
  339. expect(fn).toHaveBeenCalledTimes(1)
  340. expect(fn).toHaveBeenCalledWith('foo', { bar: ' bar ' })
  341. })
  342. test('isEmitListener', () => {
  343. const options = {
  344. get click() {
  345. return null
  346. },
  347. get 'test-event'() {
  348. return null
  349. },
  350. get fooBar() {
  351. return null
  352. },
  353. get FooBaz() {
  354. return null
  355. },
  356. }
  357. expect(isEmitListener(options, 'onClick')).toBe(true)
  358. expect(isEmitListener(options, 'onclick')).toBe(false)
  359. expect(isEmitListener(options, 'onBlick')).toBe(false)
  360. // .once listeners
  361. expect(isEmitListener(options, 'onClickOnce')).toBe(true)
  362. expect(isEmitListener(options, 'onclickOnce')).toBe(false)
  363. // kebab-case option
  364. expect(isEmitListener(options, 'onTestEvent')).toBe(true)
  365. // camelCase option
  366. expect(isEmitListener(options, 'onFooBar')).toBe(true)
  367. // PascalCase option
  368. expect(isEmitListener(options, 'onFooBaz')).toBe(true)
  369. })
  370. test('does not emit after unmount', async () => {
  371. const fn = vi.fn()
  372. const Foo = defineVaporComponent({
  373. emits: ['closing'],
  374. setup(_, { emit }) {
  375. onBeforeUnmount(async () => {
  376. await nextTick()
  377. emit('closing', true)
  378. })
  379. return []
  380. },
  381. })
  382. const { app } = define(() =>
  383. createComponent(Foo, { onClosing: () => fn }),
  384. ).render()
  385. await nextTick()
  386. app.unmount()
  387. await nextTick()
  388. expect(fn).not.toHaveBeenCalled()
  389. })
  390. })