render.spec.ts 20 KB

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