render.spec.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. import {
  2. createApp,
  3. h,
  4. createCommentVNode,
  5. withScopeId,
  6. resolveComponent,
  7. ComponentOptions,
  8. ref,
  9. defineComponent,
  10. createTextVNode,
  11. createStaticVNode
  12. } from 'vue'
  13. import { escapeHtml } from '@vue/shared'
  14. import { renderToString } from '../src/renderToString'
  15. import { renderToStream as _renderToStream } from '../src/renderToStream'
  16. import { ssrRenderSlot, SSRSlot } from '../src/helpers/ssrRenderSlot'
  17. import { ssrRenderComponent } from '../src/helpers/ssrRenderComponent'
  18. import { Readable } from 'stream'
  19. const promisifyStream = (stream: Readable) => {
  20. return new Promise<string>((resolve, reject) => {
  21. let result = ''
  22. stream.on('data', data => {
  23. result += data
  24. })
  25. stream.on('error', () => {
  26. reject(result)
  27. })
  28. stream.on('end', () => {
  29. resolve(result)
  30. })
  31. })
  32. }
  33. const renderToStream = (app: any, context?: any) =>
  34. promisifyStream(_renderToStream(app, context))
  35. // we run the same tests twice, once for renderToString, once for renderToStream
  36. testRender(`renderToString`, renderToString)
  37. testRender(`renderToStream`, renderToStream)
  38. function testRender(type: string, render: typeof renderToString) {
  39. describe(`ssr: ${type}`, () => {
  40. test('should apply app context', async () => {
  41. const app = createApp({
  42. render() {
  43. const Foo = resolveComponent('foo') as ComponentOptions
  44. return h(Foo)
  45. }
  46. })
  47. app.component('foo', {
  48. render: () => h('div', 'foo')
  49. })
  50. const html = await render(app)
  51. expect(html).toBe(`<div>foo</div>`)
  52. })
  53. describe('components', () => {
  54. test('vnode components', async () => {
  55. expect(
  56. await render(
  57. createApp({
  58. data() {
  59. return { msg: 'hello' }
  60. },
  61. render(this: any) {
  62. return h('div', this.msg)
  63. }
  64. })
  65. )
  66. ).toBe(`<div>hello</div>`)
  67. })
  68. test('option components returning render from setup', async () => {
  69. expect(
  70. await render(
  71. createApp({
  72. setup() {
  73. const msg = ref('hello')
  74. return () => h('div', msg.value)
  75. }
  76. })
  77. )
  78. ).toBe(`<div>hello</div>`)
  79. })
  80. test('setup components returning render from setup', async () => {
  81. expect(
  82. await render(
  83. createApp(
  84. defineComponent(() => {
  85. const msg = ref('hello')
  86. return () => h('div', msg.value)
  87. })
  88. )
  89. )
  90. ).toBe(`<div>hello</div>`)
  91. })
  92. test('optimized components', async () => {
  93. expect(
  94. await render(
  95. createApp({
  96. data() {
  97. return { msg: 'hello' }
  98. },
  99. ssrRender(ctx, push) {
  100. push(`<div>${ctx.msg}</div>`)
  101. }
  102. })
  103. )
  104. ).toBe(`<div>hello</div>`)
  105. })
  106. test('nested vnode components', async () => {
  107. const Child = {
  108. props: ['msg'],
  109. render(this: any) {
  110. return h('div', this.msg)
  111. }
  112. }
  113. expect(
  114. await render(
  115. createApp({
  116. render() {
  117. return h('div', ['parent', h(Child, { msg: 'hello' })])
  118. }
  119. })
  120. )
  121. ).toBe(`<div>parent<div>hello</div></div>`)
  122. })
  123. test('nested optimized components', async () => {
  124. const Child = {
  125. props: ['msg'],
  126. ssrRender(ctx: any, push: any) {
  127. push(`<div>${ctx.msg}</div>`)
  128. }
  129. }
  130. expect(
  131. await render(
  132. createApp({
  133. ssrRender(_ctx, push, parent) {
  134. push(`<div>parent`)
  135. push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent))
  136. push(`</div>`)
  137. }
  138. })
  139. )
  140. ).toBe(`<div>parent<div>hello</div></div>`)
  141. })
  142. test('nested template components', async () => {
  143. const Child = {
  144. props: ['msg'],
  145. template: `<div>{{ msg }}</div>`
  146. }
  147. const app = createApp({
  148. template: `<div>parent<Child msg="hello" /></div>`
  149. })
  150. app.component('Child', Child)
  151. expect(await render(app)).toBe(`<div>parent<div>hello</div></div>`)
  152. })
  153. test('template components with dynamic class attribute after static', async () => {
  154. const app = createApp({
  155. template: `<div><div class="child" :class="'dynamic'"></div></div>`
  156. })
  157. expect(await render(app)).toBe(
  158. `<div><div class="dynamic child"></div></div>`
  159. )
  160. })
  161. test('template components with dynamic class attribute before static', async () => {
  162. const app = createApp({
  163. template: `<div><div :class="'dynamic'" class="child"></div></div>`
  164. })
  165. expect(await render(app)).toBe(
  166. `<div><div class="dynamic child"></div></div>`
  167. )
  168. })
  169. test('mixing optimized / vnode / template components', async () => {
  170. const OptimizedChild = {
  171. props: ['msg'],
  172. ssrRender(ctx: any, push: any) {
  173. push(`<div>${ctx.msg}</div>`)
  174. }
  175. }
  176. const VNodeChild = {
  177. props: ['msg'],
  178. render(this: any) {
  179. return h('div', this.msg)
  180. }
  181. }
  182. const TemplateChild = {
  183. props: ['msg'],
  184. template: `<div>{{ msg }}</div>`
  185. }
  186. expect(
  187. await render(
  188. createApp({
  189. ssrRender(_ctx, push, parent) {
  190. push(`<div>parent`)
  191. push(
  192. ssrRenderComponent(
  193. OptimizedChild,
  194. { msg: 'opt' },
  195. null,
  196. parent
  197. )
  198. )
  199. push(
  200. ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent)
  201. )
  202. push(
  203. ssrRenderComponent(
  204. TemplateChild,
  205. { msg: 'template' },
  206. null,
  207. parent
  208. )
  209. )
  210. push(`</div>`)
  211. }
  212. })
  213. )
  214. ).toBe(
  215. `<div>parent<div>opt</div><div>vnode</div><div>template</div></div>`
  216. )
  217. })
  218. test('async components', async () => {
  219. const Child = {
  220. // should wait for resolved render context from setup()
  221. async setup() {
  222. return {
  223. msg: 'hello'
  224. }
  225. },
  226. ssrRender(ctx: any, push: any) {
  227. push(`<div>${ctx.msg}</div>`)
  228. }
  229. }
  230. expect(
  231. await render(
  232. createApp({
  233. ssrRender(_ctx, push, parent) {
  234. push(`<div>parent`)
  235. push(ssrRenderComponent(Child, null, null, parent))
  236. push(`</div>`)
  237. }
  238. })
  239. )
  240. ).toBe(`<div>parent<div>hello</div></div>`)
  241. })
  242. test('parallel async components', async () => {
  243. const OptimizedChild = {
  244. props: ['msg'],
  245. async setup(props: any) {
  246. return {
  247. localMsg: props.msg + '!'
  248. }
  249. },
  250. ssrRender(ctx: any, push: any) {
  251. push(`<div>${ctx.localMsg}</div>`)
  252. }
  253. }
  254. const VNodeChild = {
  255. props: ['msg'],
  256. async setup(props: any) {
  257. return {
  258. localMsg: props.msg + '!'
  259. }
  260. },
  261. render(this: any) {
  262. return h('div', this.localMsg)
  263. }
  264. }
  265. expect(
  266. await render(
  267. createApp({
  268. ssrRender(_ctx, push, parent) {
  269. push(`<div>parent`)
  270. push(
  271. ssrRenderComponent(
  272. OptimizedChild,
  273. { msg: 'opt' },
  274. null,
  275. parent
  276. )
  277. )
  278. push(
  279. ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent)
  280. )
  281. push(`</div>`)
  282. }
  283. })
  284. )
  285. ).toBe(`<div>parent<div>opt!</div><div>vnode!</div></div>`)
  286. })
  287. })
  288. describe('slots', () => {
  289. test('nested components with optimized slots', async () => {
  290. const Child = {
  291. props: ['msg'],
  292. ssrRender(ctx: any, push: any, parent: any) {
  293. push(`<div class="child">`)
  294. ssrRenderSlot(
  295. ctx.$slots,
  296. 'default',
  297. { msg: 'from slot' },
  298. () => {
  299. push(`fallback`)
  300. },
  301. push,
  302. parent
  303. )
  304. push(`</div>`)
  305. }
  306. }
  307. expect(
  308. await render(
  309. createApp({
  310. ssrRender(_ctx, push, parent) {
  311. push(`<div>parent`)
  312. push(
  313. ssrRenderComponent(
  314. Child,
  315. { msg: 'hello' },
  316. {
  317. // optimized slot using string push
  318. default: (({ msg }, push, _p) => {
  319. push(`<span>${msg}</span>`)
  320. }) as SSRSlot,
  321. // important to avoid slots being normalized
  322. _: 1 as any
  323. },
  324. parent
  325. )
  326. )
  327. push(`</div>`)
  328. }
  329. })
  330. )
  331. ).toBe(
  332. `<div>parent<div class="child">` +
  333. `<!--[--><span>from slot</span><!--]-->` +
  334. `</div></div>`
  335. )
  336. // test fallback
  337. expect(
  338. await render(
  339. createApp({
  340. ssrRender(_ctx, push, parent) {
  341. push(`<div>parent`)
  342. push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent))
  343. push(`</div>`)
  344. }
  345. })
  346. )
  347. ).toBe(
  348. `<div>parent<div class="child"><!--[-->fallback<!--]--></div></div>`
  349. )
  350. })
  351. test('nested components with vnode slots', async () => {
  352. const Child = {
  353. props: ['msg'],
  354. ssrRender(ctx: any, push: any, parent: any) {
  355. push(`<div class="child">`)
  356. ssrRenderSlot(
  357. ctx.$slots,
  358. 'default',
  359. { msg: 'from slot' },
  360. null,
  361. push,
  362. parent
  363. )
  364. push(`</div>`)
  365. }
  366. }
  367. expect(
  368. await render(
  369. createApp({
  370. ssrRender(_ctx, push, parent) {
  371. push(`<div>parent`)
  372. push(
  373. ssrRenderComponent(
  374. Child,
  375. { msg: 'hello' },
  376. {
  377. // bailed slots returning raw vnodes
  378. default: ({ msg }: any) => {
  379. return h('span', msg)
  380. }
  381. },
  382. parent
  383. )
  384. )
  385. push(`</div>`)
  386. }
  387. })
  388. )
  389. ).toBe(
  390. `<div>parent<div class="child">` +
  391. `<!--[--><span>from slot</span><!--]-->` +
  392. `</div></div>`
  393. )
  394. })
  395. test('nested components with template slots', async () => {
  396. const Child = {
  397. props: ['msg'],
  398. template: `<div class="child"><slot msg="from slot"></slot></div>`
  399. }
  400. const app = createApp({
  401. components: { Child },
  402. template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
  403. })
  404. expect(await render(app)).toBe(
  405. `<div>parent<div class="child">` +
  406. `<!--[--><span>from slot</span><!--]-->` +
  407. `</div></div>`
  408. )
  409. })
  410. test('nested render fn components with template slots', async () => {
  411. const Child = {
  412. props: ['msg'],
  413. render(this: any) {
  414. return h(
  415. 'div',
  416. {
  417. class: 'child'
  418. },
  419. this.$slots.default({ msg: 'from slot' })
  420. )
  421. }
  422. }
  423. const app = createApp({
  424. template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
  425. })
  426. app.component('Child', Child)
  427. expect(await render(app)).toBe(
  428. `<div>parent<div class="child">` +
  429. // no comment anchors because slot is used directly as element children
  430. `<span>from slot</span>` +
  431. `</div></div>`
  432. )
  433. })
  434. test('template slots forwarding', async () => {
  435. const Child = {
  436. template: `<div><slot/></div>`
  437. }
  438. const Parent = {
  439. components: { Child },
  440. template: `<Child><slot/></Child>`
  441. }
  442. const app = createApp({
  443. components: { Parent },
  444. template: `<Parent>hello</Parent>`
  445. })
  446. expect(await render(app)).toBe(
  447. `<div><!--[--><!--[-->hello<!--]--><!--]--></div>`
  448. )
  449. })
  450. test('template slots forwarding, empty slot', async () => {
  451. const Child = {
  452. template: `<div><slot/></div>`
  453. }
  454. const Parent = {
  455. components: { Child },
  456. template: `<Child><slot/></Child>`
  457. }
  458. const app = createApp({
  459. components: { Parent },
  460. template: `<Parent></Parent>`
  461. })
  462. expect(await render(app)).toBe(
  463. // should only have a single fragment
  464. `<div><!--[--><!--]--></div>`
  465. )
  466. })
  467. test('template slots forwarding, empty slot w/ fallback', async () => {
  468. const Child = {
  469. template: `<div><slot>fallback</slot></div>`
  470. }
  471. const Parent = {
  472. components: { Child },
  473. template: `<Child><slot/></Child>`
  474. }
  475. const app = createApp({
  476. components: { Parent },
  477. template: `<Parent></Parent>`
  478. })
  479. expect(await render(app)).toBe(
  480. // should only have a single fragment
  481. `<div><!--[-->fallback<!--]--></div>`
  482. )
  483. })
  484. })
  485. describe('vnode element', () => {
  486. test('props', async () => {
  487. expect(
  488. await render(h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello'))
  489. ).toBe(`<div id="foo&amp;" class="bar baz">hello</div>`)
  490. })
  491. test('text children', async () => {
  492. expect(await render(h('div', 'hello'))).toBe(`<div>hello</div>`)
  493. })
  494. test('array children', async () => {
  495. expect(
  496. await render(
  497. h('div', [
  498. 'foo',
  499. h('span', 'bar'),
  500. [h('span', 'baz')],
  501. createCommentVNode('qux')
  502. ])
  503. )
  504. ).toBe(
  505. `<div>foo<span>bar</span><!--[--><span>baz</span><!--]--><!--qux--></div>`
  506. )
  507. })
  508. test('void elements', async () => {
  509. expect(await render(h('input'))).toBe(`<input>`)
  510. })
  511. test('innerHTML', async () => {
  512. expect(
  513. await render(
  514. h(
  515. 'div',
  516. {
  517. innerHTML: `<span>hello</span>`
  518. },
  519. 'ignored'
  520. )
  521. )
  522. ).toBe(`<div><span>hello</span></div>`)
  523. })
  524. test('textContent', async () => {
  525. expect(
  526. await render(
  527. h(
  528. 'div',
  529. {
  530. textContent: `<span>hello</span>`
  531. },
  532. 'ignored'
  533. )
  534. )
  535. ).toBe(`<div>${escapeHtml(`<span>hello</span>`)}</div>`)
  536. })
  537. test('textarea value', async () => {
  538. expect(
  539. await render(
  540. h(
  541. 'textarea',
  542. {
  543. value: `<span>hello</span>`
  544. },
  545. 'ignored'
  546. )
  547. )
  548. ).toBe(`<textarea>${escapeHtml(`<span>hello</span>`)}</textarea>`)
  549. })
  550. })
  551. describe('raw vnode types', () => {
  552. test('Text', async () => {
  553. expect(await render(createTextVNode('hello <div>'))).toBe(
  554. `hello &lt;div&gt;`
  555. )
  556. })
  557. test('Comment', async () => {
  558. // https://www.w3.org/TR/html52/syntax.html#comments
  559. expect(
  560. await render(
  561. h('div', [
  562. createCommentVNode('>foo'),
  563. createCommentVNode('->foo'),
  564. createCommentVNode('<!--foo-->'),
  565. createCommentVNode('--!>foo<!-')
  566. ])
  567. )
  568. ).toBe(`<div><!--foo--><!--foo--><!--foo--><!--foo--></div>`)
  569. })
  570. test('Static', async () => {
  571. const content = `<div id="ok">hello<span>world</span></div>`
  572. expect(await render(createStaticVNode(content, 1))).toBe(content)
  573. })
  574. })
  575. describe('scopeId', () => {
  576. // note: here we are only testing scopeId handling for vdom serialization.
  577. // compiled srr render functions will include scopeId directly in strings.
  578. const withId = withScopeId('data-v-test')
  579. const withChildId = withScopeId('data-v-child')
  580. test('basic', async () => {
  581. expect(
  582. await render(
  583. withId(() => {
  584. return h('div')
  585. })()
  586. )
  587. ).toBe(`<div data-v-test></div>`)
  588. })
  589. test('with slots', async () => {
  590. const Child = {
  591. __scopeId: 'data-v-child',
  592. render: withChildId(function(this: any) {
  593. return h('div', this.$slots.default())
  594. })
  595. }
  596. const Parent = {
  597. __scopeId: 'data-v-test',
  598. render: withId(() => {
  599. return h(Child, null, {
  600. default: withId(() => h('span', 'slot'))
  601. })
  602. })
  603. }
  604. expect(await render(h(Parent))).toBe(
  605. `<div data-v-child data-v-test><span data-v-test data-v-child-s>slot</span></div>`
  606. )
  607. })
  608. })
  609. describe('integration w/ compiled template', () => {
  610. test('render', async () => {
  611. expect(
  612. await render(
  613. createApp({
  614. data() {
  615. return { msg: 'hello' }
  616. },
  617. template: `<div>{{ msg }}</div>`
  618. })
  619. )
  620. ).toBe(`<div>hello</div>`)
  621. })
  622. test('handle compiler errors', async () => {
  623. await render(
  624. // render different content since compilation is cached
  625. createApp({ template: `<${type === 'renderToString' ? 'div' : 'p'}` })
  626. )
  627. expect(
  628. `Template compilation error: Unexpected EOF in tag.`
  629. ).toHaveBeenWarned()
  630. expect(`Element is missing end tag`).toHaveBeenWarned()
  631. })
  632. })
  633. test('serverPrefetch', async () => {
  634. const msg = Promise.resolve('hello')
  635. const app = createApp({
  636. data() {
  637. return {
  638. msg: ''
  639. }
  640. },
  641. async serverPrefetch() {
  642. this.msg = await msg
  643. },
  644. render() {
  645. return h('div', this.msg)
  646. }
  647. })
  648. const html = await render(app)
  649. expect(html).toBe(`<div>hello</div>`)
  650. })
  651. })
  652. }