render.spec.ts 31 KB

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