componentSlots.spec.ts 16 KB

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