componentEmits.spec.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. // Note: emits and listener fallthrough is tested in
  2. // ./rendererAttrsFallthrough.spec.ts.
  3. import {
  4. render,
  5. defineComponent,
  6. h,
  7. nodeOps,
  8. toHandlers
  9. } from '@vue/runtime-test'
  10. import { isEmitListener } from '../src/componentEmits'
  11. describe('component: emit', () => {
  12. test('trigger handlers', () => {
  13. const Foo = defineComponent({
  14. render() {},
  15. created() {
  16. // the `emit` function is bound on component instances
  17. this.$emit('foo')
  18. this.$emit('bar')
  19. this.$emit('!baz')
  20. }
  21. })
  22. const onfoo = jest.fn()
  23. const onBar = jest.fn()
  24. const onBaz = jest.fn()
  25. const Comp = () => h(Foo, { onfoo, onBar, ['on!baz']: onBaz })
  26. render(h(Comp), nodeOps.createElement('div'))
  27. expect(onfoo).not.toHaveBeenCalled()
  28. // only capitalized or special chars are considered event listeners
  29. expect(onBar).toHaveBeenCalled()
  30. expect(onBaz).toHaveBeenCalled()
  31. })
  32. test('trigger camelCase handler', () => {
  33. const Foo = defineComponent({
  34. render() {},
  35. created() {
  36. this.$emit('test-event')
  37. }
  38. })
  39. const fooSpy = jest.fn()
  40. const Comp = () =>
  41. h(Foo, {
  42. onTestEvent: fooSpy
  43. })
  44. render(h(Comp), nodeOps.createElement('div'))
  45. expect(fooSpy).toHaveBeenCalledTimes(1)
  46. })
  47. test('trigger kebab-case handler', () => {
  48. const Foo = defineComponent({
  49. render() {},
  50. created() {
  51. this.$emit('test-event')
  52. }
  53. })
  54. const fooSpy = jest.fn()
  55. const Comp = () =>
  56. h(Foo, {
  57. 'onTest-event': fooSpy
  58. })
  59. render(h(Comp), nodeOps.createElement('div'))
  60. expect(fooSpy).toHaveBeenCalledTimes(1)
  61. })
  62. // #3527
  63. test('trigger mixed case handlers', () => {
  64. const Foo = defineComponent({
  65. render() {},
  66. created() {
  67. this.$emit('test-event')
  68. this.$emit('testEvent')
  69. }
  70. })
  71. const fooSpy = jest.fn()
  72. const barSpy = jest.fn()
  73. const Comp = () =>
  74. // simulate v-on="obj" usage
  75. h(
  76. Foo,
  77. toHandlers({
  78. 'test-event': fooSpy,
  79. testEvent: barSpy
  80. })
  81. )
  82. render(h(Comp), nodeOps.createElement('div'))
  83. expect(fooSpy).toHaveBeenCalledTimes(1)
  84. expect(fooSpy).toHaveBeenCalledTimes(1)
  85. })
  86. // for v-model:foo-bar usage in DOM templates
  87. test('trigger hyphenated events for update:xxx events', () => {
  88. const Foo = defineComponent({
  89. render() {},
  90. created() {
  91. this.$emit('update:fooProp')
  92. this.$emit('update:barProp')
  93. }
  94. })
  95. const fooSpy = jest.fn()
  96. const barSpy = jest.fn()
  97. const Comp = () =>
  98. h(Foo, {
  99. 'onUpdate:fooProp': fooSpy,
  100. 'onUpdate:bar-prop': barSpy
  101. })
  102. render(h(Comp), nodeOps.createElement('div'))
  103. expect(fooSpy).toHaveBeenCalled()
  104. expect(barSpy).toHaveBeenCalled()
  105. })
  106. test('should trigger array of listeners', async () => {
  107. const Child = defineComponent({
  108. setup(_, { emit }) {
  109. emit('foo', 1)
  110. return () => h('div')
  111. }
  112. })
  113. const fn1 = jest.fn()
  114. const fn2 = jest.fn()
  115. const App = {
  116. setup() {
  117. return () =>
  118. h(Child, {
  119. onFoo: [fn1, fn2]
  120. })
  121. }
  122. }
  123. render(h(App), nodeOps.createElement('div'))
  124. expect(fn1).toHaveBeenCalledTimes(1)
  125. expect(fn1).toHaveBeenCalledWith(1)
  126. expect(fn2).toHaveBeenCalledTimes(1)
  127. expect(fn1).toHaveBeenCalledWith(1)
  128. })
  129. test('warning for undeclared event (array)', () => {
  130. const Foo = defineComponent({
  131. emits: ['foo'],
  132. render() {},
  133. created() {
  134. // @ts-ignore
  135. this.$emit('bar')
  136. }
  137. })
  138. render(h(Foo), nodeOps.createElement('div'))
  139. expect(
  140. `Component emitted event "bar" but it is neither declared`
  141. ).toHaveBeenWarned()
  142. })
  143. test('warning for undeclared event (object)', () => {
  144. const Foo = defineComponent({
  145. emits: {
  146. foo: null
  147. },
  148. render() {},
  149. created() {
  150. // @ts-ignore
  151. this.$emit('bar')
  152. }
  153. })
  154. render(h(Foo), nodeOps.createElement('div'))
  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. const Foo = defineComponent({
  161. props: ['onFoo'],
  162. emits: [],
  163. render() {},
  164. created() {
  165. // @ts-ignore
  166. this.$emit('foo')
  167. }
  168. })
  169. render(h(Foo), nodeOps.createElement('div'))
  170. expect(
  171. `Component emitted event "foo" but it is neither declared`
  172. ).not.toHaveBeenWarned()
  173. })
  174. test('validator warning', () => {
  175. const Foo = defineComponent({
  176. emits: {
  177. foo: (arg: number) => arg > 0
  178. },
  179. render() {},
  180. created() {
  181. this.$emit('foo', -1)
  182. }
  183. })
  184. render(h(Foo), nodeOps.createElement('div'))
  185. expect(`event validation failed for event "foo"`).toHaveBeenWarned()
  186. })
  187. test('merging from mixins', () => {
  188. const mixin = {
  189. emits: {
  190. foo: (arg: number) => arg > 0
  191. }
  192. }
  193. const Foo = defineComponent({
  194. mixins: [mixin],
  195. render() {},
  196. created() {
  197. this.$emit('foo', -1)
  198. }
  199. })
  200. render(h(Foo), nodeOps.createElement('div'))
  201. expect(`event validation failed for event "foo"`).toHaveBeenWarned()
  202. })
  203. // #2651
  204. test('should not attach normalized object when mixins do not contain emits', () => {
  205. const Foo = defineComponent({
  206. mixins: [{}],
  207. render() {},
  208. created() {
  209. this.$emit('foo')
  210. }
  211. })
  212. render(h(Foo), nodeOps.createElement('div'))
  213. expect(
  214. `Component emitted event "foo" but it is neither declared`
  215. ).not.toHaveBeenWarned()
  216. })
  217. test('.once', () => {
  218. const Foo = defineComponent({
  219. render() {},
  220. emits: {
  221. foo: null
  222. },
  223. created() {
  224. this.$emit('foo')
  225. this.$emit('foo')
  226. }
  227. })
  228. const fn = jest.fn()
  229. render(
  230. h(Foo, {
  231. onFooOnce: fn
  232. }),
  233. nodeOps.createElement('div')
  234. )
  235. expect(fn).toHaveBeenCalledTimes(1)
  236. })
  237. test('.once with normal listener of the same name', () => {
  238. const Foo = defineComponent({
  239. render() {},
  240. emits: {
  241. foo: null
  242. },
  243. created() {
  244. this.$emit('foo')
  245. this.$emit('foo')
  246. }
  247. })
  248. const onFoo = jest.fn()
  249. const onFooOnce = jest.fn()
  250. render(
  251. h(Foo, {
  252. onFoo,
  253. onFooOnce
  254. }),
  255. nodeOps.createElement('div')
  256. )
  257. expect(onFoo).toHaveBeenCalledTimes(2)
  258. expect(onFooOnce).toHaveBeenCalledTimes(1)
  259. })
  260. test('.number modifier should work with v-model on component', () => {
  261. const Foo = defineComponent({
  262. render() {},
  263. created() {
  264. this.$emit('update:modelValue', '1')
  265. this.$emit('update:foo', '2')
  266. }
  267. })
  268. const fn1 = jest.fn()
  269. const fn2 = jest.fn()
  270. const Comp = () =>
  271. h(Foo, {
  272. modelValue: null,
  273. modelModifiers: { number: true },
  274. 'onUpdate:modelValue': fn1,
  275. foo: null,
  276. fooModifiers: { number: true },
  277. 'onUpdate:foo': fn2
  278. })
  279. render(h(Comp), nodeOps.createElement('div'))
  280. expect(fn1).toHaveBeenCalledTimes(1)
  281. expect(fn1).toHaveBeenCalledWith(1)
  282. expect(fn2).toHaveBeenCalledTimes(1)
  283. expect(fn2).toHaveBeenCalledWith(2)
  284. })
  285. test('.trim modifier should work with v-model on component', () => {
  286. const Foo = defineComponent({
  287. render() {},
  288. created() {
  289. this.$emit('update:modelValue', ' one ')
  290. this.$emit('update:foo', ' two ')
  291. }
  292. })
  293. const fn1 = jest.fn()
  294. const fn2 = jest.fn()
  295. const Comp = () =>
  296. h(Foo, {
  297. modelValue: null,
  298. modelModifiers: { trim: true },
  299. 'onUpdate:modelValue': fn1,
  300. foo: null,
  301. fooModifiers: { trim: true },
  302. 'onUpdate:foo': fn2
  303. })
  304. render(h(Comp), nodeOps.createElement('div'))
  305. expect(fn1).toHaveBeenCalledTimes(1)
  306. expect(fn1).toHaveBeenCalledWith('one')
  307. expect(fn2).toHaveBeenCalledTimes(1)
  308. expect(fn2).toHaveBeenCalledWith('two')
  309. })
  310. test('isEmitListener', () => {
  311. const options = {
  312. click: null,
  313. 'test-event': null,
  314. fooBar: null,
  315. FooBaz: null
  316. }
  317. expect(isEmitListener(options, 'onClick')).toBe(true)
  318. expect(isEmitListener(options, 'onclick')).toBe(false)
  319. expect(isEmitListener(options, 'onBlick')).toBe(false)
  320. // .once listeners
  321. expect(isEmitListener(options, 'onClickOnce')).toBe(true)
  322. expect(isEmitListener(options, 'onclickOnce')).toBe(false)
  323. // kebab-case option
  324. expect(isEmitListener(options, 'onTestEvent')).toBe(true)
  325. // camelCase option
  326. expect(isEmitListener(options, 'onFooBar')).toBe(true)
  327. // PascalCase option
  328. expect(isEmitListener(options, 'onFooBaz')).toBe(true)
  329. })
  330. })