componentSlots.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. import {
  2. createApp,
  3. getCurrentInstance,
  4. h,
  5. nextTick,
  6. nodeOps,
  7. ref,
  8. render,
  9. serializeInner,
  10. useSlots,
  11. } from '@vue/runtime-test'
  12. import { createBlock, normalizeVNode } from '../src/vnode'
  13. import { createSlots } from '../src/helpers/createSlots'
  14. describe('component: slots', () => {
  15. function renderWithSlots(slots: any): any {
  16. let instance: any
  17. const Comp = {
  18. render() {
  19. instance = getCurrentInstance()
  20. return h('div')
  21. },
  22. }
  23. render(h(Comp, null, slots), nodeOps.createElement('div'))
  24. return instance
  25. }
  26. test('initSlots: instance.slots should be set correctly', () => {
  27. let instance: any
  28. const Comp = {
  29. render() {
  30. instance = getCurrentInstance()
  31. return h('div')
  32. },
  33. }
  34. const slots = { foo: () => {}, _: 1 }
  35. render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
  36. expect(instance.slots).toMatchObject(slots)
  37. })
  38. test('initSlots: instance.slots should remove compiler marker if parent is using manual render function', () => {
  39. const { slots } = renderWithSlots({ _: 1 })
  40. expect(slots).toMatchObject({})
  41. })
  42. test('initSlots: ensure compiler marker non-enumerable', () => {
  43. const Comp = {
  44. render() {
  45. const slots = useSlots()
  46. // Only user-defined slots should be enumerable
  47. expect(Object.keys(slots)).toEqual(['foo'])
  48. // Internal compiler markers must still exist but be non-enumerable
  49. expect(slots).toHaveProperty('_')
  50. expect(Object.getOwnPropertyDescriptor(slots, '_')!.enumerable).toBe(
  51. false,
  52. )
  53. return h('div')
  54. },
  55. }
  56. const slots = { foo: () => {}, _: 1 }
  57. render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
  58. })
  59. test('initSlots: should normalize object slots (when value is null, string, array)', () => {
  60. const { slots } = renderWithSlots({
  61. _inner: '_inner',
  62. foo: null,
  63. header: 'header',
  64. footer: ['f1', 'f2'],
  65. })
  66. expect(
  67. '[Vue warn]: Non-function value encountered for slot "_inner". Prefer function slots for better performance.',
  68. ).toHaveBeenWarned()
  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('foo')
  76. expect(slots._inner()).toMatchObject([normalizeVNode('_inner')])
  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. test('slot name starts with underscore', () => {
  377. const Comp = {
  378. setup(_: any, { slots }: any) {
  379. return () => slots._foo()
  380. },
  381. }
  382. const App = {
  383. setup() {
  384. return () => h(Comp, null, { _foo: () => 'foo' })
  385. },
  386. }
  387. const root = nodeOps.createElement('div')
  388. createApp(App).mount(root)
  389. expect(serializeInner(root)).toBe('foo')
  390. })
  391. })