renderToString.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. import {
  2. createApp,
  3. h,
  4. createCommentVNode,
  5. withScopeId,
  6. resolveComponent,
  7. ComponentOptions,
  8. Portal,
  9. ref,
  10. defineComponent
  11. } from 'vue'
  12. import { escapeHtml, mockWarn } from '@vue/shared'
  13. import {
  14. renderToString,
  15. renderComponent,
  16. SSRContext
  17. } from '../src/renderToString'
  18. import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot'
  19. mockWarn()
  20. describe('ssr: renderToString', () => {
  21. test('should apply app context', async () => {
  22. const app = createApp({
  23. render() {
  24. const Foo = resolveComponent('foo') as ComponentOptions
  25. return h(Foo)
  26. }
  27. })
  28. app.component('foo', {
  29. render: () => h('div', 'foo')
  30. })
  31. const html = await renderToString(app)
  32. expect(html).toBe(`<div>foo</div>`)
  33. })
  34. describe('components', () => {
  35. test('vnode components', async () => {
  36. expect(
  37. await renderToString(
  38. createApp({
  39. data() {
  40. return { msg: 'hello' }
  41. },
  42. render(this: any) {
  43. return h('div', this.msg)
  44. }
  45. })
  46. )
  47. ).toBe(`<div>hello</div>`)
  48. })
  49. test('option components returning render from setup', async () => {
  50. expect(
  51. await renderToString(
  52. createApp({
  53. setup() {
  54. const msg = ref('hello')
  55. return () => h('div', msg.value)
  56. }
  57. })
  58. )
  59. ).toBe(`<div>hello</div>`)
  60. })
  61. test('setup components returning render from setup', async () => {
  62. expect(
  63. await renderToString(
  64. createApp(
  65. defineComponent((props: {}) => {
  66. const msg = ref('hello')
  67. return () => h('div', msg.value)
  68. })
  69. )
  70. )
  71. ).toBe(`<div>hello</div>`)
  72. })
  73. test('optimized components', async () => {
  74. expect(
  75. await renderToString(
  76. createApp({
  77. data() {
  78. return { msg: 'hello' }
  79. },
  80. ssrRender(ctx, push) {
  81. push(`<div>${ctx.msg}</div>`)
  82. }
  83. })
  84. )
  85. ).toBe(`<div>hello</div>`)
  86. })
  87. describe('template components', () => {
  88. test('render', async () => {
  89. expect(
  90. await renderToString(
  91. createApp({
  92. data() {
  93. return { msg: 'hello' }
  94. },
  95. template: `<div>{{ msg }}</div>`
  96. })
  97. )
  98. ).toBe(`<div>hello</div>`)
  99. })
  100. test('handle compiler errors', async () => {
  101. await renderToString(createApp({ template: `<` }))
  102. expect(
  103. '[Vue warn]: Template compilation error: Unexpected EOF in tag.\n' +
  104. '1 | <\n' +
  105. ' | ^'
  106. ).toHaveBeenWarned()
  107. })
  108. })
  109. test('nested vnode components', async () => {
  110. const Child = {
  111. props: ['msg'],
  112. render(this: any) {
  113. return h('div', this.msg)
  114. }
  115. }
  116. expect(
  117. await renderToString(
  118. createApp({
  119. render() {
  120. return h('div', ['parent', h(Child, { msg: 'hello' })])
  121. }
  122. })
  123. )
  124. ).toBe(`<div>parent<div>hello</div></div>`)
  125. })
  126. test('nested optimized components', async () => {
  127. const Child = {
  128. props: ['msg'],
  129. ssrRender(ctx: any, push: any) {
  130. push(`<div>${ctx.msg}</div>`)
  131. }
  132. }
  133. expect(
  134. await renderToString(
  135. createApp({
  136. ssrRender(_ctx, push, parent) {
  137. push(`<div>parent`)
  138. push(renderComponent(Child, { msg: 'hello' }, null, parent))
  139. push(`</div>`)
  140. }
  141. })
  142. )
  143. ).toBe(`<div>parent<div>hello</div></div>`)
  144. })
  145. test('nested template components', async () => {
  146. const Child = {
  147. props: ['msg'],
  148. template: `<div>{{ msg }}</div>`
  149. }
  150. const app = createApp({
  151. template: `<div>parent<Child msg="hello" /></div>`
  152. })
  153. app.component('Child', Child)
  154. expect(await renderToString(app)).toBe(
  155. `<div>parent<div>hello</div></div>`
  156. )
  157. })
  158. test('mixing optimized / vnode / template components', async () => {
  159. const OptimizedChild = {
  160. props: ['msg'],
  161. ssrRender(ctx: any, push: any) {
  162. push(`<div>${ctx.msg}</div>`)
  163. }
  164. }
  165. const VNodeChild = {
  166. props: ['msg'],
  167. render(this: any) {
  168. return h('div', this.msg)
  169. }
  170. }
  171. const TemplateChild = {
  172. props: ['msg'],
  173. template: `<div>{{ msg }}</div>`
  174. }
  175. expect(
  176. await renderToString(
  177. createApp({
  178. ssrRender(_ctx, push, parent) {
  179. push(`<div>parent`)
  180. push(
  181. renderComponent(OptimizedChild, { msg: 'opt' }, null, parent)
  182. )
  183. push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent))
  184. push(
  185. renderComponent(
  186. TemplateChild,
  187. { msg: 'template' },
  188. null,
  189. parent
  190. )
  191. )
  192. push(`</div>`)
  193. }
  194. })
  195. )
  196. ).toBe(
  197. `<div>parent<div>opt</div><div>vnode</div><div>template</div></div>`
  198. )
  199. })
  200. test('nested components with optimized slots', async () => {
  201. const Child = {
  202. props: ['msg'],
  203. ssrRender(ctx: any, push: any, parent: any) {
  204. push(`<div class="child">`)
  205. ssrRenderSlot(
  206. ctx.$slots,
  207. 'default',
  208. { msg: 'from slot' },
  209. () => {
  210. push(`fallback`)
  211. },
  212. push,
  213. parent
  214. )
  215. push(`</div>`)
  216. }
  217. }
  218. expect(
  219. await renderToString(
  220. createApp({
  221. ssrRender(_ctx, push, parent) {
  222. push(`<div>parent`)
  223. push(
  224. renderComponent(
  225. Child,
  226. { msg: 'hello' },
  227. {
  228. // optimized slot using string push
  229. default: ({ msg }: any, push: any, p: any) => {
  230. push(`<span>${msg}</span>`)
  231. },
  232. // important to avoid slots being normalized
  233. _: 1 as any
  234. },
  235. parent
  236. )
  237. )
  238. push(`</div>`)
  239. }
  240. })
  241. )
  242. ).toBe(
  243. `<div>parent<div class="child">` +
  244. `<span>from slot</span>` +
  245. `</div></div>`
  246. )
  247. // test fallback
  248. expect(
  249. await renderToString(
  250. createApp({
  251. ssrRender(_ctx, push, parent) {
  252. push(`<div>parent`)
  253. push(renderComponent(Child, { msg: 'hello' }, null, parent))
  254. push(`</div>`)
  255. }
  256. })
  257. )
  258. ).toBe(`<div>parent<div class="child">fallback</div></div>`)
  259. })
  260. test('nested components with vnode slots', async () => {
  261. const Child = {
  262. props: ['msg'],
  263. ssrRender(ctx: any, push: any, parent: any) {
  264. push(`<div class="child">`)
  265. ssrRenderSlot(
  266. ctx.$slots,
  267. 'default',
  268. { msg: 'from slot' },
  269. null,
  270. push,
  271. parent
  272. )
  273. push(`</div>`)
  274. }
  275. }
  276. expect(
  277. await renderToString(
  278. createApp({
  279. ssrRender(_ctx, push, parent) {
  280. push(`<div>parent`)
  281. push(
  282. renderComponent(
  283. Child,
  284. { msg: 'hello' },
  285. {
  286. // bailed slots returning raw vnodes
  287. default: ({ msg }: any) => {
  288. return h('span', msg)
  289. }
  290. },
  291. parent
  292. )
  293. )
  294. push(`</div>`)
  295. }
  296. })
  297. )
  298. ).toBe(
  299. `<div>parent<div class="child">` +
  300. `<span>from slot</span>` +
  301. `</div></div>`
  302. )
  303. })
  304. test('nested components with template slots', async () => {
  305. const Child = {
  306. props: ['msg'],
  307. template: `<div class="child"><slot msg="from slot"></slot></div>`
  308. }
  309. const app = createApp({
  310. template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
  311. })
  312. app.component('Child', Child)
  313. expect(await renderToString(app)).toBe(
  314. `<div>parent<div class="child">` +
  315. `<span>from slot</span>` +
  316. `</div></div>`
  317. )
  318. })
  319. test('nested render fn components with template slots', async () => {
  320. const Child = {
  321. props: ['msg'],
  322. render(this: any) {
  323. return h(
  324. 'div',
  325. {
  326. class: 'child'
  327. },
  328. this.$slots.default({ msg: 'from slot' })
  329. )
  330. }
  331. }
  332. const app = createApp({
  333. template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
  334. })
  335. app.component('Child', Child)
  336. expect(await renderToString(app)).toBe(
  337. `<div>parent<div class="child">` +
  338. `<span>from slot</span>` +
  339. `</div></div>`
  340. )
  341. })
  342. test('async components', async () => {
  343. const Child = {
  344. // should wait for resovled 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(`<div>foo<span>bar</span><span>baz</span><!--qux--></div>`)
  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. test('portal', async () => {
  472. const ctx: SSRContext = {}
  473. await renderToString(
  474. h(
  475. Portal,
  476. {
  477. target: `#target`
  478. },
  479. h('span', 'hello')
  480. ),
  481. ctx
  482. )
  483. expect(ctx.portals!['#target']).toBe('<span>hello</span>')
  484. })
  485. describe('scopeId', () => {
  486. // note: here we are only testing scopeId handling for vdom serialization.
  487. // compiled srr render functions will include scopeId directly in strings.
  488. const withId = withScopeId('data-v-test')
  489. const withChildId = withScopeId('data-v-child')
  490. test('basic', async () => {
  491. expect(
  492. await renderToString(
  493. withId(() => {
  494. return h('div')
  495. })()
  496. )
  497. ).toBe(`<div data-v-test></div>`)
  498. })
  499. test('with slots', async () => {
  500. const Child = {
  501. __scopeId: 'data-v-child',
  502. render: withChildId(function(this: any) {
  503. return h('div', this.$slots.default())
  504. })
  505. }
  506. const Parent = {
  507. __scopeId: 'data-v-test',
  508. render: withId(() => {
  509. return h(Child, null, {
  510. default: withId(() => h('span', 'slot'))
  511. })
  512. })
  513. }
  514. expect(await renderToString(h(Parent))).toBe(
  515. `<div data-v-test data-v-child><span data-v-test data-v-child-s>slot</span></div>`
  516. )
  517. })
  518. })
  519. })