renderToString.spec.ts 13 KB

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