componentSlots.spec.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import {
  2. createApp,
  3. getCurrentInstance,
  4. h,
  5. nextTick,
  6. nodeOps,
  7. ref,
  8. render,
  9. } from '@vue/runtime-test'
  10. import { normalizeVNode } from '../src/vnode'
  11. import { createSlots } from '../src/helpers/createSlots'
  12. describe('component: slots', () => {
  13. function renderWithSlots(slots: any): any {
  14. let instance: any
  15. const Comp = {
  16. render() {
  17. instance = getCurrentInstance()
  18. return h('div')
  19. },
  20. }
  21. render(h(Comp, null, slots), nodeOps.createElement('div'))
  22. return instance
  23. }
  24. test('initSlots: instance.slots should be set correctly', () => {
  25. const { slots } = renderWithSlots({ _: 1 })
  26. expect(slots).toMatchObject({ _: 1 })
  27. })
  28. test('initSlots: should normalize object slots (when value is null, string, array)', () => {
  29. const { slots } = renderWithSlots({
  30. _inner: '_inner',
  31. foo: null,
  32. header: 'header',
  33. footer: ['f1', 'f2'],
  34. })
  35. expect(
  36. '[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.',
  37. ).toHaveBeenWarned()
  38. expect(
  39. '[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.',
  40. ).toHaveBeenWarned()
  41. expect(slots).not.toHaveProperty('_inner')
  42. expect(slots).not.toHaveProperty('foo')
  43. expect(slots.header()).toMatchObject([normalizeVNode('header')])
  44. expect(slots.footer()).toMatchObject([
  45. normalizeVNode('f1'),
  46. normalizeVNode('f2'),
  47. ])
  48. })
  49. test('initSlots: should normalize object slots (when value is function)', () => {
  50. let proxy: any
  51. const Comp = {
  52. render() {
  53. proxy = getCurrentInstance()
  54. return h('div')
  55. },
  56. }
  57. render(
  58. h(Comp, null, {
  59. header: () => 'header',
  60. }),
  61. nodeOps.createElement('div'),
  62. )
  63. expect(proxy.slots.header()).toMatchObject([normalizeVNode('header')])
  64. })
  65. test('initSlots: instance.slots should be set correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', () => {
  66. const { slots } = renderWithSlots([h('span')])
  67. expect(
  68. '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
  69. ).toHaveBeenWarned()
  70. expect(slots.default()).toMatchObject([normalizeVNode(h('span'))])
  71. })
  72. test('updateSlots: instance.slots should be updated correctly (when slotType is number)', async () => {
  73. const flag1 = ref(true)
  74. let instance: any
  75. const Child = () => {
  76. instance = getCurrentInstance()
  77. return 'child'
  78. }
  79. const Comp = {
  80. setup() {
  81. return () => [
  82. h(
  83. Child,
  84. null,
  85. createSlots({ _: 2 as any }, [
  86. flag1.value
  87. ? {
  88. name: 'one',
  89. fn: () => [h('span')],
  90. }
  91. : {
  92. name: 'two',
  93. fn: () => [h('div')],
  94. },
  95. ]),
  96. ),
  97. ]
  98. },
  99. }
  100. render(h(Comp), nodeOps.createElement('div'))
  101. expect(instance.slots).toHaveProperty('one')
  102. expect(instance.slots).not.toHaveProperty('two')
  103. flag1.value = false
  104. await nextTick()
  105. expect(instance.slots).not.toHaveProperty('one')
  106. expect(instance.slots).toHaveProperty('two')
  107. })
  108. test('updateSlots: instance.slots should be updated correctly (when slotType is null)', async () => {
  109. const flag1 = ref(true)
  110. let instance: any
  111. const Child = () => {
  112. instance = getCurrentInstance()
  113. return 'child'
  114. }
  115. const oldSlots = {
  116. header: 'header',
  117. footer: undefined,
  118. }
  119. const newSlots = {
  120. header: undefined,
  121. footer: 'footer',
  122. }
  123. const Comp = {
  124. setup() {
  125. return () => [
  126. h(Child, { n: flag1.value }, flag1.value ? oldSlots : newSlots),
  127. ]
  128. },
  129. }
  130. render(h(Comp), nodeOps.createElement('div'))
  131. expect(instance.slots).toHaveProperty('header')
  132. expect(instance.slots).not.toHaveProperty('footer')
  133. flag1.value = false
  134. await nextTick()
  135. expect(
  136. '[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.',
  137. ).toHaveBeenWarned()
  138. expect(
  139. '[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.',
  140. ).toHaveBeenWarned()
  141. expect(instance.slots).not.toHaveProperty('header')
  142. expect(instance.slots.footer()).toMatchObject([normalizeVNode('footer')])
  143. })
  144. test('updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', async () => {
  145. const flag1 = ref(true)
  146. let instance: any
  147. const Child = () => {
  148. instance = getCurrentInstance()
  149. return 'child'
  150. }
  151. const Comp = {
  152. setup() {
  153. return () => [
  154. h(Child, { n: flag1.value }, flag1.value ? ['header'] : ['footer']),
  155. ]
  156. },
  157. }
  158. render(h(Comp), nodeOps.createElement('div'))
  159. expect(instance.slots.default()).toMatchObject([normalizeVNode('header')])
  160. flag1.value = false
  161. await nextTick()
  162. expect(
  163. '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
  164. ).toHaveBeenWarned()
  165. expect(instance.slots.default()).toMatchObject([normalizeVNode('footer')])
  166. })
  167. test('should respect $stable flag', async () => {
  168. const flag1 = ref(1)
  169. const flag2 = ref(2)
  170. const spy = vi.fn()
  171. const Child = () => {
  172. spy()
  173. return 'child'
  174. }
  175. const App = {
  176. setup() {
  177. return () => [
  178. flag1.value,
  179. h(
  180. Child,
  181. { n: flag2.value },
  182. {
  183. foo: () => 'foo',
  184. $stable: true,
  185. },
  186. ),
  187. ]
  188. },
  189. }
  190. render(h(App), nodeOps.createElement('div'))
  191. expect(spy).toHaveBeenCalledTimes(1)
  192. // parent re-render, props didn't change, slots are stable
  193. // -> child should not update
  194. flag1.value++
  195. await nextTick()
  196. expect(spy).toHaveBeenCalledTimes(1)
  197. // parent re-render, props changed
  198. // -> child should update
  199. flag2.value++
  200. await nextTick()
  201. expect(spy).toHaveBeenCalledTimes(2)
  202. })
  203. test('should not warn when mounting another app in setup', () => {
  204. const Comp = {
  205. setup(_: any, { slots }: any) {
  206. return () => slots.default?.()
  207. },
  208. }
  209. const mountComp = () => {
  210. createApp({
  211. setup() {
  212. return () => h(Comp, () => 'msg')
  213. },
  214. }).mount(nodeOps.createElement('div'))
  215. }
  216. const App = {
  217. setup() {
  218. mountComp()
  219. return () => null
  220. },
  221. }
  222. createApp(App).mount(nodeOps.createElement('div'))
  223. expect(
  224. 'Slot "default" invoked outside of the render function',
  225. ).not.toHaveBeenWarned()
  226. })
  227. })