ssr-string.spec.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. import Vue from '../../dist/vue.common.js'
  2. import { compileToFunctions } from '../../packages/vue-template-compiler'
  3. import { createRenderer } from '../../packages/vue-server-renderer'
  4. const { renderToString } = createRenderer()
  5. describe('SSR: renderToString', () => {
  6. it('static attributes', done => {
  7. renderVmWithOptions({
  8. template: '<div id="foo" bar="123"></div>'
  9. }, result => {
  10. expect(result).toContain('<div id="foo" bar="123" server-rendered="true"></div>')
  11. done()
  12. })
  13. })
  14. it('unary tags', done => {
  15. renderVmWithOptions({
  16. template: '<input value="123">'
  17. }, result => {
  18. expect(result).toContain('<input value="123" server-rendered="true">')
  19. done()
  20. })
  21. })
  22. it('dynamic attributes', done => {
  23. renderVmWithOptions({
  24. template: '<div qux="quux" :id="foo" :bar="baz"></div>',
  25. data: {
  26. foo: 'hi',
  27. baz: 123
  28. }
  29. }, result => {
  30. expect(result).toContain('<div qux="quux" id="hi" bar="123" server-rendered="true"></div>')
  31. done()
  32. })
  33. })
  34. it('static class', done => {
  35. renderVmWithOptions({
  36. template: '<div class="foo bar"></div>'
  37. }, result => {
  38. expect(result).toContain('<div server-rendered="true" class="foo bar"></div>')
  39. done()
  40. })
  41. })
  42. it('dynamic class', done => {
  43. renderVmWithOptions({
  44. template: '<div class="foo bar" :class="[a, { qux: hasQux, quux: hasQuux }]"></div>',
  45. data: {
  46. a: 'baz',
  47. hasQux: true,
  48. hasQuux: false
  49. }
  50. }, result => {
  51. expect(result).toContain('<div server-rendered="true" class="foo bar baz qux"></div>')
  52. done()
  53. })
  54. })
  55. it('dynamic style', done => {
  56. renderVmWithOptions({
  57. template: '<div style="background-color:black" :style="{ fontSize: fontSize + \'px\', color: color }"></div>',
  58. data: {
  59. fontSize: 14,
  60. color: 'red'
  61. }
  62. }, result => {
  63. expect(result).toContain(
  64. '<div server-rendered="true" style="font-size:14px;color:red;background-color:black"></div>'
  65. )
  66. done()
  67. })
  68. })
  69. it('text interpolation', done => {
  70. renderVmWithOptions({
  71. template: '<div>{{ foo }} side {{ bar }}</div>',
  72. data: {
  73. foo: 'server',
  74. bar: '<span>rendering</span>'
  75. }
  76. }, result => {
  77. expect(result).toContain('<div server-rendered="true">server side &lt;span&gt;rendering&lt;&sol;span&gt;</div>')
  78. done()
  79. })
  80. })
  81. it('v-html', done => {
  82. renderVmWithOptions({
  83. template: '<div v-html="text"></div>',
  84. data: {
  85. text: '<span>foo</span>'
  86. }
  87. }, result => {
  88. expect(result).toContain('<div server-rendered="true"><span>foo</span></div>')
  89. done()
  90. })
  91. })
  92. it('v-text', done => {
  93. renderVmWithOptions({
  94. template: '<div v-text="text"></div>',
  95. data: {
  96. text: '<span>foo</span>'
  97. }
  98. }, result => {
  99. expect(result).toContain('<div server-rendered="true">&lt;span&gt;foo&lt;&sol;span&gt;</div>')
  100. done()
  101. })
  102. })
  103. it('child component (hoc)', done => {
  104. renderVmWithOptions({
  105. template: '<child class="foo" :msg="msg"></child>',
  106. data: {
  107. msg: 'hello'
  108. },
  109. components: {
  110. child: {
  111. props: ['msg'],
  112. data () {
  113. return { name: 'bar' }
  114. },
  115. render () {
  116. const h = this.$createElement
  117. return h('div', { class: ['bar'] }, [`${this.msg} ${this.name}`])
  118. }
  119. }
  120. }
  121. }, result => {
  122. expect(result).toContain('<div server-rendered="true" class="foo bar">hello bar</div>')
  123. done()
  124. })
  125. })
  126. it('has correct lifecycle during render', done => {
  127. let lifecycleCount = 1
  128. renderVmWithOptions({
  129. template: '<div><span>{{ val }}</span><test></test></div>',
  130. data: {
  131. val: 'hi'
  132. },
  133. beforeCreate () {
  134. expect(lifecycleCount++).toBe(1)
  135. },
  136. created () {
  137. this.val = 'hello'
  138. expect(this.val).toBe('hello')
  139. expect(lifecycleCount++).toBe(2)
  140. },
  141. components: {
  142. test: {
  143. beforeCreate () {
  144. expect(lifecycleCount++).toBe(3)
  145. },
  146. created () {
  147. expect(lifecycleCount++).toBe(4)
  148. },
  149. render () {
  150. expect(lifecycleCount++).toBeGreaterThan(4)
  151. return this.$createElement('span', { class: ['b'] }, 'testAsync')
  152. }
  153. }
  154. }
  155. }, result => {
  156. expect(result).toContain(
  157. '<div server-rendered="true">' +
  158. '<span>hello</span>' +
  159. '<span class="b">testAsync</span>' +
  160. '</div>'
  161. )
  162. done()
  163. })
  164. })
  165. it('renders asynchronous component', done => {
  166. renderVmWithOptions({
  167. template: `
  168. <div>
  169. <test-async></test-async>
  170. </div>
  171. `,
  172. components: {
  173. testAsync (resolve) {
  174. resolve({
  175. render () {
  176. return this.$createElement('span', { class: ['b'] }, 'testAsync')
  177. }
  178. })
  179. }
  180. }
  181. }, result => {
  182. expect(result).toContain('<div server-rendered="true"><span class="b">testAsync</span></div>')
  183. done()
  184. })
  185. })
  186. it('renders asynchronous component (hoc)', done => {
  187. renderVmWithOptions({
  188. template: '<test-async></test-async>',
  189. components: {
  190. testAsync (resolve) {
  191. resolve({
  192. render () {
  193. return this.$createElement('span', { class: ['b'] }, 'testAsync')
  194. }
  195. })
  196. }
  197. }
  198. }, result => {
  199. expect(result).toContain('<span server-rendered="true" class="b">testAsync</span>')
  200. done()
  201. })
  202. })
  203. it('renders nested asynchronous component', done => {
  204. renderVmWithOptions({
  205. template: `
  206. <div>
  207. <test-async></test-async>
  208. </div>
  209. `,
  210. components: {
  211. testAsync (resolve) {
  212. const options = compileToFunctions(`
  213. <span class="b">
  214. <test-sub-async></test-sub-async>
  215. </span>
  216. `, { preserveWhitespace: false })
  217. options.components = {
  218. testSubAsync (resolve) {
  219. resolve({
  220. render () {
  221. return this.$createElement('div', { class: ['c'] }, 'testSubAsync')
  222. }
  223. })
  224. }
  225. }
  226. resolve(options)
  227. }
  228. }
  229. }, result => {
  230. expect(result).toContain('<div server-rendered="true"><span class="b"><div class="c">testSubAsync</div></span></div>')
  231. done()
  232. })
  233. })
  234. it('everything together', done => {
  235. renderVmWithOptions({
  236. template: `
  237. <div>
  238. <p class="hi">yoyo</p>
  239. <div id="ho" :class="{ red: isRed }"></div>
  240. <span>{{ test }}</span>
  241. <input :value="test">
  242. <img :src="imageUrl">
  243. <test></test>
  244. <test-async></test-async>
  245. </div>
  246. `,
  247. data: {
  248. test: 'hi',
  249. isRed: true,
  250. imageUrl: 'https://vuejs.org/images/logo.png'
  251. },
  252. components: {
  253. test: {
  254. render () {
  255. return this.$createElement('div', { class: ['a'] }, 'test')
  256. }
  257. },
  258. testAsync (resolve) {
  259. resolve({
  260. render () {
  261. return this.$createElement('span', { class: ['b'] }, 'testAsync')
  262. }
  263. })
  264. }
  265. }
  266. }, result => {
  267. expect(result).toContain(
  268. '<div server-rendered="true">' +
  269. '<p class="hi">yoyo</p> ' +
  270. '<div id="ho" class="red"></div> ' +
  271. '<span>hi</span> ' +
  272. '<input value="hi"> ' +
  273. '<img src="https://vuejs.org/images/logo.png"> ' +
  274. '<div class="a">test</div> ' +
  275. '<span class="b">testAsync</span>' +
  276. '</div>'
  277. )
  278. done()
  279. })
  280. })
  281. it('normal attr', done => {
  282. renderVmWithOptions({
  283. template: `
  284. <div>
  285. <span :test="'ok'">hello</span>
  286. <span :test="null">hello</span>
  287. <span :test="false">hello</span>
  288. <span :test="true">hello</span>
  289. <span :test="0">hello</span>
  290. </div>
  291. `
  292. }, result => {
  293. expect(result).toContain(
  294. '<div server-rendered="true">' +
  295. '<span test="ok">hello</span> ' +
  296. '<span>hello</span> ' +
  297. '<span>hello</span> ' +
  298. '<span test="true">hello</span> ' +
  299. '<span test="0">hello</span>' +
  300. '</div>'
  301. )
  302. done()
  303. })
  304. })
  305. it('enumrated attr', done => {
  306. renderVmWithOptions({
  307. template: `
  308. <div>
  309. <span :draggable="true">hello</span>
  310. <span :draggable="'ok'">hello</span>
  311. <span :draggable="null">hello</span>
  312. <span :draggable="false">hello</span>
  313. <span :draggable="''">hello</span>
  314. <span :draggable="'false'">hello</span>
  315. </div>
  316. `
  317. }, result => {
  318. expect(result).toContain(
  319. '<div server-rendered="true">' +
  320. '<span draggable="true">hello</span> ' +
  321. '<span draggable="true">hello</span> ' +
  322. '<span draggable="false">hello</span> ' +
  323. '<span draggable="false">hello</span> ' +
  324. '<span draggable="true">hello</span> ' +
  325. '<span draggable="false">hello</span>' +
  326. '</div>'
  327. )
  328. done()
  329. })
  330. })
  331. it('boolean attr', done => {
  332. renderVmWithOptions({
  333. template: `
  334. <div>
  335. <span :disabled="true">hello</span>
  336. <span :disabled="'ok'">hello</span>
  337. <span :disabled="null">hello</span>
  338. <span :disabled="''">hello</span>
  339. </div>
  340. `
  341. }, result => {
  342. expect(result).toContain(
  343. '<div server-rendered="true">' +
  344. '<span disabled="disabled">hello</span> ' +
  345. '<span disabled="disabled">hello</span> ' +
  346. '<span>hello</span> ' +
  347. '<span disabled="disabled">hello</span>' +
  348. '</div>'
  349. )
  350. done()
  351. })
  352. })
  353. it('v-bind object', done => {
  354. renderVmWithOptions({
  355. data: {
  356. test: { id: 'a', class: 'b', value: 'c' }
  357. },
  358. template: '<input v-bind="test">'
  359. }, result => {
  360. expect(result).toContain('<input id="a" class="b" server-rendered="true" value="c">')
  361. done()
  362. })
  363. })
  364. it('custom directives', done => {
  365. const renderer = createRenderer({
  366. directives: {
  367. 'class-prefixer': (node, dir) => {
  368. if (node.data.class) {
  369. node.data.class = `${dir.value}-${node.data.class}`
  370. }
  371. if (node.data.staticClass) {
  372. node.data.staticClass = `${dir.value}-${node.data.staticClass}`
  373. }
  374. }
  375. }
  376. })
  377. renderer.renderToString(new Vue({
  378. render () {
  379. const h = this.$createElement
  380. return h('p', {
  381. class: 'class1',
  382. staticClass: 'class2',
  383. directives: [{
  384. name: 'class-prefixer',
  385. value: 'my'
  386. }]
  387. }, ['hello world'])
  388. }
  389. }), (err, result) => {
  390. expect(err).toBeNull()
  391. expect(result).toContain('<p server-rendered="true" class="my-class2 my-class1">hello world</p>')
  392. done()
  393. })
  394. })
  395. it('_scopeId', done => {
  396. renderVmWithOptions({
  397. _scopeId: '_v-parent',
  398. template: '<div id="foo"><p><child></child></p></div>',
  399. components: {
  400. child: {
  401. _scopeId: '_v-child',
  402. render () {
  403. const h = this.$createElement
  404. return h('div', null, [h('span', null, ['foo'])])
  405. }
  406. }
  407. }
  408. }, result => {
  409. expect(result).toContain(
  410. '<div id="foo" server-rendered="true" _v-parent>' +
  411. '<p _v-parent>' +
  412. '<div _v-child _v-parent><span _v-child>foo</span></div>' +
  413. '</p>' +
  414. '</div>'
  415. )
  416. done()
  417. })
  418. })
  419. it('_scopeId on slot content', done => {
  420. renderVmWithOptions({
  421. _scopeId: '_v-parent',
  422. template: '<div><child><p>foo</p></child></div>',
  423. components: {
  424. child: {
  425. _scopeId: '_v-child',
  426. render () {
  427. const h = this.$createElement
  428. return h('div', null, this.$slots.default)
  429. }
  430. }
  431. }
  432. }, result => {
  433. expect(result).toContain(
  434. '<div server-rendered="true" _v-parent>' +
  435. '<div _v-child _v-parent><p _v-child _v-parent>foo</p></div>' +
  436. '</div>'
  437. )
  438. done()
  439. })
  440. })
  441. it('should catch error', done => {
  442. renderToString(new Vue({
  443. render () {
  444. throw new Error('oops')
  445. }
  446. }), err => {
  447. expect(err instanceof Error).toBe(true)
  448. done()
  449. })
  450. })
  451. })
  452. function renderVmWithOptions (options, cb) {
  453. const res = compileToFunctions(options.template)
  454. Object.assign(options, res)
  455. delete options.template
  456. renderToString(new Vue(options), (err, res) => {
  457. expect(err).toBeNull()
  458. cb(res)
  459. })
  460. }