render.spec.ts 35 KB

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