renderToString.spec.ts 14 KB

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