render.spec.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863
  1. import {
  2. createApp,
  3. h,
  4. createCommentVNode,
  5. resolveComponent,
  6. ComponentOptions,
  7. ref,
  8. defineComponent,
  9. createTextVNode,
  10. createStaticVNode,
  11. withCtx,
  12. KeepAlive,
  13. Transition,
  14. watchEffect,
  15. createVNode,
  16. resolveDynamicComponent,
  17. renderSlot
  18. } from 'vue'
  19. import { escapeHtml } from '@vue/shared'
  20. import { renderToString } from '../src/renderToString'
  21. import { renderToStream as _renderToStream } from '../src/renderToStream'
  22. import { ssrRenderSlot, SSRSlot } from '../src/helpers/ssrRenderSlot'
  23. import { ssrRenderComponent } from '../src/helpers/ssrRenderComponent'
  24. import { Readable } from 'stream'
  25. import { ssrRenderVNode } from '../src'
  26. const promisifyStream = (stream: Readable) => {
  27. return new Promise<string>((resolve, reject) => {
  28. let result = ''
  29. stream.on('data', data => {
  30. result += data
  31. })
  32. stream.on('error', () => {
  33. reject(result)
  34. })
  35. stream.on('end', () => {
  36. resolve(result)
  37. })
  38. })
  39. }
  40. const renderToStream = (app: any, context?: any) =>
  41. promisifyStream(_renderToStream(app, context))
  42. // we run the same tests twice, once for renderToString, once for renderToStream
  43. testRender(`renderToString`, renderToString)
  44. testRender(`renderToStream`, renderToStream)
  45. function testRender(type: string, render: typeof renderToString) {
  46. describe(`ssr: ${type}`, () => {
  47. test('should apply app context', async () => {
  48. const app = createApp({
  49. render() {
  50. const Foo = resolveComponent('foo') as ComponentOptions
  51. return h(Foo)
  52. }
  53. })
  54. app.component('foo', {
  55. render: () => h('div', 'foo')
  56. })
  57. const html = await render(app)
  58. expect(html).toBe(`<div>foo</div>`)
  59. })
  60. describe('components', () => {
  61. test('vnode components', async () => {
  62. expect(
  63. await render(
  64. createApp({
  65. data() {
  66. return { msg: 'hello' }
  67. },
  68. render(this: any) {
  69. return h('div', this.msg)
  70. }
  71. })
  72. )
  73. ).toBe(`<div>hello</div>`)
  74. })
  75. test('option components returning render from setup', async () => {
  76. expect(
  77. await render(
  78. createApp({
  79. setup() {
  80. const msg = ref('hello')
  81. return () => h('div', msg.value)
  82. }
  83. })
  84. )
  85. ).toBe(`<div>hello</div>`)
  86. })
  87. test('setup components returning render from setup', async () => {
  88. expect(
  89. await render(
  90. createApp(
  91. defineComponent(() => {
  92. const msg = ref('hello')
  93. return () => h('div', msg.value)
  94. })
  95. )
  96. )
  97. ).toBe(`<div>hello</div>`)
  98. })
  99. test('components using defineComponent with extends option', async () => {
  100. expect(
  101. await render(
  102. createApp(
  103. defineComponent({
  104. extends: {
  105. data() {
  106. return { msg: 'hello' }
  107. },
  108. render(this: any) {
  109. return h('div', this.msg)
  110. }
  111. }
  112. })
  113. )
  114. )
  115. ).toBe(`<div>hello</div>`)
  116. })
  117. test('components using defineComponent with mixins option', async () => {
  118. expect(
  119. await render(
  120. createApp(
  121. defineComponent({
  122. mixins: [
  123. {
  124. data() {
  125. return { msg: 'hello' }
  126. },
  127. render(this: any) {
  128. return h('div', this.msg)
  129. }
  130. }
  131. ]
  132. })
  133. )
  134. )
  135. ).toBe(`<div>hello</div>`)
  136. })
  137. test('optimized components', async () => {
  138. expect(
  139. await render(
  140. createApp({
  141. data() {
  142. return { msg: 'hello' }
  143. },
  144. ssrRender(ctx, push) {
  145. push(`<div>${ctx.msg}</div>`)
  146. }
  147. })
  148. )
  149. ).toBe(`<div>hello</div>`)
  150. })
  151. test('nested vnode components', async () => {
  152. const Child = {
  153. props: ['msg'],
  154. render(this: any) {
  155. return h('div', this.msg)
  156. }
  157. }
  158. expect(
  159. await render(
  160. createApp({
  161. render() {
  162. return h('div', ['parent', h(Child, { msg: 'hello' })])
  163. }
  164. })
  165. )
  166. ).toBe(`<div>parent<div>hello</div></div>`)
  167. })
  168. test('nested optimized components', async () => {
  169. const Child = {
  170. props: ['msg'],
  171. ssrRender(ctx: any, push: any) {
  172. push(`<div>${ctx.msg}</div>`)
  173. }
  174. }
  175. expect(
  176. await render(
  177. createApp({
  178. ssrRender(_ctx, push, parent) {
  179. push(`<div>parent`)
  180. push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent))
  181. push(`</div>`)
  182. }
  183. })
  184. )
  185. ).toBe(`<div>parent<div>hello</div></div>`)
  186. })
  187. test('nested template components', async () => {
  188. const Child = {
  189. props: ['msg'],
  190. template: `<div>{{ msg }}</div>`
  191. }
  192. const app = createApp({
  193. template: `<div>parent<Child msg="hello" /></div>`
  194. })
  195. app.component('Child', Child)
  196. expect(await render(app)).toBe(`<div>parent<div>hello</div></div>`)
  197. })
  198. test('template components with dynamic class attribute after static', async () => {
  199. const app = createApp({
  200. template: `<div><div class="child" :class="'dynamic'"></div></div>`
  201. })
  202. expect(await render(app)).toBe(
  203. `<div><div class="dynamic child"></div></div>`
  204. )
  205. })
  206. test('template components with dynamic class attribute before static', async () => {
  207. const app = createApp({
  208. template: `<div><div :class="'dynamic'" class="child"></div></div>`
  209. })
  210. expect(await render(app)).toBe(
  211. `<div><div class="dynamic child"></div></div>`
  212. )
  213. })
  214. test('mixing optimized / vnode / template components', async () => {
  215. const OptimizedChild = {
  216. props: ['msg'],
  217. ssrRender(ctx: any, push: any) {
  218. push(`<div>${ctx.msg}</div>`)
  219. }
  220. }
  221. const VNodeChild = {
  222. props: ['msg'],
  223. render(this: any) {
  224. return h('div', this.msg)
  225. }
  226. }
  227. const TemplateChild = {
  228. props: ['msg'],
  229. template: `<div>{{ msg }}</div>`
  230. }
  231. expect(
  232. await render(
  233. createApp({
  234. ssrRender(_ctx, push, parent) {
  235. push(`<div>parent`)
  236. push(
  237. ssrRenderComponent(
  238. OptimizedChild,
  239. { msg: 'opt' },
  240. null,
  241. parent
  242. )
  243. )
  244. push(
  245. ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent)
  246. )
  247. push(
  248. ssrRenderComponent(
  249. TemplateChild,
  250. { msg: 'template' },
  251. null,
  252. parent
  253. )
  254. )
  255. push(`</div>`)
  256. }
  257. })
  258. )
  259. ).toBe(
  260. `<div>parent<div>opt</div><div>vnode</div><div>template</div></div>`
  261. )
  262. })
  263. test('async components', async () => {
  264. const Child = {
  265. // should wait for resolved render context from setup()
  266. async setup() {
  267. return {
  268. msg: 'hello'
  269. }
  270. },
  271. ssrRender(ctx: any, push: any) {
  272. push(`<div>${ctx.msg}</div>`)
  273. }
  274. }
  275. expect(
  276. await render(
  277. createApp({
  278. ssrRender(_ctx, push, parent) {
  279. push(`<div>parent`)
  280. push(ssrRenderComponent(Child, null, null, parent))
  281. push(`</div>`)
  282. }
  283. })
  284. )
  285. ).toBe(`<div>parent<div>hello</div></div>`)
  286. })
  287. test('parallel async components', async () => {
  288. const OptimizedChild = {
  289. props: ['msg'],
  290. async setup(props: any) {
  291. return {
  292. localMsg: props.msg + '!'
  293. }
  294. },
  295. ssrRender(ctx: any, push: any) {
  296. push(`<div>${ctx.localMsg}</div>`)
  297. }
  298. }
  299. const VNodeChild = {
  300. props: ['msg'],
  301. async setup(props: any) {
  302. return {
  303. localMsg: props.msg + '!'
  304. }
  305. },
  306. render(this: any) {
  307. return h('div', this.localMsg)
  308. }
  309. }
  310. expect(
  311. await render(
  312. createApp({
  313. ssrRender(_ctx, push, parent) {
  314. push(`<div>parent`)
  315. push(
  316. ssrRenderComponent(
  317. OptimizedChild,
  318. { msg: 'opt' },
  319. null,
  320. parent
  321. )
  322. )
  323. push(
  324. ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent)
  325. )
  326. push(`</div>`)
  327. }
  328. })
  329. )
  330. ).toBe(`<div>parent<div>opt!</div><div>vnode!</div></div>`)
  331. })
  332. })
  333. describe('slots', () => {
  334. test('nested components with optimized slots', async () => {
  335. const Child = {
  336. props: ['msg'],
  337. ssrRender(ctx: any, push: any, parent: any) {
  338. push(`<div class="child">`)
  339. ssrRenderSlot(
  340. ctx.$slots,
  341. 'default',
  342. { msg: 'from slot' },
  343. () => {
  344. push(`fallback`)
  345. },
  346. push,
  347. parent
  348. )
  349. push(`</div>`)
  350. }
  351. }
  352. expect(
  353. await render(
  354. createApp({
  355. ssrRender(_ctx, push, parent) {
  356. push(`<div>parent`)
  357. push(
  358. ssrRenderComponent(
  359. Child,
  360. { msg: 'hello' },
  361. {
  362. // optimized slot using string push
  363. default: (({ msg }, push, _p) => {
  364. push(`<span>${msg}</span>`)
  365. }) as SSRSlot,
  366. // important to avoid slots being normalized
  367. _: 1 as any
  368. },
  369. parent
  370. )
  371. )
  372. push(`</div>`)
  373. }
  374. })
  375. )
  376. ).toBe(
  377. `<div>parent<div class="child">` +
  378. `<!--[--><span>from slot</span><!--]-->` +
  379. `</div></div>`
  380. )
  381. // test fallback
  382. expect(
  383. await render(
  384. createApp({
  385. ssrRender(_ctx, push, parent) {
  386. push(`<div>parent`)
  387. push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent))
  388. push(`</div>`)
  389. }
  390. })
  391. )
  392. ).toBe(
  393. `<div>parent<div class="child"><!--[-->fallback<!--]--></div></div>`
  394. )
  395. })
  396. test('nested components with vnode slots', async () => {
  397. const Child = {
  398. props: ['msg'],
  399. ssrRender(ctx: any, push: any, parent: any) {
  400. push(`<div class="child">`)
  401. ssrRenderSlot(
  402. ctx.$slots,
  403. 'default',
  404. { msg: 'from slot' },
  405. null,
  406. push,
  407. parent
  408. )
  409. push(`</div>`)
  410. }
  411. }
  412. expect(
  413. await render(
  414. createApp({
  415. ssrRender(_ctx, push, parent) {
  416. push(`<div>parent`)
  417. push(
  418. ssrRenderComponent(
  419. Child,
  420. { msg: 'hello' },
  421. {
  422. // bailed slots returning raw vnodes
  423. default: ({ msg }: any) => {
  424. return h('span', msg)
  425. }
  426. },
  427. parent
  428. )
  429. )
  430. push(`</div>`)
  431. }
  432. })
  433. )
  434. ).toBe(
  435. `<div>parent<div class="child">` +
  436. `<!--[--><span>from slot</span><!--]-->` +
  437. `</div></div>`
  438. )
  439. })
  440. test('nested components with template slots', async () => {
  441. const Child = {
  442. props: ['msg'],
  443. template: `<div class="child"><slot msg="from slot"></slot></div>`
  444. }
  445. const app = createApp({
  446. components: { Child },
  447. template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
  448. })
  449. expect(await render(app)).toBe(
  450. `<div>parent<div class="child">` +
  451. `<!--[--><span>from slot</span><!--]-->` +
  452. `</div></div>`
  453. )
  454. })
  455. test('nested render fn components with template slots', async () => {
  456. const Child = {
  457. props: ['msg'],
  458. render(this: any) {
  459. return h(
  460. 'div',
  461. {
  462. class: 'child'
  463. },
  464. this.$slots.default({ msg: 'from slot' })
  465. )
  466. }
  467. }
  468. const app = createApp({
  469. template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
  470. })
  471. app.component('Child', Child)
  472. expect(await render(app)).toBe(
  473. `<div>parent<div class="child">` +
  474. // no comment anchors because slot is used directly as element children
  475. `<span>from slot</span>` +
  476. `</div></div>`
  477. )
  478. })
  479. test('template slots forwarding', async () => {
  480. const Child = {
  481. template: `<div><slot/></div>`
  482. }
  483. const Parent = {
  484. components: { Child },
  485. template: `<Child><slot/></Child>`
  486. }
  487. const app = createApp({
  488. components: { Parent },
  489. template: `<Parent>hello</Parent>`
  490. })
  491. expect(await render(app)).toBe(
  492. `<div><!--[--><!--[-->hello<!--]--><!--]--></div>`
  493. )
  494. })
  495. test('template slots forwarding, empty slot', async () => {
  496. const Child = {
  497. template: `<div><slot/></div>`
  498. }
  499. const Parent = {
  500. components: { Child },
  501. template: `<Child><slot/></Child>`
  502. }
  503. const app = createApp({
  504. components: { Parent },
  505. template: `<Parent></Parent>`
  506. })
  507. expect(await render(app)).toBe(
  508. // should only have a single fragment
  509. `<div><!--[--><!--]--></div>`
  510. )
  511. })
  512. test('template slots forwarding, empty slot w/ fallback', async () => {
  513. const Child = {
  514. template: `<div><slot>fallback</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><!--[-->fallback<!--]--></div>`
  527. )
  528. })
  529. })
  530. describe('vnode element', () => {
  531. test('props', async () => {
  532. expect(
  533. await render(h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello'))
  534. ).toBe(`<div id="foo&amp;" class="bar baz">hello</div>`)
  535. })
  536. test('text children', async () => {
  537. expect(await render(h('div', 'hello'))).toBe(`<div>hello</div>`)
  538. })
  539. test('array children', async () => {
  540. expect(
  541. await render(
  542. h('div', [
  543. 'foo',
  544. h('span', 'bar'),
  545. [h('span', 'baz')],
  546. createCommentVNode('qux')
  547. ])
  548. )
  549. ).toBe(
  550. `<div>foo<span>bar</span><!--[--><span>baz</span><!--]--><!--qux--></div>`
  551. )
  552. })
  553. test('void elements', async () => {
  554. expect(await render(h('input'))).toBe(`<input>`)
  555. })
  556. test('innerHTML', async () => {
  557. expect(
  558. await render(
  559. h(
  560. 'div',
  561. {
  562. innerHTML: `<span>hello</span>`
  563. },
  564. 'ignored'
  565. )
  566. )
  567. ).toBe(`<div><span>hello</span></div>`)
  568. })
  569. test('textContent', async () => {
  570. expect(
  571. await render(
  572. h(
  573. 'div',
  574. {
  575. textContent: `<span>hello</span>`
  576. },
  577. 'ignored'
  578. )
  579. )
  580. ).toBe(`<div>${escapeHtml(`<span>hello</span>`)}</div>`)
  581. })
  582. test('textarea value', async () => {
  583. expect(
  584. await render(
  585. h(
  586. 'textarea',
  587. {
  588. value: `<span>hello</span>`
  589. },
  590. 'ignored'
  591. )
  592. )
  593. ).toBe(`<textarea>${escapeHtml(`<span>hello</span>`)}</textarea>`)
  594. })
  595. })
  596. describe('vnode component', () => {
  597. test('KeepAlive', async () => {
  598. const MyComp = {
  599. render: () => h('p', 'hello')
  600. }
  601. expect(await render(h(KeepAlive, () => h(MyComp)))).toBe(
  602. `<!--[--><p>hello</p><!--]-->`
  603. )
  604. })
  605. test('Transition', async () => {
  606. const MyComp = {
  607. render: () => h('p', 'hello')
  608. }
  609. expect(await render(h(Transition, () => h(MyComp)))).toBe(
  610. `<p>hello</p>`
  611. )
  612. })
  613. })
  614. describe('raw vnode types', () => {
  615. test('Text', async () => {
  616. expect(await render(createTextVNode('hello <div>'))).toBe(
  617. `hello &lt;div&gt;`
  618. )
  619. })
  620. test('Comment', async () => {
  621. // https://www.w3.org/TR/html52/syntax.html#comments
  622. expect(
  623. await render(
  624. h('div', [
  625. createCommentVNode('>foo'),
  626. createCommentVNode('->foo'),
  627. createCommentVNode('<!--foo-->'),
  628. createCommentVNode('--!>foo<!-')
  629. ])
  630. )
  631. ).toBe(`<div><!--foo--><!--foo--><!--foo--><!--foo--></div>`)
  632. })
  633. test('Static', async () => {
  634. const content = `<div id="ok">hello<span>world</span></div>`
  635. expect(await render(createStaticVNode(content, 1))).toBe(content)
  636. })
  637. })
  638. describe('scopeId', () => {
  639. // note: here we are only testing scopeId handling for vdom serialization.
  640. // compiled srr render functions will include scopeId directly in strings.
  641. test('basic', async () => {
  642. const Foo = {
  643. __scopeId: 'data-v-test',
  644. render() {
  645. return h('div')
  646. }
  647. }
  648. expect(await render(h(Foo))).toBe(`<div data-v-test></div>`)
  649. })
  650. test('with client-compiled vnode slots', async () => {
  651. const Child = {
  652. __scopeId: 'data-v-child',
  653. render: function(this: any) {
  654. return h('div', null, [renderSlot(this.$slots, 'default')])
  655. }
  656. }
  657. const Parent = {
  658. __scopeId: 'data-v-test',
  659. render: () => {
  660. return h(Child, null, {
  661. default: withCtx(() => [h('span', 'slot')])
  662. })
  663. }
  664. }
  665. expect(await render(h(Parent))).toBe(
  666. `<div data-v-child data-v-test>` +
  667. `<!--[--><span data-v-test data-v-child-s>slot</span><!--]-->` +
  668. `</div>`
  669. )
  670. })
  671. })
  672. describe('integration w/ compiled template', () => {
  673. test('render', async () => {
  674. expect(
  675. await render(
  676. createApp({
  677. data() {
  678. return { msg: 'hello' }
  679. },
  680. template: `<div>{{ msg }}</div>`
  681. })
  682. )
  683. ).toBe(`<div>hello</div>`)
  684. })
  685. test('handle compiler errors', async () => {
  686. await render(
  687. // render different content since compilation is cached
  688. createApp({ template: `<${type === 'renderToString' ? 'div' : 'p'}` })
  689. )
  690. expect(
  691. `Template compilation error: Unexpected EOF in tag.`
  692. ).toHaveBeenWarned()
  693. expect(`Element is missing end tag`).toHaveBeenWarned()
  694. })
  695. })
  696. test('serverPrefetch', async () => {
  697. const msg = Promise.resolve('hello')
  698. const app = createApp({
  699. data() {
  700. return {
  701. msg: ''
  702. }
  703. },
  704. async serverPrefetch() {
  705. this.msg = await msg
  706. },
  707. render() {
  708. return h('div', this.msg)
  709. }
  710. })
  711. const html = await render(app)
  712. expect(html).toBe(`<div>hello</div>`)
  713. })
  714. // #2763
  715. test('error handling w/ async setup', async () => {
  716. const fn = jest.fn()
  717. const fn2 = jest.fn()
  718. const asyncChildren = defineComponent({
  719. async setup() {
  720. return Promise.reject('async child error')
  721. },
  722. template: `<div>asyncChildren</div>`
  723. })
  724. const app = createApp({
  725. name: 'App',
  726. components: {
  727. asyncChildren
  728. },
  729. template: `<div class="app"><async-children /></div>`,
  730. errorCaptured(error) {
  731. fn(error)
  732. }
  733. })
  734. app.config.errorHandler = error => {
  735. fn2(error)
  736. }
  737. const html = await renderToString(app)
  738. expect(html).toBe(`<div class="app"><div>asyncChildren</div></div>`)
  739. expect(fn).toHaveBeenCalledTimes(1)
  740. expect(fn).toBeCalledWith('async child error')
  741. expect(fn2).toHaveBeenCalledTimes(1)
  742. expect(fn2).toBeCalledWith('async child error')
  743. })
  744. // https://github.com/vuejs/vue-next/issues/3322
  745. test('effect onInvalidate does not error', async () => {
  746. const noop = () => {}
  747. const app = createApp({
  748. setup: () => {
  749. watchEffect(onInvalidate => onInvalidate(noop))
  750. },
  751. render: noop
  752. })
  753. expect(await render(app)).toBe('<!---->')
  754. })
  755. // #2863
  756. test('assets should be resolved correctly', async () => {
  757. expect(
  758. await render(
  759. createApp({
  760. components: {
  761. A: {
  762. ssrRender(_ctx, _push) {
  763. _push(`<div>A</div>`)
  764. }
  765. },
  766. B: {
  767. render: () => h('div', 'B')
  768. }
  769. },
  770. ssrRender(_ctx, _push, _parent) {
  771. const A: any = resolveComponent('A')
  772. _push(ssrRenderComponent(A, null, null, _parent))
  773. ssrRenderVNode(
  774. _push,
  775. createVNode(resolveDynamicComponent('B'), null, null),
  776. _parent
  777. )
  778. }
  779. })
  780. )
  781. ).toBe(`<div>A</div><div>B</div>`)
  782. })
  783. })
  784. }