componentSlots.spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`.
  2. import {
  3. createComponent,
  4. createForSlots,
  5. createSlot,
  6. createVaporApp,
  7. defineComponent,
  8. getCurrentInstance,
  9. insert,
  10. nextTick,
  11. prepend,
  12. ref,
  13. renderEffect,
  14. setText,
  15. template,
  16. withDestructure,
  17. } from '../src'
  18. import { makeRender } from './_utils'
  19. const define = makeRender<any>()
  20. function renderWithSlots(slots: any): any {
  21. let instance: any
  22. const Comp = defineComponent({
  23. render() {
  24. const t0 = template('<div></div>')
  25. const n0 = t0()
  26. instance = getCurrentInstance()
  27. return n0
  28. },
  29. })
  30. const { render } = define({
  31. render() {
  32. return createComponent(Comp, {}, slots)
  33. },
  34. })
  35. render()
  36. return instance
  37. }
  38. describe('component: slots', () => {
  39. test('initSlots: instance.slots should be set correctly', () => {
  40. let instance: any
  41. const Comp = defineComponent({
  42. render() {
  43. const t0 = template('<div></div>')
  44. const n0 = t0()
  45. instance = getCurrentInstance()
  46. return n0
  47. },
  48. })
  49. const { render } = define({
  50. render() {
  51. return createComponent(Comp, {}, { header: () => template('header')() })
  52. },
  53. })
  54. render()
  55. expect(instance.slots.header()).toMatchObject(
  56. document.createTextNode('header'),
  57. )
  58. })
  59. // NOTE: slot normalization is not supported
  60. test.todo(
  61. 'initSlots: should normalize object slots (when value is null, string, array)',
  62. () => {},
  63. )
  64. test.todo(
  65. 'initSlots: should normalize object slots (when value is function)',
  66. () => {},
  67. )
  68. // runtime-core's "initSlots: instance.slots should be set correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)"
  69. test('initSlots: instance.slots should be set correctly', () => {
  70. const { slots } = renderWithSlots({
  71. default: () => template('<span></span>')(),
  72. })
  73. // expect(
  74. // '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
  75. // ).toHaveBeenWarned()
  76. expect(slots.default()).toMatchObject(document.createElement('span'))
  77. })
  78. test('updateSlots: instance.slots should be updated correctly', async () => {
  79. const flag1 = ref(true)
  80. let instance: any
  81. const Child = () => {
  82. instance = getCurrentInstance()
  83. return template('child')()
  84. }
  85. const { render } = define({
  86. render() {
  87. return createComponent(Child, {}, [
  88. () =>
  89. flag1.value
  90. ? { name: 'one', fn: () => template('<span></span>')() }
  91. : { name: 'two', fn: () => template('<div></div>')() },
  92. ])
  93. },
  94. })
  95. render()
  96. expect(instance.slots).toHaveProperty('one')
  97. expect(instance.slots).not.toHaveProperty('two')
  98. flag1.value = false
  99. await nextTick()
  100. expect(instance.slots).not.toHaveProperty('one')
  101. expect(instance.slots).toHaveProperty('two')
  102. })
  103. // NOTE: it is not supported
  104. // test('updateSlots: instance.slots should be updated correctly (when slotType is null)', () => {})
  105. // runtime-core's "updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)"
  106. test('updateSlots: instance.slots should be update correctly', async () => {
  107. const flag1 = ref(true)
  108. let instance: any
  109. const Child = () => {
  110. instance = getCurrentInstance()
  111. return template('child')()
  112. }
  113. const { render } = define({
  114. setup() {
  115. return createComponent(Child, {}, [
  116. () =>
  117. flag1.value
  118. ? { name: 'header', fn: () => template('header')() }
  119. : { name: 'footer', fn: () => template('footer')() },
  120. ])
  121. },
  122. })
  123. render()
  124. expect(instance.slots).toHaveProperty('header')
  125. flag1.value = false
  126. await nextTick()
  127. // expect(
  128. // '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
  129. // ).toHaveBeenWarned()
  130. expect(instance.slots).toHaveProperty('footer')
  131. })
  132. test('the current instance should be kept in the slot', async () => {
  133. let instanceInDefaultSlot: any
  134. let instanceInFooSlot: any
  135. const Comp = defineComponent({
  136. render() {
  137. const instance = getCurrentInstance()
  138. instance!.slots.default!()
  139. instance!.slots.foo!()
  140. return template('<div></div>')()
  141. },
  142. })
  143. const { instance } = define({
  144. render() {
  145. return createComponent(Comp, {}, [
  146. {
  147. default: () => {
  148. instanceInDefaultSlot = getCurrentInstance()
  149. return template('content')()
  150. },
  151. foo: () => {
  152. instanceInFooSlot = getCurrentInstance()
  153. return template('content')()
  154. },
  155. },
  156. ])
  157. },
  158. }).render()
  159. expect(instanceInDefaultSlot).toBe(instance)
  160. expect(instanceInFooSlot).toBe(instance)
  161. })
  162. test('the current instance should be kept in the dynamic slots', async () => {
  163. let instanceInDefaultSlot: any
  164. let instanceInVForSlot: any
  165. let instanceInVIfSlot: any
  166. const Comp = defineComponent({
  167. render() {
  168. const instance = getCurrentInstance()
  169. instance!.slots.default!()
  170. instance!.slots.inVFor!()
  171. instance!.slots.inVIf!()
  172. return template('<div></div>')()
  173. },
  174. })
  175. const { instance } = define({
  176. render() {
  177. return createComponent(Comp, {}, [
  178. {
  179. default: () => {
  180. instanceInDefaultSlot = getCurrentInstance()
  181. return template('content')()
  182. },
  183. },
  184. () => ({
  185. name: 'inVFor',
  186. fn: () => {
  187. instanceInVForSlot = getCurrentInstance()
  188. return template('content')()
  189. },
  190. }),
  191. () => ({
  192. name: 'inVIf',
  193. fn: () => {
  194. instanceInVIfSlot = getCurrentInstance()
  195. return template('content')()
  196. },
  197. }),
  198. ])
  199. },
  200. }).render()
  201. expect(instanceInDefaultSlot).toBe(instance)
  202. expect(instanceInVForSlot).toBe(instance)
  203. expect(instanceInVIfSlot).toBe(instance)
  204. })
  205. test('dynamicSlots should update separately', async () => {
  206. const flag1 = ref(true)
  207. const flag2 = ref(true)
  208. const slotFn1 = vitest.fn()
  209. const slotFn2 = vitest.fn()
  210. let instance: any
  211. const Child = () => {
  212. instance = getCurrentInstance()
  213. return template('child')()
  214. }
  215. const { render } = define({
  216. render() {
  217. return createComponent(Child, {}, [
  218. () => {
  219. slotFn1()
  220. return flag1.value
  221. ? { name: 'one', fn: () => template('one')() }
  222. : { name: 'two', fn: () => template('two')() }
  223. },
  224. () => {
  225. slotFn2()
  226. return flag2.value
  227. ? { name: 'three', fn: () => template('three')() }
  228. : { name: 'four', fn: () => template('four')() }
  229. },
  230. ])
  231. },
  232. })
  233. render()
  234. expect(instance.slots).toHaveProperty('one')
  235. expect(instance.slots).toHaveProperty('three')
  236. expect(slotFn1).toHaveBeenCalledTimes(1)
  237. expect(slotFn2).toHaveBeenCalledTimes(1)
  238. flag1.value = false
  239. await nextTick()
  240. expect(instance.slots).toHaveProperty('two')
  241. expect(instance.slots).toHaveProperty('three')
  242. expect(slotFn1).toHaveBeenCalledTimes(2)
  243. expect(slotFn2).toHaveBeenCalledTimes(1)
  244. flag2.value = false
  245. await nextTick()
  246. expect(instance.slots).toHaveProperty('two')
  247. expect(instance.slots).toHaveProperty('four')
  248. expect(slotFn1).toHaveBeenCalledTimes(2)
  249. expect(slotFn2).toHaveBeenCalledTimes(2)
  250. })
  251. test('should work with createFlorSlots', async () => {
  252. const loop = ref([1, 2, 3])
  253. let instance: any
  254. const Child = () => {
  255. instance = getCurrentInstance()
  256. return template('child')()
  257. }
  258. const { render } = define({
  259. setup() {
  260. return createComponent(Child, {}, [
  261. () =>
  262. createForSlots(loop.value, (item, i) => ({
  263. name: item,
  264. fn: () => template(item + i)(),
  265. })),
  266. ])
  267. },
  268. })
  269. render()
  270. expect(instance.slots).toHaveProperty('1')
  271. expect(instance.slots).toHaveProperty('2')
  272. expect(instance.slots).toHaveProperty('3')
  273. loop.value.push(4)
  274. await nextTick()
  275. expect(instance.slots).toHaveProperty('4')
  276. loop.value.shift()
  277. await nextTick()
  278. expect(instance.slots).not.toHaveProperty('1')
  279. })
  280. test.todo('should respect $stable flag', async () => {
  281. // TODO: $stable flag?
  282. })
  283. test.todo('should not warn when mounting another app in setup', () => {
  284. // TODO: warning
  285. const Comp = defineComponent({
  286. render() {
  287. const i = getCurrentInstance()
  288. return i!.slots.default!()
  289. },
  290. })
  291. const mountComp = () => {
  292. createVaporApp({
  293. render() {
  294. return createComponent(
  295. Comp,
  296. {},
  297. { default: () => template('msg')() },
  298. )!
  299. },
  300. })
  301. }
  302. const App = {
  303. setup() {
  304. mountComp()
  305. },
  306. render() {
  307. return null!
  308. },
  309. }
  310. createVaporApp(App).mount(document.createElement('div'))
  311. expect(
  312. 'Slot "default" invoked outside of the render function',
  313. ).not.toHaveBeenWarned()
  314. })
  315. describe('createSlot', () => {
  316. test('slot should be render correctly', () => {
  317. const Comp = defineComponent(() => {
  318. const n0 = template('<div></div>')()
  319. insert(createSlot('header'), n0 as any as ParentNode)
  320. return n0
  321. })
  322. const { host } = define(() => {
  323. return createComponent(Comp, {}, { header: () => template('header')() })
  324. }).render()
  325. expect(host.innerHTML).toBe('<div>header</div>')
  326. })
  327. test('slot should be render correctly with binds', async () => {
  328. const Comp = defineComponent(() => {
  329. const n0 = template('<div></div>')()
  330. insert(
  331. createSlot('header', [{ title: () => 'header' }]),
  332. n0 as any as ParentNode,
  333. )
  334. return n0
  335. })
  336. const { host } = define(() => {
  337. return createComponent(Comp, {}, [
  338. {
  339. header: withDestructure(
  340. ({ title }) => [title],
  341. ctx => {
  342. const el = template('<h1></h1>')()
  343. renderEffect(() => {
  344. setText(el, ctx[0])
  345. })
  346. return el
  347. },
  348. ),
  349. },
  350. ])
  351. }).render()
  352. expect(host.innerHTML).toBe('<div><h1>header</h1></div>')
  353. })
  354. test('dynamic slot props', async () => {
  355. let props: any
  356. const bindObj = ref<Record<string, any>>({ foo: 1, baz: 'qux' })
  357. const Comp = defineComponent(() =>
  358. createSlot('default', [() => bindObj.value]),
  359. )
  360. define(() =>
  361. createComponent(
  362. Comp,
  363. {},
  364. { default: _props => ((props = _props), []) },
  365. ),
  366. ).render()
  367. expect(props).toEqual({ foo: 1, baz: 'qux' })
  368. bindObj.value.foo = 2
  369. await nextTick()
  370. expect(props).toEqual({ foo: 2, baz: 'qux' })
  371. delete bindObj.value.baz
  372. await nextTick()
  373. expect(props).toEqual({ foo: 2 })
  374. })
  375. test('dynamic slot props with static slot props', async () => {
  376. let props: any
  377. const foo = ref(0)
  378. const bindObj = ref<Record<string, any>>({ foo: 100, baz: 'qux' })
  379. const Comp = defineComponent(() =>
  380. createSlot('default', [{ foo: () => foo.value }, () => bindObj.value]),
  381. )
  382. define(() =>
  383. createComponent(
  384. Comp,
  385. {},
  386. { default: _props => ((props = _props), []) },
  387. ),
  388. ).render()
  389. expect(props).toEqual({ foo: 100, baz: 'qux' })
  390. foo.value = 2
  391. await nextTick()
  392. expect(props).toEqual({ foo: 100, baz: 'qux' })
  393. delete bindObj.value.foo
  394. await nextTick()
  395. expect(props).toEqual({ foo: 2, baz: 'qux' })
  396. })
  397. test('slot class binding should be merged', async () => {
  398. let props: any
  399. const className = ref('foo')
  400. const classObj = ref({ bar: true })
  401. const Comp = defineComponent(() =>
  402. createSlot('default', [
  403. { class: () => className.value },
  404. () => ({ class: ['baz', 'qux'] }),
  405. { class: () => classObj.value },
  406. ]),
  407. )
  408. define(() =>
  409. createComponent(
  410. Comp,
  411. {},
  412. { default: _props => ((props = _props), []) },
  413. ),
  414. ).render()
  415. expect(props).toEqual({ class: 'foo baz qux bar' })
  416. classObj.value.bar = false
  417. await nextTick()
  418. expect(props).toEqual({ class: 'foo baz qux' })
  419. className.value = ''
  420. await nextTick()
  421. expect(props).toEqual({ class: 'baz qux' })
  422. })
  423. test('slot style binding should be merged', async () => {
  424. let props: any
  425. const style = ref<any>({ fontSize: '12px' })
  426. const Comp = defineComponent(() =>
  427. createSlot('default', [
  428. { style: () => style.value },
  429. () => ({ style: { width: '100px', color: 'blue' } }),
  430. { style: () => 'color: red' },
  431. ]),
  432. )
  433. define(() =>
  434. createComponent(
  435. Comp,
  436. {},
  437. { default: _props => ((props = _props), []) },
  438. ),
  439. ).render()
  440. expect(props).toEqual({
  441. style: {
  442. fontSize: '12px',
  443. width: '100px',
  444. color: 'red',
  445. },
  446. })
  447. style.value = null
  448. await nextTick()
  449. expect(props).toEqual({
  450. style: {
  451. width: '100px',
  452. color: 'red',
  453. },
  454. })
  455. })
  456. test('dynamic slot should be render correctly with binds', async () => {
  457. const Comp = defineComponent(() => {
  458. const n0 = template('<div></div>')()
  459. prepend(
  460. n0 as any as ParentNode,
  461. createSlot('header', [{ title: () => 'header' }]),
  462. )
  463. return n0
  464. })
  465. const { host } = define(() => {
  466. // dynamic slot
  467. return createComponent(Comp, {}, [
  468. () => ({
  469. name: 'header',
  470. fn: (props: any) => template(props.title)(),
  471. }),
  472. ])
  473. }).render()
  474. expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
  475. })
  476. test('dynamic slot outlet should be render correctly with binds', async () => {
  477. const Comp = defineComponent(() => {
  478. const n0 = template('<div></div>')()
  479. prepend(
  480. n0 as any as ParentNode,
  481. createSlot(
  482. () => 'header', // dynamic slot outlet name
  483. [{ title: () => 'header' }],
  484. ),
  485. )
  486. return n0
  487. })
  488. const { host } = define(() => {
  489. return createComponent(
  490. Comp,
  491. {},
  492. { header: props => template(props.title)() },
  493. )
  494. }).render()
  495. expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
  496. })
  497. test('fallback should be render correctly', () => {
  498. const Comp = defineComponent(() => {
  499. const n0 = template('<div></div>')()
  500. insert(
  501. createSlot('header', undefined, () => template('fallback')()),
  502. n0 as any as ParentNode,
  503. )
  504. return n0
  505. })
  506. const { host } = define(() => {
  507. return createComponent(Comp, {}, {})
  508. }).render()
  509. expect(host.innerHTML).toBe('<div>fallback</div>')
  510. })
  511. test('dynamic slot should be updated correctly', async () => {
  512. const flag1 = ref(true)
  513. const Child = defineComponent(() => {
  514. const temp0 = template('<p></p>')
  515. const el0 = temp0()
  516. const el1 = temp0()
  517. const slot1 = createSlot('one', [], () => template('one fallback')())
  518. const slot2 = createSlot('two', [], () => template('two fallback')())
  519. insert(slot1, el0 as any as ParentNode)
  520. insert(slot2, el1 as any as ParentNode)
  521. return [el0, el1]
  522. })
  523. const { host } = define(() => {
  524. return createComponent(Child, {}, [
  525. () =>
  526. flag1.value
  527. ? { name: 'one', fn: () => template('one content')() }
  528. : { name: 'two', fn: () => template('two content')() },
  529. ])
  530. }).render()
  531. expect(host.innerHTML).toBe(
  532. '<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
  533. )
  534. flag1.value = false
  535. await nextTick()
  536. expect(host.innerHTML).toBe(
  537. '<p>one fallback<!--slot--></p><p>two content<!--slot--></p>',
  538. )
  539. flag1.value = true
  540. await nextTick()
  541. expect(host.innerHTML).toBe(
  542. '<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
  543. )
  544. })
  545. test('dynamic slot outlet should be updated correctly', async () => {
  546. const slotOutletName = ref('one')
  547. const Child = defineComponent(() => {
  548. const temp0 = template('<p></p>')
  549. const el0 = temp0()
  550. const slot1 = createSlot(
  551. () => slotOutletName.value,
  552. undefined,
  553. () => template('fallback')(),
  554. )
  555. insert(slot1, el0 as any as ParentNode)
  556. return el0
  557. })
  558. const { host } = define(() => {
  559. return createComponent(
  560. Child,
  561. {},
  562. {
  563. one: () => template('one content')(),
  564. two: () => template('two content')(),
  565. },
  566. )
  567. }).render()
  568. expect(host.innerHTML).toBe('<p>one content<!--slot--></p>')
  569. slotOutletName.value = 'two'
  570. await nextTick()
  571. expect(host.innerHTML).toBe('<p>two content<!--slot--></p>')
  572. slotOutletName.value = 'none'
  573. await nextTick()
  574. expect(host.innerHTML).toBe('<p>fallback<!--slot--></p>')
  575. })
  576. })
  577. })