renderToString.spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. import {
  2. createApp,
  3. h,
  4. createCommentVNode,
  5. withScopeId,
  6. resolveComponent,
  7. ComponentOptions,
  8. ref,
  9. defineComponent,
  10. createTextVNode,
  11. createStaticVNode
  12. } from 'vue'
  13. import { escapeHtml, mockWarn } from '@vue/shared'
  14. import { renderToString, renderComponent } from '../src/renderToString'
  15. import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot'
  16. mockWarn()
  17. describe('ssr: renderToString', () => {
  18. test('should apply app context', async () => {
  19. const app = createApp({
  20. render() {
  21. const Foo = resolveComponent('foo') as ComponentOptions
  22. return h(Foo)
  23. }
  24. })
  25. app.component('foo', {
  26. render: () => h('div', 'foo')
  27. })
  28. const html = await renderToString(app)
  29. expect(html).toBe(`<div>foo</div>`)
  30. })
  31. describe('components', () => {
  32. test('vnode components', async () => {
  33. expect(
  34. await renderToString(
  35. createApp({
  36. data() {
  37. return { msg: 'hello' }
  38. },
  39. render(this: any) {
  40. return h('div', this.msg)
  41. }
  42. })
  43. )
  44. ).toBe(`<div>hello</div>`)
  45. })
  46. test('option components returning render from setup', async () => {
  47. expect(
  48. await renderToString(
  49. createApp({
  50. setup() {
  51. const msg = ref('hello')
  52. return () => h('div', msg.value)
  53. }
  54. })
  55. )
  56. ).toBe(`<div>hello</div>`)
  57. })
  58. test('setup components returning render from setup', async () => {
  59. expect(
  60. await renderToString(
  61. createApp(
  62. defineComponent((props: {}) => {
  63. const msg = ref('hello')
  64. return () => h('div', msg.value)
  65. })
  66. )
  67. )
  68. ).toBe(`<div>hello</div>`)
  69. })
  70. test('optimized components', async () => {
  71. expect(
  72. await renderToString(
  73. createApp({
  74. data() {
  75. return { msg: 'hello' }
  76. },
  77. ssrRender(ctx, push) {
  78. push(`<div>${ctx.msg}</div>`)
  79. }
  80. })
  81. )
  82. ).toBe(`<div>hello</div>`)
  83. })
  84. describe('template components', () => {
  85. test('render', async () => {
  86. expect(
  87. await renderToString(
  88. createApp({
  89. data() {
  90. return { msg: 'hello' }
  91. },
  92. template: `<div>{{ msg }}</div>`
  93. })
  94. )
  95. ).toBe(`<div>hello</div>`)
  96. })
  97. test('handle compiler errors', async () => {
  98. await renderToString(createApp({ template: `<` }))
  99. expect(
  100. 'Template compilation error: Unexpected EOF in tag.\n' +
  101. '1 | <\n' +
  102. ' | ^'
  103. ).toHaveBeenWarned()
  104. })
  105. })
  106. test('nested vnode components', async () => {
  107. const Child = {
  108. props: ['msg'],
  109. render(this: any) {
  110. return h('div', this.msg)
  111. }
  112. }
  113. expect(
  114. await renderToString(
  115. createApp({
  116. render() {
  117. return h('div', ['parent', h(Child, { msg: 'hello' })])
  118. }
  119. })
  120. )
  121. ).toBe(`<div>parent<div>hello</div></div>`)
  122. })
  123. test('nested optimized components', async () => {
  124. const Child = {
  125. props: ['msg'],
  126. ssrRender(ctx: any, push: any) {
  127. push(`<div>${ctx.msg}</div>`)
  128. }
  129. }
  130. expect(
  131. await renderToString(
  132. createApp({
  133. ssrRender(_ctx, push, parent) {
  134. push(`<div>parent`)
  135. push(renderComponent(Child, { msg: 'hello' }, null, parent))
  136. push(`</div>`)
  137. }
  138. })
  139. )
  140. ).toBe(`<div>parent<div>hello</div></div>`)
  141. })
  142. test('nested template components', async () => {
  143. const Child = {
  144. props: ['msg'],
  145. template: `<div>{{ msg }}</div>`
  146. }
  147. const app = createApp({
  148. template: `<div>parent<Child msg="hello" /></div>`
  149. })
  150. app.component('Child', Child)
  151. expect(await renderToString(app)).toBe(
  152. `<div>parent<div>hello</div></div>`
  153. )
  154. })
  155. test('mixing optimized / vnode / template components', async () => {
  156. const OptimizedChild = {
  157. props: ['msg'],
  158. ssrRender(ctx: any, push: any) {
  159. push(`<div>${ctx.msg}</div>`)
  160. }
  161. }
  162. const VNodeChild = {
  163. props: ['msg'],
  164. render(this: any) {
  165. return h('div', this.msg)
  166. }
  167. }
  168. const TemplateChild = {
  169. props: ['msg'],
  170. template: `<div>{{ msg }}</div>`
  171. }
  172. expect(
  173. await renderToString(
  174. createApp({
  175. ssrRender(_ctx, push, parent) {
  176. push(`<div>parent`)
  177. push(
  178. renderComponent(OptimizedChild, { msg: 'opt' }, null, parent)
  179. )
  180. push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent))
  181. push(
  182. renderComponent(
  183. TemplateChild,
  184. { msg: 'template' },
  185. null,
  186. parent
  187. )
  188. )
  189. push(`</div>`)
  190. }
  191. })
  192. )
  193. ).toBe(
  194. `<div>parent<div>opt</div><div>vnode</div><div>template</div></div>`
  195. )
  196. })
  197. test('nested components with optimized slots', async () => {
  198. const Child = {
  199. props: ['msg'],
  200. ssrRender(ctx: any, push: any, parent: any) {
  201. push(`<div class="child">`)
  202. ssrRenderSlot(
  203. ctx.$slots,
  204. 'default',
  205. { msg: 'from slot' },
  206. () => {
  207. push(`fallback`)
  208. },
  209. push,
  210. parent
  211. )
  212. push(`</div>`)
  213. }
  214. }
  215. expect(
  216. await renderToString(
  217. createApp({
  218. ssrRender(_ctx, push, parent) {
  219. push(`<div>parent`)
  220. push(
  221. renderComponent(
  222. Child,
  223. { msg: 'hello' },
  224. {
  225. // optimized slot using string push
  226. default: ({ msg }: any, push: any, p: any) => {
  227. push(`<span>${msg}</span>`)
  228. },
  229. // important to avoid slots being normalized
  230. _: 1 as any
  231. },
  232. parent
  233. )
  234. )
  235. push(`</div>`)
  236. }
  237. })
  238. )
  239. ).toBe(
  240. `<div>parent<div class="child">` +
  241. `<!--[--><span>from slot</span><!--]-->` +
  242. `</div></div>`
  243. )
  244. // test fallback
  245. expect(
  246. await renderToString(
  247. createApp({
  248. ssrRender(_ctx, push, parent) {
  249. push(`<div>parent`)
  250. push(renderComponent(Child, { msg: 'hello' }, null, parent))
  251. push(`</div>`)
  252. }
  253. })
  254. )
  255. ).toBe(
  256. `<div>parent<div class="child"><!--[-->fallback<!--]--></div></div>`
  257. )
  258. })
  259. test('nested components with vnode slots', async () => {
  260. const Child = {
  261. props: ['msg'],
  262. ssrRender(ctx: any, push: any, parent: any) {
  263. push(`<div class="child">`)
  264. ssrRenderSlot(
  265. ctx.$slots,
  266. 'default',
  267. { msg: 'from slot' },
  268. null,
  269. push,
  270. parent
  271. )
  272. push(`</div>`)
  273. }
  274. }
  275. expect(
  276. await renderToString(
  277. createApp({
  278. ssrRender(_ctx, push, parent) {
  279. push(`<div>parent`)
  280. push(
  281. renderComponent(
  282. Child,
  283. { msg: 'hello' },
  284. {
  285. // bailed slots returning raw vnodes
  286. default: ({ msg }: any) => {
  287. return h('span', msg)
  288. }
  289. },
  290. parent
  291. )
  292. )
  293. push(`</div>`)
  294. }
  295. })
  296. )
  297. ).toBe(
  298. `<div>parent<div class="child">` +
  299. `<!--[--><span>from slot</span><!--]-->` +
  300. `</div></div>`
  301. )
  302. })
  303. test('nested components with template slots', async () => {
  304. const Child = {
  305. props: ['msg'],
  306. template: `<div class="child"><slot msg="from slot"></slot></div>`
  307. }
  308. const app = createApp({
  309. components: { Child },
  310. template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
  311. })
  312. expect(await renderToString(app)).toBe(
  313. `<div>parent<div class="child">` +
  314. `<!--[--><span>from slot</span><!--]-->` +
  315. `</div></div>`
  316. )
  317. })
  318. test('nested render fn components with template slots', async () => {
  319. const Child = {
  320. props: ['msg'],
  321. render(this: any) {
  322. return h(
  323. 'div',
  324. {
  325. class: 'child'
  326. },
  327. this.$slots.default({ msg: 'from slot' })
  328. )
  329. }
  330. }
  331. const app = createApp({
  332. template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
  333. })
  334. app.component('Child', Child)
  335. expect(await renderToString(app)).toBe(
  336. `<div>parent<div class="child">` +
  337. // no comment anchors because slot is used directly as element children
  338. `<span>from slot</span>` +
  339. `</div></div>`
  340. )
  341. })
  342. test('async components', async () => {
  343. const Child = {
  344. // should wait for resolved render context from setup()
  345. async setup() {
  346. return {
  347. msg: 'hello'
  348. }
  349. },
  350. ssrRender(ctx: any, push: any) {
  351. push(`<div>${ctx.msg}</div>`)
  352. }
  353. }
  354. expect(
  355. await renderToString(
  356. createApp({
  357. ssrRender(_ctx, push, parent) {
  358. push(`<div>parent`)
  359. push(renderComponent(Child, null, null, parent))
  360. push(`</div>`)
  361. }
  362. })
  363. )
  364. ).toBe(`<div>parent<div>hello</div></div>`)
  365. })
  366. test('parallel async components', async () => {
  367. const OptimizedChild = {
  368. props: ['msg'],
  369. async setup(props: any) {
  370. return {
  371. localMsg: props.msg + '!'
  372. }
  373. },
  374. ssrRender(ctx: any, push: any) {
  375. push(`<div>${ctx.localMsg}</div>`)
  376. }
  377. }
  378. const VNodeChild = {
  379. props: ['msg'],
  380. async setup(props: any) {
  381. return {
  382. localMsg: props.msg + '!'
  383. }
  384. },
  385. render(this: any) {
  386. return h('div', this.localMsg)
  387. }
  388. }
  389. expect(
  390. await renderToString(
  391. createApp({
  392. ssrRender(_ctx, push, parent) {
  393. push(`<div>parent`)
  394. push(
  395. renderComponent(OptimizedChild, { msg: 'opt' }, null, parent)
  396. )
  397. push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent))
  398. push(`</div>`)
  399. }
  400. })
  401. )
  402. ).toBe(`<div>parent<div>opt!</div><div>vnode!</div></div>`)
  403. })
  404. })
  405. describe('vnode element', () => {
  406. test('props', async () => {
  407. expect(
  408. await renderToString(
  409. h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello')
  410. )
  411. ).toBe(`<div id="foo&amp;" class="bar baz">hello</div>`)
  412. })
  413. test('text children', async () => {
  414. expect(await renderToString(h('div', 'hello'))).toBe(`<div>hello</div>`)
  415. })
  416. test('array children', async () => {
  417. expect(
  418. await renderToString(
  419. h('div', [
  420. 'foo',
  421. h('span', 'bar'),
  422. [h('span', 'baz')],
  423. createCommentVNode('qux')
  424. ])
  425. )
  426. ).toBe(
  427. `<div>foo<span>bar</span><!--[--><span>baz</span><!--]--><!--qux--></div>`
  428. )
  429. })
  430. test('void elements', async () => {
  431. expect(await renderToString(h('input'))).toBe(`<input>`)
  432. })
  433. test('innerHTML', async () => {
  434. expect(
  435. await renderToString(
  436. h(
  437. 'div',
  438. {
  439. innerHTML: `<span>hello</span>`
  440. },
  441. 'ignored'
  442. )
  443. )
  444. ).toBe(`<div><span>hello</span></div>`)
  445. })
  446. test('textContent', async () => {
  447. expect(
  448. await renderToString(
  449. h(
  450. 'div',
  451. {
  452. textContent: `<span>hello</span>`
  453. },
  454. 'ignored'
  455. )
  456. )
  457. ).toBe(`<div>${escapeHtml(`<span>hello</span>`)}</div>`)
  458. })
  459. test('textarea value', async () => {
  460. expect(
  461. await renderToString(
  462. h(
  463. 'textarea',
  464. {
  465. value: `<span>hello</span>`
  466. },
  467. 'ignored'
  468. )
  469. )
  470. ).toBe(`<textarea>${escapeHtml(`<span>hello</span>`)}</textarea>`)
  471. })
  472. })
  473. describe('raw vnode types', () => {
  474. test('Text', async () => {
  475. expect(await renderToString(createTextVNode('hello <div>'))).toBe(
  476. `hello &lt;div&gt;`
  477. )
  478. })
  479. test('Comment', async () => {
  480. // https://www.w3.org/TR/html52/syntax.html#comments
  481. expect(
  482. await renderToString(
  483. h('div', [
  484. createCommentVNode('>foo'),
  485. createCommentVNode('->foo'),
  486. createCommentVNode('<!--foo-->'),
  487. createCommentVNode('--!>foo<!-')
  488. ])
  489. )
  490. ).toBe(`<div><!--foo--><!--foo--><!--foo--><!--foo--></div>`)
  491. })
  492. test('Static', async () => {
  493. const content = `<div id="ok">hello<span>world</span></div>`
  494. expect(await renderToString(createStaticVNode(content))).toBe(content)
  495. })
  496. })
  497. describe('scopeId', () => {
  498. // note: here we are only testing scopeId handling for vdom serialization.
  499. // compiled srr render functions will include scopeId directly in strings.
  500. const withId = withScopeId('data-v-test')
  501. const withChildId = withScopeId('data-v-child')
  502. test('basic', async () => {
  503. expect(
  504. await renderToString(
  505. withId(() => {
  506. return h('div')
  507. })()
  508. )
  509. ).toBe(`<div data-v-test></div>`)
  510. })
  511. test('with slots', async () => {
  512. const Child = {
  513. __scopeId: 'data-v-child',
  514. render: withChildId(function(this: any) {
  515. return h('div', this.$slots.default())
  516. })
  517. }
  518. const Parent = {
  519. __scopeId: 'data-v-test',
  520. render: withId(() => {
  521. return h(Child, null, {
  522. default: withId(() => h('span', 'slot'))
  523. })
  524. })
  525. }
  526. expect(await renderToString(h(Parent))).toBe(
  527. `<div data-v-test data-v-child><span data-v-test data-v-child-s>slot</span></div>`
  528. )
  529. })
  530. })
  531. })