ssr-string.spec.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287
  1. import Vue from '../../dist/vue.runtime.common.js'
  2. import VM from 'vm'
  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" data-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" data-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" data-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 data-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 data-server-rendered="true" class="foo bar baz qux"></div>')
  52. done()
  53. })
  54. })
  55. it('custom component class', done => {
  56. renderVmWithOptions({
  57. template: '<div><cmp class="cmp"></cmp></div>',
  58. components: {
  59. cmp: {
  60. render: h => h('div', 'test')
  61. }
  62. }
  63. }, result => {
  64. expect(result).toContain('<div data-server-rendered="true"><div class="cmp">test</div></div>')
  65. done()
  66. })
  67. })
  68. it('nested component class', done => {
  69. renderVmWithOptions({
  70. template: '<cmp class="outer" :class="cls"></cmp>',
  71. data: { cls: { 'success': 1 }},
  72. components: {
  73. cmp: {
  74. render: h => h('div', [h('nested', { staticClass: 'nested', 'class': { 'error': 1 }})]),
  75. components: {
  76. nested: {
  77. render: h => h('div', { staticClass: 'inner' }, 'test')
  78. }
  79. }
  80. }
  81. }
  82. }, result => {
  83. expect(result).toContain('<div data-server-rendered="true" class="outer success">' +
  84. '<div class="inner nested error">test</div>' +
  85. '</div>')
  86. done()
  87. })
  88. })
  89. it('dynamic style', done => {
  90. renderVmWithOptions({
  91. template: '<div style="background-color:black" :style="{ fontSize: fontSize + \'px\', color: color }"></div>',
  92. data: {
  93. fontSize: 14,
  94. color: 'red'
  95. }
  96. }, result => {
  97. expect(result).toContain(
  98. '<div data-server-rendered="true" style="background-color:black;font-size:14px;color:red;"></div>'
  99. )
  100. done()
  101. })
  102. })
  103. it('dynamic string style', done => {
  104. renderVmWithOptions({
  105. template: '<div :style="style"></div>',
  106. data: {
  107. style: 'color:red'
  108. }
  109. }, result => {
  110. expect(result).toContain(
  111. '<div data-server-rendered="true" style="color:red;"></div>'
  112. )
  113. done()
  114. })
  115. })
  116. it('auto-prefixed style value as array', done => {
  117. renderVmWithOptions({
  118. template: '<div :style="style"></div>',
  119. data: {
  120. style: {
  121. display: ['-webkit-box', '-ms-flexbox', 'flex']
  122. }
  123. }
  124. }, result => {
  125. expect(result).toContain(
  126. '<div data-server-rendered="true" style="display:-webkit-box;display:-ms-flexbox;display:flex;"></div>'
  127. )
  128. done()
  129. })
  130. })
  131. it('custom component style', done => {
  132. renderVmWithOptions({
  133. template: '<section><comp :style="style"></comp></section>',
  134. data: {
  135. style: 'color:red'
  136. },
  137. components: {
  138. comp: {
  139. template: '<div></div>'
  140. }
  141. }
  142. }, result => {
  143. expect(result).toContain(
  144. '<section data-server-rendered="true"><div style="color:red;"></div></section>'
  145. )
  146. done()
  147. })
  148. })
  149. it('nested custom component style', done => {
  150. renderVmWithOptions({
  151. template: '<comp style="color: blue" :style="style"></comp>',
  152. data: {
  153. style: 'color:red'
  154. },
  155. components: {
  156. comp: {
  157. template: '<nested style="text-align: left;" :style="{fontSize:\'520rem\'}"></nested>',
  158. components: {
  159. nested: {
  160. template: '<div></div>'
  161. }
  162. }
  163. }
  164. }
  165. }, result => {
  166. expect(result).toContain(
  167. '<div data-server-rendered="true" style="text-align:left;font-size:520rem;color:red;"></div>'
  168. )
  169. done()
  170. })
  171. })
  172. it('component style not passed to child', done => {
  173. renderVmWithOptions({
  174. template: '<comp :style="style"></comp>',
  175. data: {
  176. style: 'color:red'
  177. },
  178. components: {
  179. comp: {
  180. template: '<div><div></div></div>'
  181. }
  182. }
  183. }, result => {
  184. expect(result).toContain(
  185. '<div data-server-rendered="true" style="color:red;"><div></div></div>'
  186. )
  187. done()
  188. })
  189. })
  190. it('component style not passed to slot', done => {
  191. renderVmWithOptions({
  192. template: '<comp :style="style"><span style="color:black"></span></comp>',
  193. data: {
  194. style: 'color:red'
  195. },
  196. components: {
  197. comp: {
  198. template: '<div><slot></slot></div>'
  199. }
  200. }
  201. }, result => {
  202. expect(result).toContain(
  203. '<div data-server-rendered="true" style="color:red;"><span style="color:black;"></span></div>'
  204. )
  205. done()
  206. })
  207. })
  208. it('attrs merging on components', done => {
  209. const Test = {
  210. render: h => h('div', {
  211. attrs: { id: 'a' }
  212. })
  213. }
  214. renderVmWithOptions({
  215. render: h => h(Test, {
  216. attrs: { id: 'b', name: 'c' }
  217. })
  218. }, res => {
  219. expect(res).toContain(
  220. '<div id="b" data-server-rendered="true" name="c"></div>'
  221. )
  222. done()
  223. })
  224. })
  225. it('domProps merging on components', done => {
  226. const Test = {
  227. render: h => h('div', {
  228. domProps: { innerHTML: 'a' }
  229. })
  230. }
  231. renderVmWithOptions({
  232. render: h => h(Test, {
  233. domProps: { innerHTML: 'b', value: 'c' }
  234. })
  235. }, res => {
  236. expect(res).toContain(
  237. '<div data-server-rendered="true" value="c">b</div>'
  238. )
  239. done()
  240. })
  241. })
  242. it('v-show directive render', done => {
  243. renderVmWithOptions({
  244. template: '<div v-show="false"><span>inner</span></div>'
  245. }, res => {
  246. expect(res).toContain(
  247. '<div data-server-rendered="true" style="display:none;"><span>inner</span></div>'
  248. )
  249. done()
  250. })
  251. })
  252. it('v-show directive merge with style', done => {
  253. renderVmWithOptions({
  254. template: '<div :style="[{lineHeight: 1}]" v-show="false"><span>inner</span></div>'
  255. }, res => {
  256. expect(res).toContain(
  257. '<div data-server-rendered="true" style="line-height:1;display:none;"><span>inner</span></div>'
  258. )
  259. done()
  260. })
  261. })
  262. it('v-show directive not passed to child', done => {
  263. renderVmWithOptions({
  264. template: '<foo v-show="false"></foo>',
  265. components: {
  266. foo: {
  267. template: '<div><span>inner</span></div>'
  268. }
  269. }
  270. }, res => {
  271. expect(res).toContain(
  272. '<div data-server-rendered="true" style="display:none;"><span>inner</span></div>'
  273. )
  274. done()
  275. })
  276. })
  277. it('v-show directive not passed to slot', done => {
  278. renderVmWithOptions({
  279. template: '<foo v-show="false"><span>inner</span></foo>',
  280. components: {
  281. foo: {
  282. template: '<div><slot></slot></div>'
  283. }
  284. }
  285. }, res => {
  286. expect(res).toContain(
  287. '<div data-server-rendered="true" style="display:none;"><span>inner</span></div>'
  288. )
  289. done()
  290. })
  291. })
  292. it('v-show directive merging on components', done => {
  293. renderVmWithOptions({
  294. template: '<foo v-show="false"></foo>',
  295. components: {
  296. foo: {
  297. render: h => h('bar', {
  298. directives: [{
  299. name: 'show',
  300. value: true
  301. }]
  302. }),
  303. components: {
  304. bar: {
  305. render: h => h('div', 'inner')
  306. }
  307. }
  308. }
  309. }
  310. }, res => {
  311. expect(res).toContain(
  312. '<div data-server-rendered="true" style="display:none;">inner</div>'
  313. )
  314. done()
  315. })
  316. })
  317. it('text interpolation', done => {
  318. renderVmWithOptions({
  319. template: '<div>{{ foo }} side {{ bar }}</div>',
  320. data: {
  321. foo: 'server',
  322. bar: '<span>rendering</span>'
  323. }
  324. }, result => {
  325. expect(result).toContain('<div data-server-rendered="true">server side &lt;span&gt;rendering&lt;/span&gt;</div>')
  326. done()
  327. })
  328. })
  329. it('v-html on root', done => {
  330. renderVmWithOptions({
  331. template: '<div v-html="text"></div>',
  332. data: {
  333. text: '<span>foo</span>'
  334. }
  335. }, result => {
  336. expect(result).toContain('<div data-server-rendered="true"><span>foo</span></div>')
  337. done()
  338. })
  339. })
  340. it('v-text on root', done => {
  341. renderVmWithOptions({
  342. template: '<div v-text="text"></div>',
  343. data: {
  344. text: '<span>foo</span>'
  345. }
  346. }, result => {
  347. expect(result).toContain('<div data-server-rendered="true">&lt;span&gt;foo&lt;/span&gt;</div>')
  348. done()
  349. })
  350. })
  351. it('v-html', done => {
  352. renderVmWithOptions({
  353. template: '<div><div v-html="text"></div></div>',
  354. data: {
  355. text: '<span>foo</span>'
  356. }
  357. }, result => {
  358. expect(result).toContain('<div data-server-rendered="true"><div><span>foo</span></div></div>')
  359. done()
  360. })
  361. })
  362. it('v-html with null value', done => {
  363. renderVmWithOptions({
  364. template: '<div><div v-html="text"></div></div>',
  365. data: {
  366. text: null
  367. }
  368. }, result => {
  369. expect(result).toContain('<div data-server-rendered="true"><div></div></div>')
  370. done()
  371. })
  372. })
  373. it('v-text', done => {
  374. renderVmWithOptions({
  375. template: '<div><div v-text="text"></div></div>',
  376. data: {
  377. text: '<span>foo</span>'
  378. }
  379. }, result => {
  380. expect(result).toContain('<div data-server-rendered="true"><div>&lt;span&gt;foo&lt;/span&gt;</div></div>')
  381. done()
  382. })
  383. })
  384. it('v-text with null value', done => {
  385. renderVmWithOptions({
  386. template: '<div><div v-text="text"></div></div>',
  387. data: {
  388. text: null
  389. }
  390. }, result => {
  391. expect(result).toContain('<div data-server-rendered="true"><div></div></div>')
  392. done()
  393. })
  394. })
  395. it('child component (hoc)', done => {
  396. renderVmWithOptions({
  397. template: '<child class="foo" :msg="msg"></child>',
  398. data: {
  399. msg: 'hello'
  400. },
  401. components: {
  402. child: {
  403. props: ['msg'],
  404. data () {
  405. return { name: 'bar' }
  406. },
  407. render () {
  408. const h = this.$createElement
  409. return h('div', { class: ['bar'] }, [`${this.msg} ${this.name}`])
  410. }
  411. }
  412. }
  413. }, result => {
  414. expect(result).toContain('<div data-server-rendered="true" class="foo bar">hello bar</div>')
  415. done()
  416. })
  417. })
  418. it('has correct lifecycle during render', done => {
  419. let lifecycleCount = 1
  420. renderVmWithOptions({
  421. template: '<div><span>{{ val }}</span><test></test></div>',
  422. data: {
  423. val: 'hi'
  424. },
  425. beforeCreate () {
  426. expect(lifecycleCount++).toBe(1)
  427. },
  428. created () {
  429. this.val = 'hello'
  430. expect(this.val).toBe('hello')
  431. expect(lifecycleCount++).toBe(2)
  432. },
  433. components: {
  434. test: {
  435. beforeCreate () {
  436. expect(lifecycleCount++).toBe(3)
  437. },
  438. created () {
  439. expect(lifecycleCount++).toBe(4)
  440. },
  441. render () {
  442. expect(lifecycleCount++).toBeGreaterThan(4)
  443. return this.$createElement('span', { class: ['b'] }, 'testAsync')
  444. }
  445. }
  446. }
  447. }, result => {
  448. expect(result).toContain(
  449. '<div data-server-rendered="true">' +
  450. '<span>hello</span>' +
  451. '<span class="b">testAsync</span>' +
  452. '</div>'
  453. )
  454. done()
  455. })
  456. })
  457. it('computed properties', done => {
  458. renderVmWithOptions({
  459. template: '<div>{{ b }}</div>',
  460. data: {
  461. a: {
  462. b: 1
  463. }
  464. },
  465. computed: {
  466. b () {
  467. return this.a.b + 1
  468. }
  469. },
  470. created () {
  471. this.a.b = 2
  472. expect(this.b).toBe(3)
  473. }
  474. }, result => {
  475. expect(result).toContain('<div data-server-rendered="true">3</div>')
  476. done()
  477. })
  478. })
  479. it('renders async component', done => {
  480. renderVmWithOptions({
  481. template: `
  482. <div>
  483. <test-async></test-async>
  484. </div>
  485. `,
  486. components: {
  487. testAsync (resolve) {
  488. setTimeout(() => resolve({
  489. render () {
  490. return this.$createElement('span', { class: ['b'] }, 'testAsync')
  491. }
  492. }), 1)
  493. }
  494. }
  495. }, result => {
  496. expect(result).toContain('<div data-server-rendered="true"><span class="b">testAsync</span></div>')
  497. done()
  498. })
  499. })
  500. it('renders async component (Promise, nested)', done => {
  501. const Foo = () => Promise.resolve({
  502. render: h => h('div', [h('span', 'foo'), h(Bar)])
  503. })
  504. const Bar = () => ({
  505. component: Promise.resolve({
  506. render: h => h('span', 'bar')
  507. })
  508. })
  509. renderVmWithOptions({
  510. render: h => h(Foo)
  511. }, res => {
  512. expect(res).toContain(`<div data-server-rendered="true"><span>foo</span><span>bar</span></div>`)
  513. done()
  514. })
  515. })
  516. it('renders async component (ES module)', done => {
  517. const Foo = () => Promise.resolve({
  518. __esModule: true,
  519. default: {
  520. render: h => h('div', [h('span', 'foo'), h(Bar)])
  521. }
  522. })
  523. const Bar = () => ({
  524. component: Promise.resolve({
  525. __esModule: true,
  526. default: {
  527. render: h => h('span', 'bar')
  528. }
  529. })
  530. })
  531. renderVmWithOptions({
  532. render: h => h(Foo)
  533. }, res => {
  534. expect(res).toContain(`<div data-server-rendered="true"><span>foo</span><span>bar</span></div>`)
  535. done()
  536. })
  537. })
  538. it('renders async component (hoc)', done => {
  539. renderVmWithOptions({
  540. template: '<test-async></test-async>',
  541. components: {
  542. testAsync: () => Promise.resolve({
  543. render () {
  544. return this.$createElement('span', { class: ['b'] }, 'testAsync')
  545. }
  546. })
  547. }
  548. }, result => {
  549. expect(result).toContain('<span data-server-rendered="true" class="b">testAsync</span>')
  550. done()
  551. })
  552. })
  553. it('renders async component (functional, single node)', done => {
  554. renderVmWithOptions({
  555. template: `
  556. <div>
  557. <test-async></test-async>
  558. </div>
  559. `,
  560. components: {
  561. testAsync (resolve) {
  562. setTimeout(() => resolve({
  563. functional: true,
  564. render (h) {
  565. return h('span', { class: ['b'] }, 'testAsync')
  566. }
  567. }), 1)
  568. }
  569. }
  570. }, result => {
  571. expect(result).toContain('<div data-server-rendered="true"><span class="b">testAsync</span></div>')
  572. done()
  573. })
  574. })
  575. it('renders async component (functional, multiple nodes)', done => {
  576. renderVmWithOptions({
  577. template: `
  578. <div>
  579. <test-async></test-async>
  580. </div>
  581. `,
  582. components: {
  583. testAsync (resolve) {
  584. setTimeout(() => resolve({
  585. functional: true,
  586. render (h) {
  587. return [
  588. h('span', { class: ['a'] }, 'foo'),
  589. h('span', { class: ['b'] }, 'bar')
  590. ]
  591. }
  592. }), 1)
  593. }
  594. }
  595. }, result => {
  596. expect(result).toContain(
  597. '<div data-server-rendered="true">' +
  598. '<span class="a">foo</span>' +
  599. '<span class="b">bar</span>' +
  600. '</div>'
  601. )
  602. done()
  603. })
  604. })
  605. it('should catch async component error', done => {
  606. Vue.config.silent = true
  607. renderToString(new Vue({
  608. template: '<test-async></test-async>',
  609. components: {
  610. testAsync: () => Promise.resolve({
  611. render () {
  612. throw new Error('foo')
  613. }
  614. })
  615. }
  616. }), (err, result) => {
  617. Vue.config.silent = false
  618. expect(err).toBeTruthy()
  619. expect(result).toBeUndefined()
  620. done()
  621. })
  622. })
  623. it('everything together', done => {
  624. renderVmWithOptions({
  625. template: `
  626. <div>
  627. <p class="hi">yoyo</p>
  628. <div id="ho" :class="{ red: isRed }"></div>
  629. <span>{{ test }}</span>
  630. <input :value="test">
  631. <img :src="imageUrl">
  632. <test></test>
  633. <test-async></test-async>
  634. </div>
  635. `,
  636. data: {
  637. test: 'hi',
  638. isRed: true,
  639. imageUrl: 'https://vuejs.org/images/logo.png'
  640. },
  641. components: {
  642. test: {
  643. render () {
  644. return this.$createElement('div', { class: ['a'] }, 'test')
  645. }
  646. },
  647. testAsync (resolve) {
  648. resolve({
  649. render () {
  650. return this.$createElement('span', { class: ['b'] }, 'testAsync')
  651. }
  652. })
  653. }
  654. }
  655. }, result => {
  656. expect(result).toContain(
  657. '<div data-server-rendered="true">' +
  658. '<p class="hi">yoyo</p> ' +
  659. '<div id="ho" class="red"></div> ' +
  660. '<span>hi</span> ' +
  661. '<input value="hi"> ' +
  662. '<img src="https://vuejs.org/images/logo.png"> ' +
  663. '<div class="a">test</div> ' +
  664. '<span class="b">testAsync</span>' +
  665. '</div>'
  666. )
  667. done()
  668. })
  669. })
  670. it('normal attr', done => {
  671. renderVmWithOptions({
  672. template: `
  673. <div>
  674. <span :test="'ok'">hello</span>
  675. <span :test="null">hello</span>
  676. <span :test="false">hello</span>
  677. <span :test="true">hello</span>
  678. <span :test="0">hello</span>
  679. </div>
  680. `
  681. }, result => {
  682. expect(result).toContain(
  683. '<div data-server-rendered="true">' +
  684. '<span test="ok">hello</span> ' +
  685. '<span>hello</span> ' +
  686. '<span>hello</span> ' +
  687. '<span test="true">hello</span> ' +
  688. '<span test="0">hello</span>' +
  689. '</div>'
  690. )
  691. done()
  692. })
  693. })
  694. it('enumerated attr', done => {
  695. renderVmWithOptions({
  696. template: `
  697. <div>
  698. <span :draggable="true">hello</span>
  699. <span :draggable="'ok'">hello</span>
  700. <span :draggable="null">hello</span>
  701. <span :draggable="false">hello</span>
  702. <span :draggable="''">hello</span>
  703. <span :draggable="'false'">hello</span>
  704. </div>
  705. `
  706. }, result => {
  707. expect(result).toContain(
  708. '<div data-server-rendered="true">' +
  709. '<span draggable="true">hello</span> ' +
  710. '<span draggable="true">hello</span> ' +
  711. '<span draggable="false">hello</span> ' +
  712. '<span draggable="false">hello</span> ' +
  713. '<span draggable="true">hello</span> ' +
  714. '<span draggable="false">hello</span>' +
  715. '</div>'
  716. )
  717. done()
  718. })
  719. })
  720. it('boolean attr', done => {
  721. renderVmWithOptions({
  722. template: `
  723. <div>
  724. <span :disabled="true">hello</span>
  725. <span :disabled="'ok'">hello</span>
  726. <span :disabled="null">hello</span>
  727. <span :disabled="''">hello</span>
  728. </div>
  729. `
  730. }, result => {
  731. expect(result).toContain(
  732. '<div data-server-rendered="true">' +
  733. '<span disabled="disabled">hello</span> ' +
  734. '<span disabled="disabled">hello</span> ' +
  735. '<span>hello</span> ' +
  736. '<span disabled="disabled">hello</span>' +
  737. '</div>'
  738. )
  739. done()
  740. })
  741. })
  742. it('v-bind object', done => {
  743. renderVmWithOptions({
  744. data: {
  745. test: { id: 'a', class: ['a', 'b'], value: 'c' }
  746. },
  747. template: '<input v-bind="test">'
  748. }, result => {
  749. expect(result).toContain('<input id="a" data-server-rendered="true" value="c" class="a b">')
  750. done()
  751. })
  752. })
  753. it('custom directives', done => {
  754. const renderer = createRenderer({
  755. directives: {
  756. 'class-prefixer': (node, dir) => {
  757. if (node.data.class) {
  758. node.data.class = `${dir.value}-${node.data.class}`
  759. }
  760. if (node.data.staticClass) {
  761. node.data.staticClass = `${dir.value}-${node.data.staticClass}`
  762. }
  763. }
  764. }
  765. })
  766. renderer.renderToString(new Vue({
  767. render () {
  768. const h = this.$createElement
  769. return h('p', {
  770. class: 'class1',
  771. staticClass: 'class2',
  772. directives: [{
  773. name: 'class-prefixer',
  774. value: 'my'
  775. }]
  776. }, ['hello world'])
  777. }
  778. }), (err, result) => {
  779. expect(err).toBeNull()
  780. expect(result).toContain('<p data-server-rendered="true" class="my-class2 my-class1">hello world</p>')
  781. done()
  782. })
  783. })
  784. it('_scopeId', done => {
  785. renderVmWithOptions({
  786. _scopeId: '_v-parent',
  787. template: '<div id="foo"><p><child></child></p></div>',
  788. components: {
  789. child: {
  790. _scopeId: '_v-child',
  791. render () {
  792. const h = this.$createElement
  793. return h('div', null, [h('span', null, ['foo'])])
  794. }
  795. }
  796. }
  797. }, result => {
  798. expect(result).toContain(
  799. '<div id="foo" data-server-rendered="true" _v-parent>' +
  800. '<p _v-parent>' +
  801. '<div _v-child _v-parent><span _v-child>foo</span></div>' +
  802. '</p>' +
  803. '</div>'
  804. )
  805. done()
  806. })
  807. })
  808. it('_scopeId on slot content', done => {
  809. renderVmWithOptions({
  810. _scopeId: '_v-parent',
  811. template: '<div><child><p>foo</p></child></div>',
  812. components: {
  813. child: {
  814. _scopeId: '_v-child',
  815. render () {
  816. const h = this.$createElement
  817. return h('div', null, this.$slots.default)
  818. }
  819. }
  820. }
  821. }, result => {
  822. expect(result).toContain(
  823. '<div data-server-rendered="true" _v-parent>' +
  824. '<div _v-child _v-parent><p _v-child _v-parent>foo</p></div>' +
  825. '</div>'
  826. )
  827. done()
  828. })
  829. })
  830. it('comment nodes', done => {
  831. renderVmWithOptions({
  832. template: '<div><transition><div v-if="false"></div></transition></div>'
  833. }, result => {
  834. expect(result).toContain(`<div data-server-rendered="true"><!----></div>`)
  835. done()
  836. })
  837. })
  838. it('should catch error', done => {
  839. Vue.config.silent = true
  840. renderToString(new Vue({
  841. render () {
  842. throw new Error('oops')
  843. }
  844. }), err => {
  845. expect(err instanceof Error).toBe(true)
  846. Vue.config.silent = false
  847. done()
  848. })
  849. })
  850. it('default value Foreign Function', () => {
  851. const FunctionConstructor = VM.runInNewContext('Function')
  852. const func = () => 123
  853. const vm = new Vue({
  854. props: {
  855. a: {
  856. type: FunctionConstructor,
  857. default: func
  858. }
  859. },
  860. propsData: {
  861. a: undefined
  862. }
  863. })
  864. expect(vm.a).toBe(func)
  865. })
  866. it('should prevent xss in attributes', done => {
  867. renderVmWithOptions({
  868. data: {
  869. xss: '"><script>alert(1)</script>'
  870. },
  871. template: `
  872. <div>
  873. <a :title="xss" :style="{ color: xss }" :class="[xss]">foo</a>
  874. </div>
  875. `
  876. }, res => {
  877. expect(res).not.toContain(`<script>alert(1)</script>`)
  878. done()
  879. })
  880. })
  881. it('should prevent xss in attribute names', done => {
  882. renderVmWithOptions({
  883. data: {
  884. xss: {
  885. 'foo="bar"></div><script>alert(1)</script>': ''
  886. }
  887. },
  888. template: `
  889. <div v-bind="xss"></div>
  890. `
  891. }, res => {
  892. expect(res).not.toContain(`<script>alert(1)</script>`)
  893. done()
  894. })
  895. })
  896. it('should prevent xss in attribute names (optimized)', done => {
  897. renderVmWithOptions({
  898. data: {
  899. xss: {
  900. 'foo="bar"></div><script>alert(1)</script>': ''
  901. }
  902. },
  903. template: `
  904. <div>
  905. <a v-bind="xss">foo</a>
  906. </div>
  907. `
  908. }, res => {
  909. expect(res).not.toContain(`<script>alert(1)</script>`)
  910. done()
  911. })
  912. })
  913. it('should prevent script xss with v-bind object syntax + array value', done => {
  914. renderVmWithOptions({
  915. data: {
  916. test: ['"><script>alert(1)</script><!--"']
  917. },
  918. template: `<div v-bind="{ test }"></div>`
  919. }, res => {
  920. expect(res).not.toContain(`<script>alert(1)</script>`)
  921. done()
  922. })
  923. })
  924. it('v-if', done => {
  925. renderVmWithOptions({
  926. template: `
  927. <div>
  928. <span v-if="true">foo</span>
  929. <span v-if="false">bar</span>
  930. </div>
  931. `
  932. }, res => {
  933. expect(res).toContain(`<div data-server-rendered="true"><span>foo</span> <!----></div>`)
  934. done()
  935. })
  936. })
  937. it('v-for', done => {
  938. renderVmWithOptions({
  939. template: `
  940. <div>
  941. <span>foo</span>
  942. <span v-for="i in 2">{{ i }}</span>
  943. </div>
  944. `
  945. }, res => {
  946. expect(res).toContain(`<div data-server-rendered="true"><span>foo</span> <span>1</span><span>2</span></div>`)
  947. done()
  948. })
  949. })
  950. it('template v-if', done => {
  951. renderVmWithOptions({
  952. template: `
  953. <div>
  954. <span>foo</span>
  955. <template v-if="true">
  956. <span>foo</span> bar <span>baz</span>
  957. </template>
  958. </div>
  959. `
  960. }, res => {
  961. expect(res).toContain(`<div data-server-rendered="true"><span>foo</span> <span>foo</span> bar <span>baz</span></div>`)
  962. done()
  963. })
  964. })
  965. it('template v-for', done => {
  966. renderVmWithOptions({
  967. template: `
  968. <div>
  969. <span>foo</span>
  970. <template v-for="i in 2">
  971. <span>{{ i }}</span><span>bar</span>
  972. </template>
  973. </div>
  974. `
  975. }, res => {
  976. expect(res).toContain(`<div data-server-rendered="true"><span>foo</span> <span>1</span><span>bar</span><span>2</span><span>bar</span></div>`)
  977. done()
  978. })
  979. })
  980. it('with inheritAttrs: false + $attrs', done => {
  981. renderVmWithOptions({
  982. template: `<foo id="a"/>`,
  983. components: {
  984. foo: {
  985. inheritAttrs: false,
  986. template: `<div><div v-bind="$attrs"></div></div>`
  987. }
  988. }
  989. }, res => {
  990. expect(res).toBe(`<div data-server-rendered="true"><div id="a"></div></div>`)
  991. done()
  992. })
  993. })
  994. it('should escape static strings', done => {
  995. renderVmWithOptions({
  996. template: `<div>&lt;foo&gt;</div>`
  997. }, res => {
  998. expect(res).toBe(`<div data-server-rendered="true">&lt;foo&gt;</div>`)
  999. done()
  1000. })
  1001. })
  1002. it('should not cache computed properties', done => {
  1003. renderVmWithOptions({
  1004. template: `<div>{{ foo }}</div>`,
  1005. data: () => ({ bar: 1 }),
  1006. computed: {
  1007. foo () { return this.bar + 1 }
  1008. },
  1009. created () {
  1010. this.foo // access
  1011. this.bar++ // trigger change
  1012. }
  1013. }, res => {
  1014. expect(res).toBe(`<div data-server-rendered="true">3</div>`)
  1015. done()
  1016. })
  1017. })
  1018. it('return Promise', done => {
  1019. renderToString(new Vue({
  1020. template: `<div>{{ foo }}</div>`,
  1021. data: { foo: 'bar' }
  1022. })).then(res => {
  1023. expect(res).toBe(`<div data-server-rendered="true">bar</div>`)
  1024. done()
  1025. })
  1026. })
  1027. it('return Promise (error)', done => {
  1028. Vue.config.silent = true
  1029. renderToString(new Vue({
  1030. render () {
  1031. throw new Error('foobar')
  1032. }
  1033. })).catch(err => {
  1034. expect(err.toString()).toContain(`foobar`)
  1035. Vue.config.silent = false
  1036. done()
  1037. })
  1038. })
  1039. it('should catch template compilation error', done => {
  1040. renderToString(new Vue({
  1041. template: `<div></div><div></div>`
  1042. }), (err, res) => {
  1043. expect(err.toString()).toContain('Component template should contain exactly one root element')
  1044. done()
  1045. })
  1046. })
  1047. // #6907
  1048. it('should not optimize root if conditions', done => {
  1049. renderVmWithOptions({
  1050. data: { foo: 123 },
  1051. template: `<input :type="'text'" v-model="foo">`
  1052. }, res => {
  1053. expect(res).toBe(`<input type="text" data-server-rendered="true" value="123">`)
  1054. done()
  1055. })
  1056. })
  1057. it('render muted properly', done => {
  1058. renderVmWithOptions({
  1059. template: '<video muted></video>'
  1060. }, result => {
  1061. expect(result).toContain('<video muted="muted" data-server-rendered="true"></video>')
  1062. done()
  1063. })
  1064. })
  1065. it('render v-model with textarea', done => {
  1066. renderVmWithOptions({
  1067. data: { foo: 'bar' },
  1068. template: '<div><textarea v-model="foo"></textarea></div>'
  1069. }, result => {
  1070. expect(result).toContain('<textarea>bar</textarea>')
  1071. done()
  1072. })
  1073. })
  1074. it('render v-model with textarea (non-optimized)', done => {
  1075. renderVmWithOptions({
  1076. render (h) {
  1077. return h('textarea', {
  1078. domProps: {
  1079. value: 'foo'
  1080. }
  1081. })
  1082. }
  1083. }, result => {
  1084. expect(result).toContain('<textarea data-server-rendered="true">foo</textarea>')
  1085. done()
  1086. })
  1087. })
  1088. it('render v-model with <select> (value binding)', done => {
  1089. renderVmWithOptions({
  1090. data: {
  1091. selected: 2,
  1092. options: [
  1093. { id: 1, label: 'one' },
  1094. { id: 2, label: 'two' }
  1095. ]
  1096. },
  1097. template: `
  1098. <div>
  1099. <select v-model="selected">
  1100. <option v-for="o in options" :value="o.id">{{ o.label }}</option>
  1101. </select>
  1102. </div>
  1103. `
  1104. }, result => {
  1105. expect(result).toContain(
  1106. '<select>' +
  1107. '<option value="1">one</option>' +
  1108. '<option selected="selected" value="2">two</option>' +
  1109. '</select>'
  1110. )
  1111. done()
  1112. })
  1113. })
  1114. it('render v-model with <select> (static value)', done => {
  1115. renderVmWithOptions({
  1116. data: {
  1117. selected: 2
  1118. },
  1119. template: `
  1120. <div>
  1121. <select v-model="selected">
  1122. <option value="1">one</option>
  1123. <option value="2">two</option>
  1124. </select>
  1125. </div>
  1126. `
  1127. }, result => {
  1128. expect(result).toContain(
  1129. '<select>' +
  1130. '<option value="1">one</option> ' +
  1131. '<option value="2" selected="selected">two</option>' +
  1132. '</select>'
  1133. )
  1134. done()
  1135. })
  1136. })
  1137. it('render v-model with <select> (text as value)', done => {
  1138. renderVmWithOptions({
  1139. data: {
  1140. selected: 2,
  1141. options: [
  1142. { id: 1, label: 'one' },
  1143. { id: 2, label: 'two' }
  1144. ]
  1145. },
  1146. template: `
  1147. <div>
  1148. <select v-model="selected">
  1149. <option v-for="o in options">{{ o.id }}</option>
  1150. </select>
  1151. </div>
  1152. `
  1153. }, result => {
  1154. expect(result).toContain(
  1155. '<select>' +
  1156. '<option>1</option>' +
  1157. '<option selected="selected">2</option>' +
  1158. '</select>'
  1159. )
  1160. done()
  1161. })
  1162. })
  1163. // #7223
  1164. it('should not double escape attribute values', done => {
  1165. renderVmWithOptions({
  1166. template: `
  1167. <div>
  1168. <div id="a\nb"></div>
  1169. </div>
  1170. `
  1171. }, result => {
  1172. expect(result).toContain(`<div id="a\nb"></div>`)
  1173. done()
  1174. })
  1175. })
  1176. // #7859
  1177. it('should not double escape class values', done => {
  1178. renderVmWithOptions({
  1179. template: `
  1180. <div>
  1181. <div class="a\nb"></div>
  1182. </div>
  1183. `
  1184. }, result => {
  1185. expect(result).toContain(`<div class="a\nb"></div>`)
  1186. done()
  1187. })
  1188. })
  1189. it('should expose ssr helpers on functional context', done => {
  1190. let called = false
  1191. renderVmWithOptions({
  1192. template: `<div><foo/></div>`,
  1193. components: {
  1194. foo: {
  1195. functional: true,
  1196. render (h, ctx) {
  1197. expect(ctx._ssrNode).toBeTruthy()
  1198. called = true
  1199. }
  1200. }
  1201. }
  1202. }, () => {
  1203. expect(called).toBe(true)
  1204. done()
  1205. })
  1206. })
  1207. })
  1208. function renderVmWithOptions (options, cb) {
  1209. renderToString(new Vue(options), (err, res) => {
  1210. expect(err).toBeNull()
  1211. cb(res)
  1212. })
  1213. }