componentSlots.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`.
  2. import {
  3. createComponent,
  4. createSlot,
  5. createVaporApp,
  6. defineComponent,
  7. getCurrentInstance,
  8. insert,
  9. nextTick,
  10. prepend,
  11. ref,
  12. renderEffect,
  13. setText,
  14. template,
  15. } from '../src'
  16. import { makeRender } from './_utils'
  17. const define = makeRender<any>()
  18. function renderWithSlots(slots: any): any {
  19. let instance: any
  20. const Comp = defineComponent({
  21. render() {
  22. const t0 = template('<div></div>')
  23. const n0 = t0()
  24. instance = getCurrentInstance()
  25. return n0
  26. },
  27. })
  28. const { render } = define({
  29. render() {
  30. return createComponent(Comp, {}, slots)
  31. },
  32. })
  33. render()
  34. return instance
  35. }
  36. describe('component: slots', () => {
  37. test('initSlots: instance.slots should be set correctly', () => {
  38. let instance: any
  39. const Comp = defineComponent({
  40. render() {
  41. const t0 = template('<div></div>')
  42. const n0 = t0()
  43. instance = getCurrentInstance()
  44. return n0
  45. },
  46. })
  47. const { render } = define({
  48. render() {
  49. return createComponent(Comp, {}, { header: () => template('header')() })
  50. },
  51. })
  52. render()
  53. expect(instance.slots.header()).toMatchObject(
  54. document.createTextNode('header'),
  55. )
  56. })
  57. // NOTE: slot normalization is not supported
  58. test.todo(
  59. 'initSlots: should normalize object slots (when value is null, string, array)',
  60. () => {},
  61. )
  62. test.todo(
  63. 'initSlots: should normalize object slots (when value is function)',
  64. () => {},
  65. )
  66. // runtime-core's "initSlots: instance.slots should be set correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)"
  67. test('initSlots: instance.slots should be set correctly', () => {
  68. const { slots } = renderWithSlots({
  69. default: () => template('<span></span>')(),
  70. })
  71. // expect(
  72. // '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
  73. // ).toHaveBeenWarned()
  74. expect(slots.default()).toMatchObject(document.createElement('span'))
  75. })
  76. test('updateSlots: instance.slots should be updated correctly', async () => {
  77. const flag1 = ref(true)
  78. let instance: any
  79. const Child = () => {
  80. instance = getCurrentInstance()
  81. return template('child')()
  82. }
  83. const { render } = define({
  84. render() {
  85. return createComponent(Child, {}, { _: 2 as any }, () => [
  86. flag1.value
  87. ? { name: 'one', fn: () => template('<span></span>')() }
  88. : { name: 'two', fn: () => template('<div></div>')() },
  89. ])
  90. },
  91. })
  92. render()
  93. expect(instance.slots).toHaveProperty('one')
  94. expect(instance.slots).not.toHaveProperty('two')
  95. flag1.value = false
  96. await nextTick()
  97. expect(instance.slots).not.toHaveProperty('one')
  98. expect(instance.slots).toHaveProperty('two')
  99. })
  100. // NOTE: it is not supported
  101. // test('updateSlots: instance.slots should be updated correctly (when slotType is null)', () => {})
  102. // runtime-core's "updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)"
  103. test('updateSlots: instance.slots should be update correctly', async () => {
  104. const flag1 = ref(true)
  105. let instance: any
  106. const Child = () => {
  107. instance = getCurrentInstance()
  108. return template('child')()
  109. }
  110. const { render } = define({
  111. setup() {
  112. return createComponent(Child, {}, {}, () => [
  113. flag1.value
  114. ? [{ name: 'header', fn: () => template('header')() }]
  115. : [{ name: 'footer', fn: () => template('footer')() }],
  116. ])
  117. },
  118. })
  119. render()
  120. expect(instance.slots).toHaveProperty('header')
  121. flag1.value = false
  122. await nextTick()
  123. // expect(
  124. // '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
  125. // ).toHaveBeenWarned()
  126. expect(instance.slots).toHaveProperty('footer')
  127. })
  128. test('the current instance should be kept in the slot', async () => {
  129. let instanceInDefaultSlot: any
  130. let instanceInVForSlot: any
  131. let instanceInVIfSlot: any
  132. const Comp = defineComponent({
  133. render() {
  134. const instance = getCurrentInstance()
  135. instance!.slots.default!()
  136. instance!.slots.inVFor!()
  137. instance!.slots.inVIf!()
  138. return template('<div></div>')()
  139. },
  140. })
  141. const { instance } = define({
  142. render() {
  143. return createComponent(
  144. Comp,
  145. {},
  146. {
  147. default: () => {
  148. instanceInDefaultSlot = getCurrentInstance()
  149. return template('content')()
  150. },
  151. },
  152. () => [
  153. [
  154. {
  155. name: 'inVFor',
  156. fn: () => {
  157. instanceInVForSlot = getCurrentInstance()
  158. return template('content')()
  159. },
  160. },
  161. ],
  162. {
  163. name: 'inVIf',
  164. key: '1',
  165. fn: () => {
  166. instanceInVIfSlot = getCurrentInstance()
  167. return template('content')()
  168. },
  169. },
  170. ],
  171. )
  172. },
  173. }).render()
  174. expect(instanceInDefaultSlot).toBe(instance)
  175. expect(instanceInVForSlot).toBe(instance)
  176. expect(instanceInVIfSlot).toBe(instance)
  177. })
  178. test.todo('should respect $stable flag', async () => {
  179. // TODO: $stable flag?
  180. })
  181. test.todo('should not warn when mounting another app in setup', () => {
  182. // TODO: warning
  183. const Comp = defineComponent({
  184. render() {
  185. const i = getCurrentInstance()
  186. return i!.slots.default!()
  187. },
  188. })
  189. const mountComp = () => {
  190. createVaporApp({
  191. render() {
  192. return createComponent(
  193. Comp,
  194. {},
  195. { default: () => template('msg')() },
  196. )!
  197. },
  198. })
  199. }
  200. const App = {
  201. setup() {
  202. mountComp()
  203. },
  204. render() {
  205. return null!
  206. },
  207. }
  208. createVaporApp(App).mount(document.createElement('div'))
  209. expect(
  210. 'Slot "default" invoked outside of the render function',
  211. ).not.toHaveBeenWarned()
  212. })
  213. describe('createSlot', () => {
  214. test('slot should be render correctly', () => {
  215. const Comp = defineComponent(() => {
  216. const n0 = template('<div></div>')()
  217. insert(createSlot('header'), n0 as any as ParentNode)
  218. return n0
  219. })
  220. const { host } = define(() => {
  221. return createComponent(Comp, {}, { header: () => template('header')() })
  222. }).render()
  223. expect(host.innerHTML).toBe('<div>header</div>')
  224. })
  225. test('slot should be render correctly with binds', async () => {
  226. const Comp = defineComponent(() => {
  227. const n0 = template('<div></div>')()
  228. insert(
  229. createSlot('header', { title: () => 'header' }),
  230. n0 as any as ParentNode,
  231. )
  232. return n0
  233. })
  234. const { host } = define(() => {
  235. return createComponent(
  236. Comp,
  237. {},
  238. {
  239. header: ({ title }) => {
  240. const el = template('<h1></h1>')()
  241. renderEffect(() => {
  242. setText(el, title())
  243. })
  244. return el
  245. },
  246. },
  247. )
  248. }).render()
  249. expect(host.innerHTML).toBe('<div><h1>header</h1></div>')
  250. })
  251. test('dynamic slot should be render correctly with binds', async () => {
  252. const Comp = defineComponent(() => {
  253. const n0 = template('<div></div>')()
  254. prepend(
  255. n0 as any as ParentNode,
  256. createSlot('header', { title: () => 'header' }),
  257. )
  258. return n0
  259. })
  260. const { host } = define(() => {
  261. // dynamic slot
  262. return createComponent(Comp, {}, {}, () => [
  263. { name: 'header', fn: ({ title }) => template(`${title()}`)() },
  264. ])
  265. }).render()
  266. expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
  267. })
  268. test('dynamic slot outlet should be render correctly with binds', async () => {
  269. const Comp = defineComponent(() => {
  270. const n0 = template('<div></div>')()
  271. prepend(
  272. n0 as any as ParentNode,
  273. createSlot(
  274. () => 'header', // dynamic slot outlet name
  275. { title: () => 'header' },
  276. ),
  277. )
  278. return n0
  279. })
  280. const { host } = define(() => {
  281. return createComponent(
  282. Comp,
  283. {},
  284. { header: ({ title }) => template(`${title()}`)() },
  285. )
  286. }).render()
  287. expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
  288. })
  289. test('fallback should be render correctly', () => {
  290. const Comp = defineComponent(() => {
  291. const n0 = template('<div></div>')()
  292. insert(
  293. createSlot('header', {}, () => template('fallback')()),
  294. n0 as any as ParentNode,
  295. )
  296. return n0
  297. })
  298. const { host } = define(() => {
  299. return createComponent(Comp, {}, {})
  300. }).render()
  301. expect(host.innerHTML).toBe('<div>fallback</div>')
  302. })
  303. test('dynamic slot should be updated correctly', async () => {
  304. const flag1 = ref(true)
  305. const Child = defineComponent(() => {
  306. const temp0 = template('<p></p>')
  307. const el0 = temp0()
  308. const el1 = temp0()
  309. const slot1 = createSlot('one', {}, () => template('one fallback')())
  310. const slot2 = createSlot('two', {}, () => template('two fallback')())
  311. insert(slot1, el0 as any as ParentNode)
  312. insert(slot2, el1 as any as ParentNode)
  313. return [el0, el1]
  314. })
  315. const { host } = define(() => {
  316. return createComponent(Child, {}, {}, () => [
  317. flag1.value
  318. ? { name: 'one', fn: () => template('one content')() }
  319. : { name: 'two', fn: () => template('two content')() },
  320. ])
  321. }).render()
  322. expect(host.innerHTML).toBe(
  323. '<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
  324. )
  325. flag1.value = false
  326. await nextTick()
  327. expect(host.innerHTML).toBe(
  328. '<p>one fallback<!--slot--></p><p>two content<!--slot--></p>',
  329. )
  330. flag1.value = true
  331. await nextTick()
  332. expect(host.innerHTML).toBe(
  333. '<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
  334. )
  335. })
  336. test('dynamic slot outlet should be updated correctly', async () => {
  337. const slotOutletName = ref('one')
  338. const Child = defineComponent(() => {
  339. const temp0 = template('<p></p>')
  340. const el0 = temp0()
  341. const slot1 = createSlot(
  342. () => slotOutletName.value,
  343. {},
  344. () => template('fallback')(),
  345. )
  346. insert(slot1, el0 as any as ParentNode)
  347. return el0
  348. })
  349. const { host } = define(() => {
  350. return createComponent(
  351. Child,
  352. {},
  353. {
  354. one: () => template('one content')(),
  355. two: () => template('two content')(),
  356. },
  357. )
  358. }).render()
  359. expect(host.innerHTML).toBe('<p>one content<!--slot--></p>')
  360. slotOutletName.value = 'two'
  361. await nextTick()
  362. expect(host.innerHTML).toBe('<p>two content<!--slot--></p>')
  363. slotOutletName.value = 'none'
  364. await nextTick()
  365. expect(host.innerHTML).toBe('<p>fallback<!--slot--></p>')
  366. })
  367. })
  368. })