renderToString.spec.ts 9.7 KB

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