render.spec.ts 35 KB

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