render.spec.ts 23 KB

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