renderToString.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. import {
  2. createApp,
  3. h,
  4. createCommentVNode,
  5. withScopeId,
  6. resolveComponent,
  7. ComponentOptions,
  8. ref,
  9. defineComponent
  10. } from 'vue'
  11. import { escapeHtml, mockWarn } from '@vue/shared'
  12. import { renderToString, renderComponent } from '../src/renderToString'
  13. import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot'
  14. mockWarn()
  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. '[Vue warn]: 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(`<div>parent<div class="child">fallback</div></div>`)
  254. })
  255. test('nested components with vnode slots', async () => {
  256. const Child = {
  257. props: ['msg'],
  258. ssrRender(ctx: any, push: any, parent: any) {
  259. push(`<div class="child">`)
  260. ssrRenderSlot(
  261. ctx.$slots,
  262. 'default',
  263. { msg: 'from slot' },
  264. null,
  265. push,
  266. parent
  267. )
  268. push(`</div>`)
  269. }
  270. }
  271. expect(
  272. await renderToString(
  273. createApp({
  274. ssrRender(_ctx, push, parent) {
  275. push(`<div>parent`)
  276. push(
  277. renderComponent(
  278. Child,
  279. { msg: 'hello' },
  280. {
  281. // bailed slots returning raw vnodes
  282. default: ({ msg }: any) => {
  283. return h('span', msg)
  284. }
  285. },
  286. parent
  287. )
  288. )
  289. push(`</div>`)
  290. }
  291. })
  292. )
  293. ).toBe(
  294. `<div>parent<div class="child">` +
  295. `<span>from slot</span>` +
  296. `</div></div>`
  297. )
  298. })
  299. test('nested components with template slots', async () => {
  300. const Child = {
  301. props: ['msg'],
  302. template: `<div class="child"><slot msg="from slot"></slot></div>`
  303. }
  304. const app = createApp({
  305. template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
  306. })
  307. app.component('Child', Child)
  308. expect(await renderToString(app)).toBe(
  309. `<div>parent<div class="child">` +
  310. `<span>from slot</span>` +
  311. `</div></div>`
  312. )
  313. })
  314. test('nested render fn components with template slots', async () => {
  315. const Child = {
  316. props: ['msg'],
  317. render(this: any) {
  318. return h(
  319. 'div',
  320. {
  321. class: 'child'
  322. },
  323. this.$slots.default({ msg: 'from slot' })
  324. )
  325. }
  326. }
  327. const app = createApp({
  328. template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
  329. })
  330. app.component('Child', Child)
  331. expect(await renderToString(app)).toBe(
  332. `<div>parent<div class="child">` +
  333. `<span>from slot</span>` +
  334. `</div></div>`
  335. )
  336. })
  337. test('async components', async () => {
  338. const Child = {
  339. // should wait for resovled render context from setup()
  340. async setup() {
  341. return {
  342. msg: 'hello'
  343. }
  344. },
  345. ssrRender(ctx: any, push: any) {
  346. push(`<div>${ctx.msg}</div>`)
  347. }
  348. }
  349. expect(
  350. await renderToString(
  351. createApp({
  352. ssrRender(_ctx, push, parent) {
  353. push(`<div>parent`)
  354. push(renderComponent(Child, null, null, parent))
  355. push(`</div>`)
  356. }
  357. })
  358. )
  359. ).toBe(`<div>parent<div>hello</div></div>`)
  360. })
  361. test('parallel async components', async () => {
  362. const OptimizedChild = {
  363. props: ['msg'],
  364. async setup(props: any) {
  365. return {
  366. localMsg: props.msg + '!'
  367. }
  368. },
  369. ssrRender(ctx: any, push: any) {
  370. push(`<div>${ctx.localMsg}</div>`)
  371. }
  372. }
  373. const VNodeChild = {
  374. props: ['msg'],
  375. async setup(props: any) {
  376. return {
  377. localMsg: props.msg + '!'
  378. }
  379. },
  380. render(this: any) {
  381. return h('div', this.localMsg)
  382. }
  383. }
  384. expect(
  385. await renderToString(
  386. createApp({
  387. ssrRender(_ctx, push, parent) {
  388. push(`<div>parent`)
  389. push(
  390. renderComponent(OptimizedChild, { msg: 'opt' }, null, parent)
  391. )
  392. push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent))
  393. push(`</div>`)
  394. }
  395. })
  396. )
  397. ).toBe(`<div>parent<div>opt!</div><div>vnode!</div></div>`)
  398. })
  399. })
  400. describe('vnode element', () => {
  401. test('props', async () => {
  402. expect(
  403. await renderToString(
  404. h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello')
  405. )
  406. ).toBe(`<div id="foo&amp;" class="bar baz">hello</div>`)
  407. })
  408. test('text children', async () => {
  409. expect(await renderToString(h('div', 'hello'))).toBe(`<div>hello</div>`)
  410. })
  411. test('array children', async () => {
  412. expect(
  413. await renderToString(
  414. h('div', [
  415. 'foo',
  416. h('span', 'bar'),
  417. [h('span', 'baz')],
  418. createCommentVNode('qux')
  419. ])
  420. )
  421. ).toBe(`<div>foo<span>bar</span><span>baz</span><!--qux--></div>`)
  422. })
  423. test('void elements', async () => {
  424. expect(await renderToString(h('input'))).toBe(`<input>`)
  425. })
  426. test('innerHTML', async () => {
  427. expect(
  428. await renderToString(
  429. h(
  430. 'div',
  431. {
  432. innerHTML: `<span>hello</span>`
  433. },
  434. 'ignored'
  435. )
  436. )
  437. ).toBe(`<div><span>hello</span></div>`)
  438. })
  439. test('textContent', async () => {
  440. expect(
  441. await renderToString(
  442. h(
  443. 'div',
  444. {
  445. textContent: `<span>hello</span>`
  446. },
  447. 'ignored'
  448. )
  449. )
  450. ).toBe(`<div>${escapeHtml(`<span>hello</span>`)}</div>`)
  451. })
  452. test('textarea value', async () => {
  453. expect(
  454. await renderToString(
  455. h(
  456. 'textarea',
  457. {
  458. value: `<span>hello</span>`
  459. },
  460. 'ignored'
  461. )
  462. )
  463. ).toBe(`<textarea>${escapeHtml(`<span>hello</span>`)}</textarea>`)
  464. })
  465. })
  466. describe('scopeId', () => {
  467. // note: here we are only testing scopeId handling for vdom serialization.
  468. // compiled srr render functions will include scopeId directly in strings.
  469. const withId = withScopeId('data-v-test')
  470. const withChildId = withScopeId('data-v-child')
  471. test('basic', async () => {
  472. expect(
  473. await renderToString(
  474. withId(() => {
  475. return h('div')
  476. })()
  477. )
  478. ).toBe(`<div data-v-test></div>`)
  479. })
  480. test('with slots', async () => {
  481. const Child = {
  482. __scopeId: 'data-v-child',
  483. render: withChildId(function(this: any) {
  484. return h('div', this.$slots.default())
  485. })
  486. }
  487. const Parent = {
  488. __scopeId: 'data-v-test',
  489. render: withId(() => {
  490. return h(Child, null, {
  491. default: withId(() => h('span', 'slot'))
  492. })
  493. })
  494. }
  495. expect(await renderToString(h(Parent))).toBe(
  496. `<div data-v-test data-v-child><span data-v-test data-v-child-s>slot</span></div>`
  497. )
  498. })
  499. })
  500. })