render.spec.ts 30 KB

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