render.spec.ts 28 KB

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