render.spec.ts 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075
  1. /**
  2. * @jest-environment node
  3. */
  4. import {
  5. createApp,
  6. h,
  7. createCommentVNode,
  8. resolveComponent,
  9. ComponentOptions,
  10. ref,
  11. defineComponent,
  12. createTextVNode,
  13. createStaticVNode,
  14. withCtx,
  15. KeepAlive,
  16. Transition,
  17. watchEffect,
  18. createVNode,
  19. resolveDynamicComponent,
  20. renderSlot,
  21. onErrorCaptured,
  22. onServerPrefetch
  23. } from 'vue'
  24. import { escapeHtml } from '@vue/shared'
  25. import { renderToString } from '../src/renderToString'
  26. import { renderToStream as _renderToStream } from '../src/renderToStream'
  27. import { ssrRenderSlot, SSRSlot } from '../src/helpers/ssrRenderSlot'
  28. import { ssrRenderComponent } from '../src/helpers/ssrRenderComponent'
  29. import { Readable } from 'stream'
  30. import { ssrRenderVNode } from '../src'
  31. const promisifyStream = (stream: Readable) => {
  32. return new Promise<string>((resolve, reject) => {
  33. let result = ''
  34. stream.on('data', data => {
  35. result += data
  36. })
  37. stream.on('error', () => {
  38. reject(result)
  39. })
  40. stream.on('end', () => {
  41. resolve(result)
  42. })
  43. })
  44. }
  45. const renderToStream = (app: any, context?: any) =>
  46. promisifyStream(_renderToStream(app, context))
  47. // we run the same tests twice, once for renderToString, once for renderToStream
  48. testRender(`renderToString`, renderToString)
  49. testRender(`renderToStream`, renderToStream)
  50. function testRender(type: string, render: typeof renderToString) {
  51. describe(`ssr: ${type}`, () => {
  52. test('should apply app context', async () => {
  53. const app = createApp({
  54. render() {
  55. const Foo = resolveComponent('foo') as ComponentOptions
  56. return h(Foo)
  57. }
  58. })
  59. app.component('foo', {
  60. render: () => h('div', 'foo')
  61. })
  62. const html = await render(app)
  63. expect(html).toBe(`<div>foo</div>`)
  64. })
  65. describe('components', () => {
  66. test('vnode components', async () => {
  67. expect(
  68. await render(
  69. createApp({
  70. data() {
  71. return { msg: 'hello' }
  72. },
  73. render(this: any) {
  74. return h('div', this.msg)
  75. }
  76. })
  77. )
  78. ).toBe(`<div>hello</div>`)
  79. })
  80. test('option components returning render from setup', async () => {
  81. expect(
  82. await render(
  83. createApp({
  84. setup() {
  85. const msg = ref('hello')
  86. return () => h('div', msg.value)
  87. }
  88. })
  89. )
  90. ).toBe(`<div>hello</div>`)
  91. })
  92. test('setup components returning render from setup', async () => {
  93. expect(
  94. await render(
  95. createApp(
  96. defineComponent(() => {
  97. const msg = ref('hello')
  98. return () => h('div', msg.value)
  99. })
  100. )
  101. )
  102. ).toBe(`<div>hello</div>`)
  103. })
  104. test('components using defineComponent with extends option', async () => {
  105. expect(
  106. await render(
  107. createApp(
  108. defineComponent({
  109. extends: {
  110. data() {
  111. return { msg: 'hello' }
  112. },
  113. render(this: any) {
  114. return h('div', this.msg)
  115. }
  116. }
  117. })
  118. )
  119. )
  120. ).toBe(`<div>hello</div>`)
  121. })
  122. test('components using defineComponent with mixins option', async () => {
  123. expect(
  124. await render(
  125. createApp(
  126. defineComponent({
  127. mixins: [
  128. {
  129. data() {
  130. return { msg: 'hello' }
  131. },
  132. render(this: any) {
  133. return h('div', this.msg)
  134. }
  135. }
  136. ]
  137. })
  138. )
  139. )
  140. ).toBe(`<div>hello</div>`)
  141. })
  142. test('optimized components', async () => {
  143. expect(
  144. await render(
  145. createApp({
  146. data() {
  147. return { msg: 'hello' }
  148. },
  149. ssrRender(ctx, push) {
  150. push(`<div>${ctx.msg}</div>`)
  151. }
  152. })
  153. )
  154. ).toBe(`<div>hello</div>`)
  155. })
  156. test('nested vnode components', async () => {
  157. const Child = {
  158. props: ['msg'],
  159. render(this: any) {
  160. return h('div', this.msg)
  161. }
  162. }
  163. expect(
  164. await render(
  165. createApp({
  166. render() {
  167. return h('div', ['parent', h(Child, { msg: 'hello' })])
  168. }
  169. })
  170. )
  171. ).toBe(`<div>parent<div>hello</div></div>`)
  172. })
  173. test('nested optimized components', async () => {
  174. const Child = {
  175. props: ['msg'],
  176. ssrRender(ctx: any, push: any) {
  177. push(`<div>${ctx.msg}</div>`)
  178. }
  179. }
  180. expect(
  181. await render(
  182. createApp({
  183. ssrRender(_ctx, push, parent) {
  184. push(`<div>parent`)
  185. push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent))
  186. push(`</div>`)
  187. }
  188. })
  189. )
  190. ).toBe(`<div>parent<div>hello</div></div>`)
  191. })
  192. test('nested template components', async () => {
  193. const Child = {
  194. props: ['msg'],
  195. template: `<div>{{ msg }}</div>`
  196. }
  197. const app = createApp({
  198. template: `<div>parent<Child msg="hello" /></div>`
  199. })
  200. app.component('Child', Child)
  201. expect(await render(app)).toBe(`<div>parent<div>hello</div></div>`)
  202. })
  203. test('template components with dynamic class attribute after static', async () => {
  204. const app = createApp({
  205. template: `<div><div class="child" :class="'dynamic'"></div></div>`
  206. })
  207. expect(await render(app)).toBe(
  208. `<div><div class="dynamic child"></div></div>`
  209. )
  210. })
  211. test('template components with dynamic class attribute before static', async () => {
  212. const app = createApp({
  213. template: `<div><div :class="'dynamic'" class="child"></div></div>`
  214. })
  215. expect(await render(app)).toBe(
  216. `<div><div class="dynamic child"></div></div>`
  217. )
  218. })
  219. test('mixing optimized / vnode / template components', async () => {
  220. const OptimizedChild = {
  221. props: ['msg'],
  222. ssrRender(ctx: any, push: any) {
  223. push(`<div>${ctx.msg}</div>`)
  224. }
  225. }
  226. const VNodeChild = {
  227. props: ['msg'],
  228. render(this: any) {
  229. return h('div', this.msg)
  230. }
  231. }
  232. const TemplateChild = {
  233. props: ['msg'],
  234. template: `<div>{{ msg }}</div>`
  235. }
  236. expect(
  237. await render(
  238. createApp({
  239. ssrRender(_ctx, push, parent) {
  240. push(`<div>parent`)
  241. push(
  242. ssrRenderComponent(
  243. OptimizedChild,
  244. { msg: 'opt' },
  245. null,
  246. parent
  247. )
  248. )
  249. push(
  250. ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent)
  251. )
  252. push(
  253. ssrRenderComponent(
  254. TemplateChild,
  255. { msg: 'template' },
  256. null,
  257. parent
  258. )
  259. )
  260. push(`</div>`)
  261. }
  262. })
  263. )
  264. ).toBe(
  265. `<div>parent<div>opt</div><div>vnode</div><div>template</div></div>`
  266. )
  267. })
  268. test('async components', async () => {
  269. const Child = {
  270. // should wait for resolved render context from setup()
  271. async setup() {
  272. return {
  273. msg: 'hello'
  274. }
  275. },
  276. ssrRender(ctx: any, push: any) {
  277. push(`<div>${ctx.msg}</div>`)
  278. }
  279. }
  280. expect(
  281. await render(
  282. createApp({
  283. ssrRender(_ctx, push, parent) {
  284. push(`<div>parent`)
  285. push(ssrRenderComponent(Child, null, null, parent))
  286. push(`</div>`)
  287. }
  288. })
  289. )
  290. ).toBe(`<div>parent<div>hello</div></div>`)
  291. })
  292. test('parallel async components', async () => {
  293. const OptimizedChild = {
  294. props: ['msg'],
  295. async setup(props: any) {
  296. return {
  297. localMsg: props.msg + '!'
  298. }
  299. },
  300. ssrRender(ctx: any, push: any) {
  301. push(`<div>${ctx.localMsg}</div>`)
  302. }
  303. }
  304. const VNodeChild = {
  305. props: ['msg'],
  306. async setup(props: any) {
  307. return {
  308. localMsg: props.msg + '!'
  309. }
  310. },
  311. render(this: any) {
  312. return h('div', this.localMsg)
  313. }
  314. }
  315. expect(
  316. await render(
  317. createApp({
  318. ssrRender(_ctx, push, parent) {
  319. push(`<div>parent`)
  320. push(
  321. ssrRenderComponent(
  322. OptimizedChild,
  323. { msg: 'opt' },
  324. null,
  325. parent
  326. )
  327. )
  328. push(
  329. ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent)
  330. )
  331. push(`</div>`)
  332. }
  333. })
  334. )
  335. ).toBe(`<div>parent<div>opt!</div><div>vnode!</div></div>`)
  336. })
  337. })
  338. describe('slots', () => {
  339. test('nested components with optimized slots', async () => {
  340. const Child = {
  341. props: ['msg'],
  342. ssrRender(ctx: any, push: any, parent: any) {
  343. push(`<div class="child">`)
  344. ssrRenderSlot(
  345. ctx.$slots,
  346. 'default',
  347. { msg: 'from slot' },
  348. () => {
  349. push(`fallback`)
  350. },
  351. push,
  352. parent
  353. )
  354. push(`</div>`)
  355. }
  356. }
  357. expect(
  358. await render(
  359. createApp({
  360. ssrRender(_ctx, push, parent) {
  361. push(`<div>parent`)
  362. push(
  363. ssrRenderComponent(
  364. Child,
  365. { msg: 'hello' },
  366. {
  367. // optimized slot using string push
  368. default: (({ msg }, push, _p) => {
  369. push(`<span>${msg}</span>`)
  370. }) as SSRSlot,
  371. // important to avoid slots being normalized
  372. _: 1 as any
  373. },
  374. parent
  375. )
  376. )
  377. push(`</div>`)
  378. }
  379. })
  380. )
  381. ).toBe(
  382. `<div>parent<div class="child">` +
  383. `<!--[--><span>from slot</span><!--]-->` +
  384. `</div></div>`
  385. )
  386. // test fallback
  387. expect(
  388. await render(
  389. createApp({
  390. ssrRender(_ctx, push, parent) {
  391. push(`<div>parent`)
  392. push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent))
  393. push(`</div>`)
  394. }
  395. })
  396. )
  397. ).toBe(
  398. `<div>parent<div class="child"><!--[-->fallback<!--]--></div></div>`
  399. )
  400. })
  401. test('nested components with vnode slots', async () => {
  402. const Child = {
  403. props: ['msg'],
  404. ssrRender(ctx: any, push: any, parent: any) {
  405. push(`<div class="child">`)
  406. ssrRenderSlot(
  407. ctx.$slots,
  408. 'default',
  409. { msg: 'from slot' },
  410. null,
  411. push,
  412. parent
  413. )
  414. push(`</div>`)
  415. }
  416. }
  417. expect(
  418. await render(
  419. createApp({
  420. ssrRender(_ctx, push, parent) {
  421. push(`<div>parent`)
  422. push(
  423. ssrRenderComponent(
  424. Child,
  425. { msg: 'hello' },
  426. {
  427. // bailed slots returning raw vnodes
  428. default: ({ msg }: any) => {
  429. return h('span', msg)
  430. }
  431. },
  432. parent
  433. )
  434. )
  435. push(`</div>`)
  436. }
  437. })
  438. )
  439. ).toBe(
  440. `<div>parent<div class="child">` +
  441. `<!--[--><span>from slot</span><!--]-->` +
  442. `</div></div>`
  443. )
  444. })
  445. test('nested components with template slots', async () => {
  446. const Child = {
  447. props: ['msg'],
  448. template: `<div class="child"><slot msg="from slot"></slot></div>`
  449. }
  450. const app = createApp({
  451. components: { Child },
  452. template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
  453. })
  454. expect(await render(app)).toBe(
  455. `<div>parent<div class="child">` +
  456. `<!--[--><span>from slot</span><!--]-->` +
  457. `</div></div>`
  458. )
  459. })
  460. test('nested render fn components with template slots', async () => {
  461. const Child = {
  462. props: ['msg'],
  463. render(this: any) {
  464. return h(
  465. 'div',
  466. {
  467. class: 'child'
  468. },
  469. this.$slots.default({ msg: 'from slot' })
  470. )
  471. }
  472. }
  473. const app = createApp({
  474. template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
  475. })
  476. app.component('Child', Child)
  477. expect(await render(app)).toBe(
  478. `<div>parent<div class="child">` +
  479. // no comment anchors because slot is used directly as element children
  480. `<span>from slot</span>` +
  481. `</div></div>`
  482. )
  483. })
  484. test('template slots forwarding', async () => {
  485. const Child = {
  486. template: `<div><slot/></div>`
  487. }
  488. const Parent = {
  489. components: { Child },
  490. template: `<Child><slot/></Child>`
  491. }
  492. const app = createApp({
  493. components: { Parent },
  494. template: `<Parent>hello</Parent>`
  495. })
  496. expect(await render(app)).toBe(
  497. `<div><!--[--><!--[-->hello<!--]--><!--]--></div>`
  498. )
  499. })
  500. test('template slots forwarding, empty slot', async () => {
  501. const Child = {
  502. template: `<div><slot/></div>`
  503. }
  504. const Parent = {
  505. components: { Child },
  506. template: `<Child><slot/></Child>`
  507. }
  508. const app = createApp({
  509. components: { Parent },
  510. template: `<Parent></Parent>`
  511. })
  512. expect(await render(app)).toBe(
  513. // should only have a single fragment
  514. `<div><!--[--><!--]--></div>`
  515. )
  516. })
  517. test('template slots forwarding, empty slot w/ fallback', async () => {
  518. const Child = {
  519. template: `<div><slot>fallback</slot></div>`
  520. }
  521. const Parent = {
  522. components: { Child },
  523. template: `<Child><slot/></Child>`
  524. }
  525. const app = createApp({
  526. components: { Parent },
  527. template: `<Parent></Parent>`
  528. })
  529. expect(await render(app)).toBe(
  530. // should only have a single fragment
  531. `<div><!--[-->fallback<!--]--></div>`
  532. )
  533. })
  534. })
  535. describe('vnode element', () => {
  536. test('props', async () => {
  537. expect(
  538. await render(h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello'))
  539. ).toBe(`<div id="foo&amp;" class="bar baz">hello</div>`)
  540. })
  541. test('text children', async () => {
  542. expect(await render(h('div', 'hello'))).toBe(`<div>hello</div>`)
  543. })
  544. test('array children', async () => {
  545. expect(
  546. await render(
  547. h('div', [
  548. 'foo',
  549. h('span', 'bar'),
  550. [h('span', 'baz')],
  551. createCommentVNode('qux')
  552. ])
  553. )
  554. ).toBe(
  555. `<div>foo<span>bar</span><!--[--><span>baz</span><!--]--><!--qux--></div>`
  556. )
  557. })
  558. test('void elements', async () => {
  559. expect(await render(h('input'))).toBe(`<input>`)
  560. })
  561. test('innerHTML', async () => {
  562. expect(
  563. await render(
  564. h(
  565. 'div',
  566. {
  567. innerHTML: `<span>hello</span>`
  568. },
  569. 'ignored'
  570. )
  571. )
  572. ).toBe(`<div><span>hello</span></div>`)
  573. })
  574. test('textContent', async () => {
  575. expect(
  576. await render(
  577. h(
  578. 'div',
  579. {
  580. textContent: `<span>hello</span>`
  581. },
  582. 'ignored'
  583. )
  584. )
  585. ).toBe(`<div>${escapeHtml(`<span>hello</span>`)}</div>`)
  586. })
  587. test('textarea value', async () => {
  588. expect(
  589. await render(
  590. h(
  591. 'textarea',
  592. {
  593. value: `<span>hello</span>`
  594. },
  595. 'ignored'
  596. )
  597. )
  598. ).toBe(`<textarea>${escapeHtml(`<span>hello</span>`)}</textarea>`)
  599. })
  600. })
  601. describe('vnode component', () => {
  602. test('KeepAlive', async () => {
  603. const MyComp = {
  604. render: () => h('p', 'hello')
  605. }
  606. expect(await render(h(KeepAlive, () => h(MyComp)))).toBe(
  607. `<!--[--><p>hello</p><!--]-->`
  608. )
  609. })
  610. test('Transition', async () => {
  611. const MyComp = {
  612. render: () => h('p', 'hello')
  613. }
  614. expect(await render(h(Transition, () => h(MyComp)))).toBe(
  615. `<p>hello</p>`
  616. )
  617. })
  618. })
  619. describe('raw vnode types', () => {
  620. test('Text', async () => {
  621. expect(await render(createTextVNode('hello <div>'))).toBe(
  622. `hello &lt;div&gt;`
  623. )
  624. })
  625. test('Comment', async () => {
  626. // https://www.w3.org/TR/html52/syntax.html#comments
  627. expect(
  628. await render(
  629. h('div', [
  630. createCommentVNode('>foo'),
  631. createCommentVNode('->foo'),
  632. createCommentVNode('<!--foo-->'),
  633. createCommentVNode('--!>foo<!-')
  634. ])
  635. )
  636. ).toBe(`<div><!--foo--><!--foo--><!--foo--><!--foo--></div>`)
  637. })
  638. test('Static', async () => {
  639. const content = `<div id="ok">hello<span>world</span></div>`
  640. expect(await render(createStaticVNode(content, 1))).toBe(content)
  641. })
  642. })
  643. describe('scopeId', () => {
  644. // note: here we are only testing scopeId handling for vdom serialization.
  645. // compiled srr render functions will include scopeId directly in strings.
  646. test('basic', async () => {
  647. const Foo = {
  648. __scopeId: 'data-v-test',
  649. render() {
  650. return h('div')
  651. }
  652. }
  653. expect(await render(h(Foo))).toBe(`<div data-v-test></div>`)
  654. })
  655. test('with client-compiled vnode slots', async () => {
  656. const Child = {
  657. __scopeId: 'data-v-child',
  658. render: function (this: any) {
  659. return h('div', null, [renderSlot(this.$slots, 'default')])
  660. }
  661. }
  662. const Parent = {
  663. __scopeId: 'data-v-test',
  664. render: () => {
  665. return h(Child, null, {
  666. default: withCtx(() => [h('span', 'slot')])
  667. })
  668. }
  669. }
  670. expect(await render(h(Parent))).toBe(
  671. `<div data-v-child data-v-test>` +
  672. `<!--[--><span data-v-test data-v-child-s>slot</span><!--]-->` +
  673. `</div>`
  674. )
  675. })
  676. })
  677. describe('integration w/ compiled template', () => {
  678. test('render', async () => {
  679. expect(
  680. await render(
  681. createApp({
  682. data() {
  683. return { msg: 'hello' }
  684. },
  685. template: `<div>{{ msg }}</div>`
  686. })
  687. )
  688. ).toBe(`<div>hello</div>`)
  689. })
  690. test('handle compiler errors', async () => {
  691. await render(
  692. // render different content since compilation is cached
  693. createApp({ template: `<${type === 'renderToString' ? 'div' : 'p'}` })
  694. )
  695. expect(
  696. `Template compilation error: Unexpected EOF in tag.`
  697. ).toHaveBeenWarned()
  698. expect(`Element is missing end tag`).toHaveBeenWarned()
  699. })
  700. })
  701. test('serverPrefetch', async () => {
  702. const msg = Promise.resolve('hello')
  703. const app = createApp({
  704. data() {
  705. return {
  706. msg: ''
  707. }
  708. },
  709. async serverPrefetch() {
  710. this.msg = await msg
  711. },
  712. render() {
  713. return h('div', this.msg)
  714. }
  715. })
  716. const html = await render(app)
  717. expect(html).toBe(`<div>hello</div>`)
  718. })
  719. // #2763
  720. test('error handling w/ async setup', async () => {
  721. const fn = jest.fn()
  722. const fn2 = jest.fn()
  723. const asyncChildren = defineComponent({
  724. async setup() {
  725. return Promise.reject('async child error')
  726. },
  727. template: `<div>asyncChildren</div>`
  728. })
  729. const app = createApp({
  730. name: 'App',
  731. components: {
  732. asyncChildren
  733. },
  734. template: `<div class="app"><async-children /></div>`,
  735. errorCaptured(error) {
  736. fn(error)
  737. }
  738. })
  739. app.config.errorHandler = error => {
  740. fn2(error)
  741. }
  742. const html = await renderToString(app)
  743. expect(html).toBe(`<div class="app"><div>asyncChildren</div></div>`)
  744. expect(fn).toHaveBeenCalledTimes(1)
  745. expect(fn).toBeCalledWith('async child error')
  746. expect(fn2).toHaveBeenCalledTimes(1)
  747. expect(fn2).toBeCalledWith('async child error')
  748. })
  749. // https://github.com/vuejs/vue-next/issues/3322
  750. test('effect onInvalidate does not error', async () => {
  751. const noop = () => {}
  752. const app = createApp({
  753. setup: () => {
  754. watchEffect(onInvalidate => onInvalidate(noop))
  755. },
  756. render: noop
  757. })
  758. expect(await render(app)).toBe('<!---->')
  759. })
  760. // #2863
  761. test('assets should be resolved correctly', async () => {
  762. expect(
  763. await render(
  764. createApp({
  765. components: {
  766. A: {
  767. ssrRender(_ctx, _push) {
  768. _push(`<div>A</div>`)
  769. }
  770. },
  771. B: {
  772. render: () => h('div', 'B')
  773. }
  774. },
  775. ssrRender(_ctx, _push, _parent) {
  776. const A: any = resolveComponent('A')
  777. _push(ssrRenderComponent(A, null, null, _parent))
  778. ssrRenderVNode(
  779. _push,
  780. createVNode(resolveDynamicComponent('B'), null, null),
  781. _parent
  782. )
  783. }
  784. })
  785. )
  786. ).toBe(`<div>A</div><div>B</div>`)
  787. })
  788. test('onServerPrefetch', async () => {
  789. const msg = Promise.resolve('hello')
  790. const app = createApp({
  791. setup() {
  792. const message = ref('')
  793. onServerPrefetch(async () => {
  794. message.value = await msg
  795. })
  796. return {
  797. message
  798. }
  799. },
  800. render() {
  801. return h('div', this.message)
  802. }
  803. })
  804. const html = await render(app)
  805. expect(html).toBe(`<div>hello</div>`)
  806. })
  807. test('multiple onServerPrefetch', async () => {
  808. const msg = Promise.resolve('hello')
  809. const msg2 = Promise.resolve('hi')
  810. const msg3 = Promise.resolve('bonjour')
  811. const app = createApp({
  812. setup() {
  813. const message = ref('')
  814. const message2 = ref('')
  815. const message3 = ref('')
  816. onServerPrefetch(async () => {
  817. message.value = await msg
  818. })
  819. onServerPrefetch(async () => {
  820. message2.value = await msg2
  821. })
  822. onServerPrefetch(async () => {
  823. message3.value = await msg3
  824. })
  825. return {
  826. message,
  827. message2,
  828. message3
  829. }
  830. },
  831. render() {
  832. return h('div', `${this.message} ${this.message2} ${this.message3}`)
  833. }
  834. })
  835. const html = await render(app)
  836. expect(html).toBe(`<div>hello hi bonjour</div>`)
  837. })
  838. test('onServerPrefetch are run in parallel', async () => {
  839. const first = jest.fn(() => Promise.resolve())
  840. const second = jest.fn(() => Promise.resolve())
  841. let checkOther = [false, false]
  842. let done = [false, false]
  843. const app = createApp({
  844. setup() {
  845. onServerPrefetch(async () => {
  846. checkOther[0] = done[1]
  847. await first()
  848. done[0] = true
  849. })
  850. onServerPrefetch(async () => {
  851. checkOther[1] = done[0]
  852. await second()
  853. done[1] = true
  854. })
  855. },
  856. render() {
  857. return h('div', '')
  858. }
  859. })
  860. await render(app)
  861. expect(first).toHaveBeenCalled()
  862. expect(second).toHaveBeenCalled()
  863. expect(checkOther).toEqual([false, false])
  864. expect(done).toEqual([true, true])
  865. })
  866. test('onServerPrefetch with serverPrefetch option', async () => {
  867. const msg = Promise.resolve('hello')
  868. const msg2 = Promise.resolve('hi')
  869. const app = createApp({
  870. data() {
  871. return {
  872. message: ''
  873. }
  874. },
  875. async serverPrefetch() {
  876. this.message = await msg
  877. },
  878. setup() {
  879. const message2 = ref('')
  880. onServerPrefetch(async () => {
  881. message2.value = await msg2
  882. })
  883. return {
  884. message2
  885. }
  886. },
  887. render() {
  888. return h('div', `${this.message} ${this.message2}`)
  889. }
  890. })
  891. const html = await render(app)
  892. expect(html).toBe(`<div>hello hi</div>`)
  893. })
  894. test('mixed in serverPrefetch', async () => {
  895. const msg = Promise.resolve('hello')
  896. const app = createApp({
  897. data() {
  898. return {
  899. msg: ''
  900. }
  901. },
  902. mixins: [
  903. {
  904. async serverPrefetch() {
  905. this.msg = await msg
  906. }
  907. }
  908. ],
  909. render() {
  910. return h('div', this.msg)
  911. }
  912. })
  913. const html = await render(app)
  914. expect(html).toBe(`<div>hello</div>`)
  915. })
  916. test('many serverPrefetch', async () => {
  917. const foo = Promise.resolve('foo')
  918. const bar = Promise.resolve('bar')
  919. const baz = Promise.resolve('baz')
  920. const app = createApp({
  921. data() {
  922. return {
  923. foo: '',
  924. bar: '',
  925. baz: ''
  926. }
  927. },
  928. mixins: [
  929. {
  930. async serverPrefetch() {
  931. this.foo = await foo
  932. }
  933. },
  934. {
  935. async serverPrefetch() {
  936. this.bar = await bar
  937. }
  938. }
  939. ],
  940. async serverPrefetch() {
  941. this.baz = await baz
  942. },
  943. render() {
  944. return h('div', `${this.foo}${this.bar}${this.baz}`)
  945. }
  946. })
  947. const html = await render(app)
  948. expect(html).toBe(`<div>foobarbaz</div>`)
  949. })
  950. test('onServerPrefetch throwing error', async () => {
  951. let renderError: Error | null = null
  952. let capturedError: Error | null = null
  953. const Child = {
  954. setup() {
  955. onServerPrefetch(async () => {
  956. throw new Error('An error')
  957. })
  958. },
  959. render() {
  960. return h('span')
  961. }
  962. }
  963. const app = createApp({
  964. setup() {
  965. onErrorCaptured(e => {
  966. capturedError = e
  967. return false
  968. })
  969. },
  970. render() {
  971. return h('div', h(Child))
  972. }
  973. })
  974. try {
  975. await render(app)
  976. } catch (e) {
  977. renderError = e
  978. }
  979. expect(renderError).toBe(null)
  980. expect((capturedError as unknown as Error).message).toBe('An error')
  981. })
  982. })
  983. }