componentSlots.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`.
  2. import {
  3. createComponent,
  4. createForSlots,
  5. createIf,
  6. createSlot,
  7. createVaporApp,
  8. defineVaporComponent,
  9. insert,
  10. prepend,
  11. renderEffect,
  12. setText,
  13. template,
  14. } from '../src'
  15. import { currentInstance, nextTick, ref } from '@vue/runtime-dom'
  16. import { makeRender } from './_utils'
  17. import type { DynamicSlot } from '../src/componentSlots'
  18. const define = makeRender<any>()
  19. function renderWithSlots(slots: any): any {
  20. let instance: any
  21. const Comp = defineVaporComponent({
  22. setup() {
  23. const t0 = template('<div></div>')
  24. const n0 = t0()
  25. instance = currentInstance
  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. const { slots } = renderWithSlots({
  40. default: () => template('<span></span>')(),
  41. })
  42. expect(slots.default()).toMatchObject(document.createElement('span'))
  43. })
  44. test('updateSlots: instance.slots should be updated correctly', async () => {
  45. const flag1 = ref(true)
  46. let instance: any
  47. const Child = () => {
  48. instance = currentInstance
  49. return template('child')()
  50. }
  51. const { render } = define({
  52. render() {
  53. return createComponent(
  54. Child,
  55. {},
  56. {
  57. $: [
  58. () =>
  59. flag1.value
  60. ? { name: 'one', fn: () => template('<span></span>')() }
  61. : { name: 'two', fn: () => template('<div></div>')() },
  62. ],
  63. },
  64. )
  65. },
  66. })
  67. render()
  68. expect(instance.slots).toHaveProperty('one')
  69. expect(instance.slots).not.toHaveProperty('two')
  70. flag1.value = false
  71. await nextTick()
  72. expect(instance.slots).not.toHaveProperty('one')
  73. expect(instance.slots).toHaveProperty('two')
  74. })
  75. test('should work with createFlorSlots', async () => {
  76. const loop = ref([1, 2, 3])
  77. let instance: any
  78. const Child = () => {
  79. instance = currentInstance
  80. return template('child')()
  81. }
  82. const { render } = define({
  83. setup() {
  84. return createComponent(Child, null, {
  85. $: [
  86. () =>
  87. createForSlots(loop.value, (item, i) => ({
  88. name: item,
  89. fn: () => template(item + i)(),
  90. })),
  91. ],
  92. })
  93. },
  94. })
  95. render()
  96. expect(instance.slots).toHaveProperty('1')
  97. expect(instance.slots).toHaveProperty('2')
  98. expect(instance.slots).toHaveProperty('3')
  99. loop.value.push(4)
  100. await nextTick()
  101. expect(instance.slots).toHaveProperty('4')
  102. loop.value.shift()
  103. await nextTick()
  104. expect(instance.slots).not.toHaveProperty('1')
  105. })
  106. // passes but no warning for slot invocation in vapor currently
  107. test.todo('should not warn when mounting another app in setup', () => {
  108. const Comp = defineVaporComponent({
  109. setup(_, { slots }) {
  110. return slots.default!()
  111. },
  112. })
  113. const mountComp = () => {
  114. createVaporApp({
  115. render() {
  116. return createComponent(
  117. Comp,
  118. {},
  119. { default: () => template('msg')() },
  120. )!
  121. },
  122. })
  123. }
  124. const App = {
  125. setup() {
  126. mountComp()
  127. return []
  128. },
  129. }
  130. createVaporApp(App).mount(document.createElement('div'))
  131. expect(
  132. 'Slot "default" invoked outside of the render function',
  133. ).not.toHaveBeenWarned()
  134. })
  135. describe('createSlot', () => {
  136. test('slot should be rendered correctly', () => {
  137. const Comp = defineVaporComponent(() => {
  138. const n0 = template('<div>')()
  139. insert(createSlot('header'), n0 as any as ParentNode)
  140. return n0
  141. })
  142. const { host } = define(() => {
  143. return createComponent(Comp, null, {
  144. header: () => template('header')(),
  145. })
  146. }).render()
  147. expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
  148. })
  149. test('slot should be rendered correctly with slot props', async () => {
  150. const src = ref('header')
  151. const Comp = defineVaporComponent(() => {
  152. const n0 = template('<div></div>')()
  153. insert(
  154. createSlot('header', { title: () => src.value }),
  155. n0 as any as ParentNode,
  156. )
  157. return n0
  158. })
  159. const { host } = define(() => {
  160. return createComponent(Comp, null, {
  161. header: props => {
  162. const el = template('<h1></h1>')()
  163. renderEffect(() => {
  164. setText(el, props.title)
  165. })
  166. return el
  167. },
  168. })
  169. }).render()
  170. expect(host.innerHTML).toBe('<div><h1>header</h1><!--slot--></div>')
  171. src.value = 'footer'
  172. await nextTick()
  173. expect(host.innerHTML).toBe('<div><h1>footer</h1><!--slot--></div>')
  174. })
  175. test('dynamic slot props', async () => {
  176. let props: any
  177. const bindObj = ref<Record<string, any>>({ foo: 1, baz: 'qux' })
  178. const Comp = defineVaporComponent(() =>
  179. createSlot('default', { $: [() => bindObj.value] }),
  180. )
  181. define(() =>
  182. createComponent(Comp, null, {
  183. default: _props => ((props = _props), []),
  184. }),
  185. ).render()
  186. expect(props).toEqual({ foo: 1, baz: 'qux' })
  187. bindObj.value.foo = 2
  188. await nextTick()
  189. expect(props).toEqual({ foo: 2, baz: 'qux' })
  190. delete bindObj.value.baz
  191. await nextTick()
  192. expect(props).toEqual({ foo: 2 })
  193. })
  194. test('dynamic slot props with static slot props', async () => {
  195. let props: any
  196. const foo = ref(0)
  197. const bindObj = ref<Record<string, any>>({ foo: 100, baz: 'qux' })
  198. const Comp = defineVaporComponent(() =>
  199. createSlot('default', {
  200. foo: () => foo.value,
  201. $: [() => bindObj.value],
  202. }),
  203. )
  204. define(() =>
  205. createComponent(Comp, null, {
  206. default: _props => ((props = _props), []),
  207. }),
  208. ).render()
  209. expect(props).toEqual({ foo: 100, baz: 'qux' })
  210. foo.value = 2
  211. await nextTick()
  212. expect(props).toEqual({ foo: 100, baz: 'qux' })
  213. delete bindObj.value.foo
  214. await nextTick()
  215. expect(props).toEqual({ foo: 2, baz: 'qux' })
  216. })
  217. test('dynamic slot should be rendered correctly with slot props', async () => {
  218. const val = ref('header')
  219. const Comp = defineVaporComponent(() => {
  220. const n0 = template('<div></div>')()
  221. prepend(
  222. n0 as any as ParentNode,
  223. createSlot('header', { title: () => val.value }),
  224. )
  225. return n0
  226. })
  227. const { host } = define(() => {
  228. // dynamic slot
  229. return createComponent(Comp, null, {
  230. $: [
  231. () => ({
  232. name: 'header',
  233. fn: (props: any) => {
  234. const el = template('<h1></h1>')()
  235. renderEffect(() => {
  236. setText(el, props.title)
  237. })
  238. return el
  239. },
  240. }),
  241. ],
  242. })
  243. }).render()
  244. expect(host.innerHTML).toBe('<div><h1>header</h1><!--slot--></div>')
  245. val.value = 'footer'
  246. await nextTick()
  247. expect(host.innerHTML).toBe('<div><h1>footer</h1><!--slot--></div>')
  248. })
  249. test('dynamic slot outlet should be render correctly with slot props', async () => {
  250. const val = ref('header')
  251. const Comp = defineVaporComponent(() => {
  252. const n0 = template('<div></div>')()
  253. prepend(
  254. n0 as any as ParentNode,
  255. createSlot(
  256. () => val.value, // dynamic slot outlet name
  257. ),
  258. )
  259. return n0
  260. })
  261. const { host } = define(() => {
  262. return createComponent(Comp, null, {
  263. header: () => template('header')(),
  264. footer: () => template('footer')(),
  265. })
  266. }).render()
  267. expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
  268. val.value = 'footer'
  269. await nextTick()
  270. expect(host.innerHTML).toBe('<div>footer<!--slot--></div>')
  271. })
  272. test('fallback should be render correctly', () => {
  273. const Comp = defineVaporComponent(() => {
  274. const n0 = template('<div></div>')()
  275. insert(
  276. createSlot('header', undefined, () => template('fallback')()),
  277. n0 as any as ParentNode,
  278. )
  279. return n0
  280. })
  281. const { host } = define(() => {
  282. return createComponent(Comp, {}, {})
  283. }).render()
  284. expect(host.innerHTML).toBe('<div>fallback<!--slot--></div>')
  285. })
  286. test('dynamic slot should be updated correctly', async () => {
  287. const flag1 = ref(true)
  288. const Child = defineVaporComponent(() => {
  289. const temp0 = template('<p></p>')
  290. const el0 = temp0()
  291. const el1 = temp0()
  292. const slot1 = createSlot('one', null, () => template('one fallback')())
  293. const slot2 = createSlot('two', null, () => template('two fallback')())
  294. insert(slot1, el0 as any as ParentNode)
  295. insert(slot2, el1 as any as ParentNode)
  296. return [el0, el1]
  297. })
  298. const { host } = define(() => {
  299. return createComponent(Child, null, {
  300. $: [
  301. () =>
  302. flag1.value
  303. ? { name: 'one', fn: () => template('one content')() }
  304. : { name: 'two', fn: () => template('two content')() },
  305. ],
  306. })
  307. }).render()
  308. expect(host.innerHTML).toBe(
  309. '<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
  310. )
  311. flag1.value = false
  312. await nextTick()
  313. expect(host.innerHTML).toBe(
  314. '<p>one fallback<!--slot--></p><p>two content<!--slot--></p>',
  315. )
  316. flag1.value = true
  317. await nextTick()
  318. expect(host.innerHTML).toBe(
  319. '<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
  320. )
  321. })
  322. test('dynamic slot outlet should be updated correctly', async () => {
  323. const slotOutletName = ref('one')
  324. const Child = defineVaporComponent(() => {
  325. const temp0 = template('<p>')
  326. const el0 = temp0()
  327. const slot1 = createSlot(
  328. () => slotOutletName.value,
  329. undefined,
  330. () => template('fallback')(),
  331. )
  332. insert(slot1, el0 as any as ParentNode)
  333. return el0
  334. })
  335. const { host } = define(() => {
  336. return createComponent(
  337. Child,
  338. {},
  339. {
  340. one: () => template('one content')(),
  341. two: () => template('two content')(),
  342. },
  343. )
  344. }).render()
  345. expect(host.innerHTML).toBe('<p>one content<!--slot--></p>')
  346. slotOutletName.value = 'two'
  347. await nextTick()
  348. expect(host.innerHTML).toBe('<p>two content<!--slot--></p>')
  349. slotOutletName.value = 'none'
  350. await nextTick()
  351. expect(host.innerHTML).toBe('<p>fallback<!--slot--></p>')
  352. })
  353. test('non-exist slot', async () => {
  354. const Child = defineVaporComponent(() => {
  355. const el0 = template('<p>')()
  356. const slot = createSlot('not-exist', undefined)
  357. insert(slot, el0 as any as ParentNode)
  358. return el0
  359. })
  360. const { host } = define(() => {
  361. return createComponent(Child)
  362. }).render()
  363. expect(host.innerHTML).toBe('<p><!--slot--></p>')
  364. })
  365. test('use fallback when inner content changes', async () => {
  366. const Child = {
  367. setup() {
  368. return createSlot('default', null, () =>
  369. document.createTextNode('fallback'),
  370. )
  371. },
  372. }
  373. const toggle = ref(true)
  374. const { html } = define({
  375. setup() {
  376. return createComponent(Child, null, {
  377. default: () => {
  378. return createIf(
  379. () => toggle.value,
  380. () => {
  381. return document.createTextNode('content')
  382. },
  383. )
  384. },
  385. })
  386. },
  387. }).render()
  388. expect(html()).toBe('content<!--if--><!--slot-->')
  389. toggle.value = false
  390. await nextTick()
  391. expect(html()).toBe('fallback<!--if--><!--slot-->')
  392. toggle.value = true
  393. await nextTick()
  394. expect(html()).toBe('content<!--if--><!--slot-->')
  395. })
  396. test('dynamic slot work with v-if', async () => {
  397. const val = ref('header')
  398. const toggle = ref(false)
  399. const Comp = defineVaporComponent(() => {
  400. const n0 = template('<div></div>')()
  401. prepend(n0 as any as ParentNode, createSlot('header', null))
  402. return n0
  403. })
  404. const { host } = define(() => {
  405. // dynamic slot
  406. return createComponent(Comp, null, {
  407. $: [
  408. () =>
  409. (toggle.value
  410. ? {
  411. name: val.value,
  412. fn: () => {
  413. return template('<h1></h1>')()
  414. },
  415. }
  416. : void 0) as DynamicSlot,
  417. ],
  418. })
  419. }).render()
  420. expect(host.innerHTML).toBe('<div><!--slot--></div>')
  421. toggle.value = true
  422. await nextTick()
  423. expect(host.innerHTML).toBe('<div><h1></h1><!--slot--></div>')
  424. })
  425. })
  426. })