render.spec.ts 31 KB

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