render.spec.ts 29 KB

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