render.spec.ts 29 KB

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