render.spec.ts 28 KB

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