componentEmits.spec.ts 7.7 KB

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