componentSlots.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. import {
  2. createApp,
  3. getCurrentInstance,
  4. h,
  5. nextTick,
  6. nodeOps,
  7. ref,
  8. render,
  9. useSlots,
  10. } from '@vue/runtime-test'
  11. import { createBlock, normalizeVNode } from '../src/vnode'
  12. import { createSlots } from '../src/helpers/createSlots'
  13. describe('component: slots', () => {
  14. function renderWithSlots(slots: any): any {
  15. let instance: any
  16. const Comp = {
  17. render() {
  18. instance = getCurrentInstance()
  19. return h('div')
  20. },
  21. }
  22. render(h(Comp, null, slots), nodeOps.createElement('div'))
  23. return instance
  24. }
  25. test('initSlots: instance.slots should be set correctly', () => {
  26. let instance: any
  27. const Comp = {
  28. render() {
  29. instance = getCurrentInstance()
  30. return h('div')
  31. },
  32. }
  33. const slots = { foo: () => {}, _: 1 }
  34. render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
  35. expect(instance.slots).toMatchObject(slots)
  36. })
  37. test('initSlots: instance.slots should remove compiler marker if parent is using manual render function', () => {
  38. const { slots } = renderWithSlots({ _: 1 })
  39. expect(slots).toMatchObject({})
  40. })
  41. test('initSlots: ensure compiler marker non-enumerable', () => {
  42. const Comp = {
  43. render() {
  44. const slots = useSlots()
  45. // Only user-defined slots should be enumerable
  46. expect(Object.keys(slots)).toEqual(['foo'])
  47. // Internal compiler markers must still exist but be non-enumerable
  48. expect(slots).toHaveProperty('_')
  49. expect(Object.getOwnPropertyDescriptor(slots, '_')!.enumerable).toBe(
  50. false,
  51. )
  52. expect(slots).toHaveProperty('__')
  53. expect(Object.getOwnPropertyDescriptor(slots, '__')!.enumerable).toBe(
  54. false,
  55. )
  56. return h('div')
  57. },
  58. }
  59. const slots = { foo: () => {}, _: 1, __: [1] }
  60. render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
  61. })
  62. test('initSlots: should normalize object slots (when value is null, string, array)', () => {
  63. const { slots } = renderWithSlots({
  64. _inner: '_inner',
  65. foo: null,
  66. header: 'header',
  67. footer: ['f1', 'f2'],
  68. })
  69. expect(
  70. '[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.',
  71. ).toHaveBeenWarned()
  72. expect(
  73. '[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.',
  74. ).toHaveBeenWarned()
  75. expect(slots).not.toHaveProperty('_inner')
  76. expect(slots).not.toHaveProperty('foo')
  77. expect(slots.header()).toMatchObject([normalizeVNode('header')])
  78. expect(slots.footer()).toMatchObject([
  79. normalizeVNode('f1'),
  80. normalizeVNode('f2'),
  81. ])
  82. })
  83. test('initSlots: should normalize object slots (when value is function)', () => {
  84. let proxy: any
  85. const Comp = {
  86. render() {
  87. proxy = getCurrentInstance()
  88. return h('div')
  89. },
  90. }
  91. render(
  92. h(Comp, null, {
  93. header: () => 'header',
  94. }),
  95. nodeOps.createElement('div'),
  96. )
  97. expect(proxy.slots.header()).toMatchObject([normalizeVNode('header')])
  98. })
  99. test('initSlots: instance.slots should be set correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', () => {
  100. const { slots } = renderWithSlots([h('span')])
  101. expect(
  102. '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
  103. ).toHaveBeenWarned()
  104. expect(slots.default()).toMatchObject([normalizeVNode(h('span'))])
  105. })
  106. test('updateSlots: instance.slots should be updated correctly (when slotType is number)', async () => {
  107. const flag1 = ref(true)
  108. let instance: any
  109. const Child = () => {
  110. instance = getCurrentInstance()
  111. return 'child'
  112. }
  113. const Comp = {
  114. setup() {
  115. return () => [
  116. h(
  117. Child,
  118. null,
  119. createSlots({ _: 2 as any }, [
  120. flag1.value
  121. ? {
  122. name: 'one',
  123. fn: () => [h('span')],
  124. }
  125. : {
  126. name: 'two',
  127. fn: () => [h('div')],
  128. },
  129. ]),
  130. ),
  131. ]
  132. },
  133. }
  134. render(h(Comp), nodeOps.createElement('div'))
  135. expect(instance.slots).toHaveProperty('one')
  136. expect(instance.slots).not.toHaveProperty('two')
  137. flag1.value = false
  138. await nextTick()
  139. expect(instance.slots).not.toHaveProperty('one')
  140. expect(instance.slots).toHaveProperty('two')
  141. })
  142. test('updateSlots: instance.slots should be updated correctly (when slotType is null)', async () => {
  143. const flag1 = ref(true)
  144. let instance: any
  145. const Child = () => {
  146. instance = getCurrentInstance()
  147. return 'child'
  148. }
  149. const oldSlots = {
  150. header: 'header',
  151. footer: undefined,
  152. }
  153. const newSlots = {
  154. header: undefined,
  155. footer: 'footer',
  156. }
  157. const Comp = {
  158. setup() {
  159. return () => [
  160. h(Child, { n: flag1.value }, flag1.value ? oldSlots : newSlots),
  161. ]
  162. },
  163. }
  164. render(h(Comp), nodeOps.createElement('div'))
  165. expect(instance.slots).toHaveProperty('header')
  166. expect(instance.slots).not.toHaveProperty('footer')
  167. flag1.value = false
  168. await nextTick()
  169. expect(
  170. '[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.',
  171. ).toHaveBeenWarned()
  172. expect(
  173. '[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.',
  174. ).toHaveBeenWarned()
  175. expect(instance.slots).not.toHaveProperty('header')
  176. expect(instance.slots.footer()).toMatchObject([normalizeVNode('footer')])
  177. })
  178. test('updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', async () => {
  179. const flag1 = ref(true)
  180. let instance: any
  181. const Child = () => {
  182. instance = getCurrentInstance()
  183. return 'child'
  184. }
  185. const Comp = {
  186. setup() {
  187. return () => [
  188. h(Child, { n: flag1.value }, flag1.value ? ['header'] : ['footer']),
  189. ]
  190. },
  191. }
  192. render(h(Comp), nodeOps.createElement('div'))
  193. expect(instance.slots.default()).toMatchObject([normalizeVNode('header')])
  194. flag1.value = false
  195. await nextTick()
  196. expect(
  197. '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
  198. ).toHaveBeenWarned()
  199. expect(instance.slots.default()).toMatchObject([normalizeVNode('footer')])
  200. })
  201. test('should respect $stable flag with a value of true', async () => {
  202. const flag1 = ref(1)
  203. const flag2 = ref(2)
  204. const spy = vi.fn()
  205. const Child = () => {
  206. spy()
  207. return 'child'
  208. }
  209. const App = {
  210. setup() {
  211. return () => [
  212. flag1.value,
  213. h(
  214. Child,
  215. { n: flag2.value },
  216. {
  217. foo: () => 'foo',
  218. $stable: true,
  219. },
  220. ),
  221. ]
  222. },
  223. }
  224. render(h(App), nodeOps.createElement('div'))
  225. expect(spy).toHaveBeenCalledTimes(1)
  226. // parent re-render, props didn't change, slots are stable
  227. // -> child should not update
  228. flag1.value++
  229. await nextTick()
  230. expect(spy).toHaveBeenCalledTimes(1)
  231. // parent re-render, props changed
  232. // -> child should update
  233. flag2.value++
  234. await nextTick()
  235. expect(spy).toHaveBeenCalledTimes(2)
  236. })
  237. test('should respect $stable flag with a value of false', async () => {
  238. const flag1 = ref(1)
  239. const flag2 = ref(2)
  240. const spy = vi.fn()
  241. const Child = () => {
  242. spy()
  243. return 'child'
  244. }
  245. const App = {
  246. setup() {
  247. return () => [
  248. flag1.value,
  249. h(
  250. Child,
  251. { n: flag2.value },
  252. {
  253. foo: () => 'foo',
  254. $stable: false,
  255. },
  256. ),
  257. ]
  258. },
  259. }
  260. render(h(App), nodeOps.createElement('div'))
  261. expect(spy).toHaveBeenCalledTimes(1)
  262. // parent re-render, props didn't change, slots are not stable
  263. // -> child should update
  264. flag1.value++
  265. await nextTick()
  266. expect(spy).toHaveBeenCalledTimes(2)
  267. // parent re-render, props changed
  268. // -> child should update
  269. flag2.value++
  270. await nextTick()
  271. expect(spy).toHaveBeenCalledTimes(3)
  272. })
  273. test('should not warn when mounting another app in setup', () => {
  274. const Comp = {
  275. setup(_: any, { slots }: any) {
  276. return () => slots.default?.()
  277. },
  278. }
  279. const mountComp = () => {
  280. createApp({
  281. setup() {
  282. return () => h(Comp, () => 'msg')
  283. },
  284. }).mount(nodeOps.createElement('div'))
  285. }
  286. const App = {
  287. setup() {
  288. mountComp()
  289. return () => null
  290. },
  291. }
  292. createApp(App).mount(nodeOps.createElement('div'))
  293. expect(
  294. 'Slot "default" invoked outside of the render function',
  295. ).not.toHaveBeenWarned()
  296. })
  297. test('basic warn', () => {
  298. const Comp = {
  299. setup(_: any, { slots }: any) {
  300. slots.default && slots.default()
  301. return () => null
  302. },
  303. }
  304. const App = {
  305. setup() {
  306. return () => h(Comp, () => h('div'))
  307. },
  308. }
  309. createApp(App).mount(nodeOps.createElement('div'))
  310. expect(
  311. 'Slot "default" invoked outside of the render function',
  312. ).toHaveBeenWarned()
  313. })
  314. test('basic warn when mounting another app in setup', () => {
  315. const Comp = {
  316. setup(_: any, { slots }: any) {
  317. slots.default?.()
  318. return () => null
  319. },
  320. }
  321. const mountComp = () => {
  322. createApp({
  323. setup() {
  324. return () => h(Comp, () => 'msg')
  325. },
  326. }).mount(nodeOps.createElement('div'))
  327. }
  328. const App = {
  329. setup() {
  330. mountComp()
  331. return () => null
  332. },
  333. }
  334. createApp(App).mount(nodeOps.createElement('div'))
  335. expect(
  336. 'Slot "default" invoked outside of the render function',
  337. ).toHaveBeenWarned()
  338. })
  339. test('should not warn when render in setup', () => {
  340. const container = {
  341. setup(_: any, { slots }: any) {
  342. return () => slots.default && slots.default()
  343. },
  344. }
  345. const comp = h(container, null, () => h('div'))
  346. const App = {
  347. setup() {
  348. render(h(comp), nodeOps.createElement('div'))
  349. return () => null
  350. },
  351. }
  352. createApp(App).mount(nodeOps.createElement('div'))
  353. expect(
  354. 'Slot "default" invoked outside of the render function',
  355. ).not.toHaveBeenWarned()
  356. })
  357. test('basic warn when render in setup', () => {
  358. const container = {
  359. setup(_: any, { slots }: any) {
  360. slots.default && slots.default()
  361. return () => null
  362. },
  363. }
  364. const comp = h(container, null, () => h('div'))
  365. const App = {
  366. setup() {
  367. render(h(comp), nodeOps.createElement('div'))
  368. return () => null
  369. },
  370. }
  371. createApp(App).mount(nodeOps.createElement('div'))
  372. expect(
  373. 'Slot "default" invoked outside of the render function',
  374. ).toHaveBeenWarned()
  375. })
  376. })