render.spec.ts 21 KB

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