componentSlots.spec.ts 12 KB

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