render.spec.ts 33 KB

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