render.spec.ts 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088
  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: {
  122. data() {
  123. return { msg: 'hello' }
  124. },
  125. render(this: any) {
  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. {
  141. data() {
  142. return { msg: 'hello' }
  143. },
  144. render(this: any) {
  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(
  619. `<!--[--><p>hello</p><!--]-->`
  620. )
  621. })
  622. test('Transition', async () => {
  623. const MyComp = {
  624. render: () => h('p', 'hello')
  625. }
  626. expect(await render(h(Transition, () => h(MyComp)))).toBe(
  627. `<p>hello</p>`
  628. )
  629. })
  630. })
  631. describe('raw vnode types', () => {
  632. test('Text', async () => {
  633. expect(await render(createTextVNode('hello <div>'))).toBe(
  634. `hello &lt;div&gt;`
  635. )
  636. })
  637. test('Comment', async () => {
  638. // https://www.w3.org/TR/html52/syntax.html#comments
  639. expect(
  640. await render(
  641. h('div', [
  642. createCommentVNode('>foo'),
  643. createCommentVNode('->foo'),
  644. createCommentVNode('<!--foo-->'),
  645. createCommentVNode('--!>foo<!-')
  646. ])
  647. )
  648. ).toBe(`<div><!--foo--><!--foo--><!--foo--><!--foo--></div>`)
  649. })
  650. test('Static', async () => {
  651. const content = `<div id="ok">hello<span>world</span></div>`
  652. expect(await render(createStaticVNode(content, 1))).toBe(content)
  653. })
  654. })
  655. describe('scopeId', () => {
  656. // note: here we are only testing scopeId handling for vdom serialization.
  657. // compiled srr render functions will include scopeId directly in strings.
  658. test('basic', async () => {
  659. const Foo = {
  660. __scopeId: 'data-v-test',
  661. render() {
  662. return h('div')
  663. }
  664. }
  665. expect(await render(h(Foo))).toBe(`<div data-v-test></div>`)
  666. })
  667. test('with client-compiled vnode slots', async () => {
  668. const Child = {
  669. __scopeId: 'data-v-child',
  670. render: function (this: any) {
  671. return h('div', null, [renderSlot(this.$slots, 'default')])
  672. }
  673. }
  674. const Parent = {
  675. __scopeId: 'data-v-test',
  676. render: () => {
  677. return h(Child, null, {
  678. default: withCtx(() => [h('span', 'slot')])
  679. })
  680. }
  681. }
  682. expect(await render(h(Parent))).toBe(
  683. `<div data-v-child data-v-test>` +
  684. `<!--[--><span data-v-test data-v-child-s>slot</span><!--]-->` +
  685. `</div>`
  686. )
  687. })
  688. })
  689. describe('integration w/ compiled template', () => {
  690. test('render', async () => {
  691. expect(
  692. await render(
  693. createApp({
  694. data() {
  695. return { msg: 'hello' }
  696. },
  697. template: `<div>{{ msg }}</div>`
  698. })
  699. )
  700. ).toBe(`<div>hello</div>`)
  701. })
  702. test('handle compiler errors', async () => {
  703. await render(
  704. // render different content since compilation is cached
  705. createApp({ template: `<div>${type}</` })
  706. )
  707. expect(
  708. `Template compilation error: Unexpected EOF in tag.`
  709. ).toHaveBeenWarned()
  710. expect(`Element is missing end tag`).toHaveBeenWarned()
  711. })
  712. })
  713. test('serverPrefetch', async () => {
  714. const msg = Promise.resolve('hello')
  715. const app = createApp({
  716. data() {
  717. return {
  718. msg: ''
  719. }
  720. },
  721. async serverPrefetch() {
  722. this.msg = await msg
  723. },
  724. render() {
  725. return h('div', this.msg)
  726. }
  727. })
  728. const html = await render(app)
  729. expect(html).toBe(`<div>hello</div>`)
  730. })
  731. // #2763
  732. test('error handling w/ async setup', async () => {
  733. const fn = jest.fn()
  734. const fn2 = jest.fn()
  735. const asyncChildren = defineComponent({
  736. async setup() {
  737. return Promise.reject('async child error')
  738. },
  739. template: `<div>asyncChildren</div>`
  740. })
  741. const app = createApp({
  742. name: 'App',
  743. components: {
  744. asyncChildren
  745. },
  746. template: `<div class="app"><async-children /></div>`,
  747. errorCaptured(error) {
  748. fn(error)
  749. }
  750. })
  751. app.config.errorHandler = error => {
  752. fn2(error)
  753. }
  754. const html = await renderToString(app)
  755. expect(html).toBe(`<div class="app"><div>asyncChildren</div></div>`)
  756. expect(fn).toHaveBeenCalledTimes(1)
  757. expect(fn).toBeCalledWith('async child error')
  758. expect(fn2).toHaveBeenCalledTimes(1)
  759. expect(fn2).toBeCalledWith('async child error')
  760. })
  761. // https://github.com/vuejs/vue-next/issues/3322
  762. test('effect onInvalidate does not error', async () => {
  763. const noop = () => {}
  764. const app = createApp({
  765. setup: () => {
  766. watchEffect(onInvalidate => onInvalidate(noop))
  767. },
  768. render: noop
  769. })
  770. expect(await render(app)).toBe('<!---->')
  771. })
  772. // #2863
  773. test('assets should be resolved correctly', async () => {
  774. expect(
  775. await render(
  776. createApp({
  777. components: {
  778. A: {
  779. ssrRender(_ctx, _push) {
  780. _push(`<div>A</div>`)
  781. }
  782. },
  783. B: {
  784. render: () => h('div', 'B')
  785. }
  786. },
  787. ssrRender(_ctx, _push, _parent) {
  788. const A: any = resolveComponent('A')
  789. _push(ssrRenderComponent(A, null, null, _parent))
  790. ssrRenderVNode(
  791. _push,
  792. createVNode(resolveDynamicComponent('B'), null, null),
  793. _parent
  794. )
  795. }
  796. })
  797. )
  798. ).toBe(`<div>A</div><div>B</div>`)
  799. })
  800. test('onServerPrefetch', async () => {
  801. const msg = Promise.resolve('hello')
  802. const app = createApp({
  803. setup() {
  804. const message = ref('')
  805. onServerPrefetch(async () => {
  806. message.value = await msg
  807. })
  808. return {
  809. message
  810. }
  811. },
  812. render() {
  813. return h('div', this.message)
  814. }
  815. })
  816. const html = await render(app)
  817. expect(html).toBe(`<div>hello</div>`)
  818. })
  819. test('multiple onServerPrefetch', async () => {
  820. const msg = Promise.resolve('hello')
  821. const msg2 = Promise.resolve('hi')
  822. const msg3 = Promise.resolve('bonjour')
  823. const app = createApp({
  824. setup() {
  825. const message = ref('')
  826. const message2 = ref('')
  827. const message3 = ref('')
  828. onServerPrefetch(async () => {
  829. message.value = await msg
  830. })
  831. onServerPrefetch(async () => {
  832. message2.value = await msg2
  833. })
  834. onServerPrefetch(async () => {
  835. message3.value = await msg3
  836. })
  837. return {
  838. message,
  839. message2,
  840. message3
  841. }
  842. },
  843. render() {
  844. return h('div', `${this.message} ${this.message2} ${this.message3}`)
  845. }
  846. })
  847. const html = await render(app)
  848. expect(html).toBe(`<div>hello hi bonjour</div>`)
  849. })
  850. test('onServerPrefetch are run in parallel', async () => {
  851. const first = jest.fn(() => Promise.resolve())
  852. const second = jest.fn(() => Promise.resolve())
  853. let checkOther = [false, false]
  854. let done = [false, false]
  855. const app = createApp({
  856. setup() {
  857. onServerPrefetch(async () => {
  858. checkOther[0] = done[1]
  859. await first()
  860. done[0] = true
  861. })
  862. onServerPrefetch(async () => {
  863. checkOther[1] = done[0]
  864. await second()
  865. done[1] = true
  866. })
  867. },
  868. render() {
  869. return h('div', '')
  870. }
  871. })
  872. await render(app)
  873. expect(first).toHaveBeenCalled()
  874. expect(second).toHaveBeenCalled()
  875. expect(checkOther).toEqual([false, false])
  876. expect(done).toEqual([true, true])
  877. })
  878. test('onServerPrefetch with serverPrefetch option', async () => {
  879. const msg = Promise.resolve('hello')
  880. const msg2 = Promise.resolve('hi')
  881. const app = createApp({
  882. data() {
  883. return {
  884. message: ''
  885. }
  886. },
  887. async serverPrefetch() {
  888. this.message = await msg
  889. },
  890. setup() {
  891. const message2 = ref('')
  892. onServerPrefetch(async () => {
  893. message2.value = await msg2
  894. })
  895. return {
  896. message2
  897. }
  898. },
  899. render() {
  900. return h('div', `${this.message} ${this.message2}`)
  901. }
  902. })
  903. const html = await render(app)
  904. expect(html).toBe(`<div>hello hi</div>`)
  905. })
  906. test('mixed in serverPrefetch', async () => {
  907. const msg = Promise.resolve('hello')
  908. const app = createApp({
  909. data() {
  910. return {
  911. msg: ''
  912. }
  913. },
  914. mixins: [
  915. {
  916. async serverPrefetch() {
  917. this.msg = await msg
  918. }
  919. }
  920. ],
  921. render() {
  922. return h('div', this.msg)
  923. }
  924. })
  925. const html = await render(app)
  926. expect(html).toBe(`<div>hello</div>`)
  927. })
  928. test('many serverPrefetch', async () => {
  929. const foo = Promise.resolve('foo')
  930. const bar = Promise.resolve('bar')
  931. const baz = Promise.resolve('baz')
  932. const app = createApp({
  933. data() {
  934. return {
  935. foo: '',
  936. bar: '',
  937. baz: ''
  938. }
  939. },
  940. mixins: [
  941. {
  942. async serverPrefetch() {
  943. this.foo = await foo
  944. }
  945. },
  946. {
  947. async serverPrefetch() {
  948. this.bar = await bar
  949. }
  950. }
  951. ],
  952. async serverPrefetch() {
  953. this.baz = await baz
  954. },
  955. render() {
  956. return h('div', `${this.foo}${this.bar}${this.baz}`)
  957. }
  958. })
  959. const html = await render(app)
  960. expect(html).toBe(`<div>foobarbaz</div>`)
  961. })
  962. test('onServerPrefetch throwing error', async () => {
  963. let renderError: Error | null = null
  964. let capturedError: Error | null = null
  965. const Child = {
  966. setup() {
  967. onServerPrefetch(async () => {
  968. throw new Error('An error')
  969. })
  970. },
  971. render() {
  972. return h('span')
  973. }
  974. }
  975. const app = createApp({
  976. setup() {
  977. onErrorCaptured(e => {
  978. capturedError = e
  979. return false
  980. })
  981. },
  982. render() {
  983. return h('div', h(Child))
  984. }
  985. })
  986. try {
  987. await render(app)
  988. } catch (e: any) {
  989. renderError = e
  990. }
  991. expect(renderError).toBe(null)
  992. expect((capturedError as unknown as Error).message).toBe('An error')
  993. })
  994. })
  995. }