ssrRenderToString.spec.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. import {
  2. createApp,
  3. h,
  4. createCommentVNode,
  5. withScopeId,
  6. resolveComponent,
  7. ComponentOptions
  8. } from 'vue'
  9. import { escapeHtml } from '@vue/shared'
  10. import { renderToString, renderComponent } from '../src/renderToString'
  11. import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot'
  12. describe('ssr: renderToString', () => {
  13. test('should apply app context', async () => {
  14. const app = createApp({
  15. render() {
  16. const Foo = resolveComponent('foo') as ComponentOptions
  17. return h(Foo)
  18. }
  19. })
  20. app.component('foo', {
  21. render: () => h('div', 'foo')
  22. })
  23. const html = await renderToString(app)
  24. expect(html).toBe(`<div>foo</div>`)
  25. })
  26. describe('components', () => {
  27. test('vnode components', async () => {
  28. expect(
  29. await renderToString(
  30. createApp({
  31. data() {
  32. return { msg: 'hello' }
  33. },
  34. render(this: any) {
  35. return h('div', this.msg)
  36. }
  37. })
  38. )
  39. ).toBe(`<div>hello</div>`)
  40. })
  41. test('optimized components', async () => {
  42. expect(
  43. await renderToString(
  44. createApp({
  45. data() {
  46. return { msg: 'hello' }
  47. },
  48. ssrRender(ctx, push) {
  49. push(`<div>${ctx.msg}</div>`)
  50. }
  51. })
  52. )
  53. ).toBe(`<div>hello</div>`)
  54. })
  55. test('nested vnode components', async () => {
  56. const Child = {
  57. props: ['msg'],
  58. render(this: any) {
  59. return h('div', this.msg)
  60. }
  61. }
  62. expect(
  63. await renderToString(
  64. createApp({
  65. render() {
  66. return h('div', ['parent', h(Child, { msg: 'hello' })])
  67. }
  68. })
  69. )
  70. ).toBe(`<div>parent<div>hello</div></div>`)
  71. })
  72. test('nested optimized components', async () => {
  73. const Child = {
  74. props: ['msg'],
  75. ssrRender(ctx: any, push: any) {
  76. push(`<div>${ctx.msg}</div>`)
  77. }
  78. }
  79. expect(
  80. await renderToString(
  81. createApp({
  82. ssrRender(_ctx, push, parent) {
  83. push(`<div>parent`)
  84. push(renderComponent(Child, { msg: 'hello' }, null, parent))
  85. push(`</div>`)
  86. }
  87. })
  88. )
  89. ).toBe(`<div>parent<div>hello</div></div>`)
  90. })
  91. test('mixing optimized / vnode components', async () => {
  92. const OptimizedChild = {
  93. props: ['msg'],
  94. ssrRender(ctx: any, push: any) {
  95. push(`<div>${ctx.msg}</div>`)
  96. }
  97. }
  98. const VNodeChild = {
  99. props: ['msg'],
  100. render(this: any) {
  101. return h('div', this.msg)
  102. }
  103. }
  104. expect(
  105. await renderToString(
  106. createApp({
  107. ssrRender(_ctx, push, parent) {
  108. push(`<div>parent`)
  109. push(
  110. renderComponent(OptimizedChild, { msg: 'opt' }, null, parent)
  111. )
  112. push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent))
  113. push(`</div>`)
  114. }
  115. })
  116. )
  117. ).toBe(`<div>parent<div>opt</div><div>vnode</div></div>`)
  118. })
  119. test('nested components with optimized slots', async () => {
  120. const Child = {
  121. props: ['msg'],
  122. ssrRender(ctx: any, push: any, parent: any) {
  123. push(`<div class="child">`)
  124. ssrRenderSlot(
  125. ctx.$slots,
  126. 'default',
  127. { msg: 'from slot' },
  128. () => {
  129. push(`fallback`)
  130. },
  131. push,
  132. parent
  133. )
  134. push(`</div>`)
  135. }
  136. }
  137. expect(
  138. await renderToString(
  139. createApp({
  140. ssrRender(_ctx, push, parent) {
  141. push(`<div>parent`)
  142. push(
  143. renderComponent(
  144. Child,
  145. { msg: 'hello' },
  146. {
  147. // optimized slot using string push
  148. default: ({ msg }: any, push: any, p: any) => {
  149. push(`<span>${msg}</span>`)
  150. },
  151. // important to avoid slots being normalized
  152. _compiled: true as any
  153. },
  154. parent
  155. )
  156. )
  157. push(`</div>`)
  158. }
  159. })
  160. )
  161. ).toBe(
  162. `<div>parent<div class="child">` +
  163. `<!----><span>from slot</span><!---->` +
  164. `</div></div>`
  165. )
  166. // test fallback
  167. expect(
  168. await renderToString(
  169. createApp({
  170. ssrRender(_ctx, push, parent) {
  171. push(`<div>parent`)
  172. push(renderComponent(Child, { msg: 'hello' }, null, parent))
  173. push(`</div>`)
  174. }
  175. })
  176. )
  177. ).toBe(`<div>parent<div class="child"><!---->fallback<!----></div></div>`)
  178. })
  179. test('nested components with vnode slots', async () => {
  180. const Child = {
  181. props: ['msg'],
  182. ssrRender(ctx: any, push: any, parent: any) {
  183. push(`<div class="child">`)
  184. ssrRenderSlot(
  185. ctx.$slots,
  186. 'default',
  187. { msg: 'from slot' },
  188. null,
  189. push,
  190. parent
  191. )
  192. push(`</div>`)
  193. }
  194. }
  195. expect(
  196. await renderToString(
  197. createApp({
  198. ssrRender(_ctx, push, parent) {
  199. push(`<div>parent`)
  200. push(
  201. renderComponent(
  202. Child,
  203. { msg: 'hello' },
  204. {
  205. // bailed slots returning raw vnodes
  206. default: ({ msg }: any) => {
  207. return h('span', msg)
  208. }
  209. },
  210. parent
  211. )
  212. )
  213. push(`</div>`)
  214. }
  215. })
  216. )
  217. ).toBe(
  218. `<div>parent<div class="child">` +
  219. `<!----><span>from slot</span><!---->` +
  220. `</div></div>`
  221. )
  222. })
  223. test('async components', async () => {
  224. const Child = {
  225. // should wait for resovled render context from setup()
  226. async setup() {
  227. return {
  228. msg: 'hello'
  229. }
  230. },
  231. ssrRender(ctx: any, push: any) {
  232. push(`<div>${ctx.msg}</div>`)
  233. }
  234. }
  235. expect(
  236. await renderToString(
  237. createApp({
  238. ssrRender(_ctx, push, parent) {
  239. push(`<div>parent`)
  240. push(renderComponent(Child, null, null, parent))
  241. push(`</div>`)
  242. }
  243. })
  244. )
  245. ).toBe(`<div>parent<div>hello</div></div>`)
  246. })
  247. test('parallel async components', async () => {
  248. const OptimizedChild = {
  249. props: ['msg'],
  250. async setup(props: any) {
  251. return {
  252. localMsg: props.msg + '!'
  253. }
  254. },
  255. ssrRender(ctx: any, push: any) {
  256. push(`<div>${ctx.localMsg}</div>`)
  257. }
  258. }
  259. const VNodeChild = {
  260. props: ['msg'],
  261. async setup(props: any) {
  262. return {
  263. localMsg: props.msg + '!'
  264. }
  265. },
  266. render(this: any) {
  267. return h('div', this.localMsg)
  268. }
  269. }
  270. expect(
  271. await renderToString(
  272. createApp({
  273. ssrRender(_ctx, push, parent) {
  274. push(`<div>parent`)
  275. push(
  276. renderComponent(OptimizedChild, { msg: 'opt' }, null, parent)
  277. )
  278. push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent))
  279. push(`</div>`)
  280. }
  281. })
  282. )
  283. ).toBe(`<div>parent<div>opt!</div><div>vnode!</div></div>`)
  284. })
  285. })
  286. describe('vnode element', () => {
  287. test('props', async () => {
  288. expect(
  289. await renderToString(
  290. h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello')
  291. )
  292. ).toBe(`<div id="foo&amp;" class="bar baz">hello</div>`)
  293. })
  294. test('text children', async () => {
  295. expect(await renderToString(h('div', 'hello'))).toBe(`<div>hello</div>`)
  296. })
  297. test('array children', async () => {
  298. expect(
  299. await renderToString(
  300. h('div', [
  301. 'foo',
  302. h('span', 'bar'),
  303. [h('span', 'baz')],
  304. createCommentVNode('qux')
  305. ])
  306. )
  307. ).toBe(
  308. `<div>foo<span>bar</span><!----><span>baz</span><!----><!--qux--></div>`
  309. )
  310. })
  311. test('void elements', async () => {
  312. expect(await renderToString(h('input'))).toBe(`<input>`)
  313. })
  314. test('innerHTML', async () => {
  315. expect(
  316. await renderToString(
  317. h(
  318. 'div',
  319. {
  320. innerHTML: `<span>hello</span>`
  321. },
  322. 'ignored'
  323. )
  324. )
  325. ).toBe(`<div><span>hello</span></div>`)
  326. })
  327. test('textContent', async () => {
  328. expect(
  329. await renderToString(
  330. h(
  331. 'div',
  332. {
  333. textContent: `<span>hello</span>`
  334. },
  335. 'ignored'
  336. )
  337. )
  338. ).toBe(`<div>${escapeHtml(`<span>hello</span>`)}</div>`)
  339. })
  340. test('textarea value', async () => {
  341. expect(
  342. await renderToString(
  343. h(
  344. 'textarea',
  345. {
  346. value: `<span>hello</span>`
  347. },
  348. 'ignored'
  349. )
  350. )
  351. ).toBe(`<textarea>${escapeHtml(`<span>hello</span>`)}</textarea>`)
  352. })
  353. })
  354. describe('scopeId', () => {
  355. // note: here we are only testing scopeId handling for vdom serialization.
  356. // compiled srr render functions will include scopeId directly in strings.
  357. const withId = withScopeId('data-v-test')
  358. const withChildId = withScopeId('data-v-child')
  359. test('basic', async () => {
  360. expect(
  361. await renderToString(
  362. withId(() => {
  363. return h('div')
  364. })()
  365. )
  366. ).toBe(`<div data-v-test></div>`)
  367. })
  368. test('with slots', async () => {
  369. const Child = {
  370. __scopeId: 'data-v-child',
  371. render: withChildId(function(this: any) {
  372. return h('div', this.$slots.default())
  373. })
  374. }
  375. const Parent = {
  376. __scopeId: 'data-v-test',
  377. render: withId(() => {
  378. return h(Child, null, {
  379. default: withId(() => h('span', 'slot'))
  380. })
  381. })
  382. }
  383. expect(await renderToString(h(Parent))).toBe(
  384. `<div data-v-child><span data-v-test data-v-child-s>slot</span></div>`
  385. )
  386. })
  387. })
  388. })