render.spec.ts 33 KB

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