ssr-string.spec.js 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550
  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('should not warn for custom directives that do not have server-side implementation', done => {
  785. renderToString(new Vue({
  786. directives: {
  787. test: {
  788. bind() {
  789. // noop
  790. }
  791. }
  792. },
  793. template: '<div v-test></div>',
  794. }), () => {
  795. expect('Failed to resolve directive: test').not.toHaveBeenWarned()
  796. done()
  797. })
  798. })
  799. it('_scopeId', done => {
  800. renderVmWithOptions({
  801. _scopeId: '_v-parent',
  802. template: '<div id="foo"><p><child></child></p></div>',
  803. components: {
  804. child: {
  805. _scopeId: '_v-child',
  806. render () {
  807. const h = this.$createElement
  808. return h('div', null, [h('span', null, ['foo'])])
  809. }
  810. }
  811. }
  812. }, result => {
  813. expect(result).toContain(
  814. '<div id="foo" data-server-rendered="true" _v-parent>' +
  815. '<p _v-parent>' +
  816. '<div _v-child _v-parent><span _v-child>foo</span></div>' +
  817. '</p>' +
  818. '</div>'
  819. )
  820. done()
  821. })
  822. })
  823. it('_scopeId on slot content', done => {
  824. renderVmWithOptions({
  825. _scopeId: '_v-parent',
  826. template: '<div><child><p>foo</p></child></div>',
  827. components: {
  828. child: {
  829. _scopeId: '_v-child',
  830. render () {
  831. const h = this.$createElement
  832. return h('div', null, this.$slots.default)
  833. }
  834. }
  835. }
  836. }, result => {
  837. expect(result).toContain(
  838. '<div data-server-rendered="true" _v-parent>' +
  839. '<div _v-child _v-parent><p _v-child _v-parent>foo</p></div>' +
  840. '</div>'
  841. )
  842. done()
  843. })
  844. })
  845. it('comment nodes', done => {
  846. renderVmWithOptions({
  847. template: '<div><transition><div v-if="false"></div></transition></div>'
  848. }, result => {
  849. expect(result).toContain(`<div data-server-rendered="true"><!----></div>`)
  850. done()
  851. })
  852. })
  853. it('should catch error', done => {
  854. Vue.config.silent = true
  855. renderToString(new Vue({
  856. render () {
  857. throw new Error('oops')
  858. }
  859. }), err => {
  860. expect(err instanceof Error).toBe(true)
  861. Vue.config.silent = false
  862. done()
  863. })
  864. })
  865. it('default value Foreign Function', () => {
  866. const FunctionConstructor = VM.runInNewContext('Function')
  867. const func = () => 123
  868. const vm = new Vue({
  869. props: {
  870. a: {
  871. type: FunctionConstructor,
  872. default: func
  873. }
  874. },
  875. propsData: {
  876. a: undefined
  877. }
  878. })
  879. expect(vm.a).toBe(func)
  880. })
  881. it('should prevent xss in attributes', done => {
  882. renderVmWithOptions({
  883. data: {
  884. xss: '"><script>alert(1)</script>'
  885. },
  886. template: `
  887. <div>
  888. <a :title="xss" :style="{ color: xss }" :class="[xss]">foo</a>
  889. </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', done => {
  897. renderVmWithOptions({
  898. data: {
  899. xss: {
  900. 'foo="bar"></div><script>alert(1)</script>': ''
  901. }
  902. },
  903. template: `
  904. <div v-bind="xss"></div>
  905. `
  906. }, res => {
  907. expect(res).not.toContain(`<script>alert(1)</script>`)
  908. done()
  909. })
  910. })
  911. it('should prevent xss in attribute names (optimized)', done => {
  912. renderVmWithOptions({
  913. data: {
  914. xss: {
  915. 'foo="bar"></div><script>alert(1)</script>': ''
  916. }
  917. },
  918. template: `
  919. <div>
  920. <a v-bind="xss">foo</a>
  921. </div>
  922. `
  923. }, res => {
  924. expect(res).not.toContain(`<script>alert(1)</script>`)
  925. done()
  926. })
  927. })
  928. it('should prevent script xss with v-bind object syntax + array value', done => {
  929. renderVmWithOptions({
  930. data: {
  931. test: ['"><script>alert(1)</script><!--"']
  932. },
  933. template: `<div v-bind="{ test }"></div>`
  934. }, res => {
  935. expect(res).not.toContain(`<script>alert(1)</script>`)
  936. done()
  937. })
  938. })
  939. it('v-if', done => {
  940. renderVmWithOptions({
  941. template: `
  942. <div>
  943. <span v-if="true">foo</span>
  944. <span v-if="false">bar</span>
  945. </div>
  946. `
  947. }, res => {
  948. expect(res).toContain(`<div data-server-rendered="true"><span>foo</span> <!----></div>`)
  949. done()
  950. })
  951. })
  952. it('v-for', done => {
  953. renderVmWithOptions({
  954. template: `
  955. <div>
  956. <span>foo</span>
  957. <span v-for="i in 2">{{ i }}</span>
  958. </div>
  959. `
  960. }, res => {
  961. expect(res).toContain(`<div data-server-rendered="true"><span>foo</span> <span>1</span><span>2</span></div>`)
  962. done()
  963. })
  964. })
  965. it('template v-if', done => {
  966. renderVmWithOptions({
  967. template: `
  968. <div>
  969. <span>foo</span>
  970. <template v-if="true">
  971. <span>foo</span> bar <span>baz</span>
  972. </template>
  973. </div>
  974. `
  975. }, res => {
  976. expect(res).toContain(`<div data-server-rendered="true"><span>foo</span> <span>foo</span> bar <span>baz</span></div>`)
  977. done()
  978. })
  979. })
  980. it('template v-for', done => {
  981. renderVmWithOptions({
  982. template: `
  983. <div>
  984. <span>foo</span>
  985. <template v-for="i in 2">
  986. <span>{{ i }}</span><span>bar</span>
  987. </template>
  988. </div>
  989. `
  990. }, res => {
  991. expect(res).toContain(`<div data-server-rendered="true"><span>foo</span> <span>1</span><span>bar</span><span>2</span><span>bar</span></div>`)
  992. done()
  993. })
  994. })
  995. it('with inheritAttrs: false + $attrs', done => {
  996. renderVmWithOptions({
  997. template: `<foo id="a"/>`,
  998. components: {
  999. foo: {
  1000. inheritAttrs: false,
  1001. template: `<div><div v-bind="$attrs"></div></div>`
  1002. }
  1003. }
  1004. }, res => {
  1005. expect(res).toBe(`<div data-server-rendered="true"><div id="a"></div></div>`)
  1006. done()
  1007. })
  1008. })
  1009. it('should escape static strings', done => {
  1010. renderVmWithOptions({
  1011. template: `<div>&lt;foo&gt;</div>`
  1012. }, res => {
  1013. expect(res).toBe(`<div data-server-rendered="true">&lt;foo&gt;</div>`)
  1014. done()
  1015. })
  1016. })
  1017. it('should not cache computed properties', done => {
  1018. renderVmWithOptions({
  1019. template: `<div>{{ foo }}</div>`,
  1020. data: () => ({ bar: 1 }),
  1021. computed: {
  1022. foo () { return this.bar + 1 }
  1023. },
  1024. created () {
  1025. this.foo // access
  1026. this.bar++ // trigger change
  1027. }
  1028. }, res => {
  1029. expect(res).toBe(`<div data-server-rendered="true">3</div>`)
  1030. done()
  1031. })
  1032. })
  1033. // #8977
  1034. it('should call computed properties with vm as first argument', done => {
  1035. renderToString(new Vue({
  1036. data: {
  1037. firstName: 'Evan',
  1038. lastName: 'You'
  1039. },
  1040. computed: {
  1041. fullName: ({ firstName, lastName }) => `${firstName} ${lastName}`,
  1042. },
  1043. template: '<div>{{ fullName }}</div>',
  1044. }), (err, result) => {
  1045. expect(err).toBeNull()
  1046. expect(result).toContain('<div data-server-rendered="true">Evan You</div>')
  1047. done()
  1048. })
  1049. })
  1050. it('return Promise', done => {
  1051. renderToString(new Vue({
  1052. template: `<div>{{ foo }}</div>`,
  1053. data: { foo: 'bar' }
  1054. })).then(res => {
  1055. expect(res).toBe(`<div data-server-rendered="true">bar</div>`)
  1056. done()
  1057. })
  1058. })
  1059. it('return Promise (error)', done => {
  1060. Vue.config.silent = true
  1061. renderToString(new Vue({
  1062. render () {
  1063. throw new Error('foobar')
  1064. }
  1065. })).catch(err => {
  1066. expect(err.toString()).toContain(`foobar`)
  1067. Vue.config.silent = false
  1068. done()
  1069. })
  1070. })
  1071. it('should catch template compilation error', done => {
  1072. renderToString(new Vue({
  1073. template: `<div></div><div></div>`
  1074. }), (err) => {
  1075. expect(err.toString()).toContain('Component template should contain exactly one root element')
  1076. done()
  1077. })
  1078. })
  1079. // #6907
  1080. it('should not optimize root if conditions', done => {
  1081. renderVmWithOptions({
  1082. data: { foo: 123 },
  1083. template: `<input :type="'text'" v-model="foo">`
  1084. }, res => {
  1085. expect(res).toBe(`<input type="text" data-server-rendered="true" value="123">`)
  1086. done()
  1087. })
  1088. })
  1089. it('render muted properly', done => {
  1090. renderVmWithOptions({
  1091. template: '<video muted></video>'
  1092. }, result => {
  1093. expect(result).toContain('<video muted="muted" data-server-rendered="true"></video>')
  1094. done()
  1095. })
  1096. })
  1097. it('render v-model with textarea', done => {
  1098. renderVmWithOptions({
  1099. data: { foo: 'bar' },
  1100. template: '<div><textarea v-model="foo"></textarea></div>'
  1101. }, result => {
  1102. expect(result).toContain('<textarea>bar</textarea>')
  1103. done()
  1104. })
  1105. })
  1106. it('render v-model with textarea (non-optimized)', done => {
  1107. renderVmWithOptions({
  1108. render (h) {
  1109. return h('textarea', {
  1110. domProps: {
  1111. value: 'foo'
  1112. }
  1113. })
  1114. }
  1115. }, result => {
  1116. expect(result).toContain('<textarea data-server-rendered="true">foo</textarea>')
  1117. done()
  1118. })
  1119. })
  1120. it('render v-model with <select> (value binding)', done => {
  1121. renderVmWithOptions({
  1122. data: {
  1123. selected: 2,
  1124. options: [
  1125. { id: 1, label: 'one' },
  1126. { id: 2, label: 'two' }
  1127. ]
  1128. },
  1129. template: `
  1130. <div>
  1131. <select v-model="selected">
  1132. <option v-for="o in options" :value="o.id">{{ o.label }}</option>
  1133. </select>
  1134. </div>
  1135. `
  1136. }, result => {
  1137. expect(result).toContain(
  1138. '<select>' +
  1139. '<option value="1">one</option>' +
  1140. '<option selected="selected" value="2">two</option>' +
  1141. '</select>'
  1142. )
  1143. done()
  1144. })
  1145. })
  1146. it('render v-model with <select> (static value)', done => {
  1147. renderVmWithOptions({
  1148. data: {
  1149. selected: 2
  1150. },
  1151. template: `
  1152. <div>
  1153. <select v-model="selected">
  1154. <option value="1">one</option>
  1155. <option value="2">two</option>
  1156. </select>
  1157. </div>
  1158. `
  1159. }, result => {
  1160. expect(result).toContain(
  1161. '<select>' +
  1162. '<option value="1">one</option> ' +
  1163. '<option value="2" selected="selected">two</option>' +
  1164. '</select>'
  1165. )
  1166. done()
  1167. })
  1168. })
  1169. it('render v-model with <select> (text as value)', done => {
  1170. renderVmWithOptions({
  1171. data: {
  1172. selected: 2,
  1173. options: [
  1174. { id: 1, label: 'one' },
  1175. { id: 2, label: 'two' }
  1176. ]
  1177. },
  1178. template: `
  1179. <div>
  1180. <select v-model="selected">
  1181. <option v-for="o in options">{{ o.id }}</option>
  1182. </select>
  1183. </div>
  1184. `
  1185. }, result => {
  1186. expect(result).toContain(
  1187. '<select>' +
  1188. '<option>1</option>' +
  1189. '<option selected="selected">2</option>' +
  1190. '</select>'
  1191. )
  1192. done()
  1193. })
  1194. })
  1195. // #7223
  1196. it('should not double escape attribute values', done => {
  1197. renderVmWithOptions({
  1198. template: `
  1199. <div>
  1200. <div id="a\nb"></div>
  1201. </div>
  1202. `
  1203. }, result => {
  1204. expect(result).toContain(`<div id="a\nb"></div>`)
  1205. done()
  1206. })
  1207. })
  1208. // #7859
  1209. it('should not double escape class values', done => {
  1210. renderVmWithOptions({
  1211. template: `
  1212. <div>
  1213. <div class="a\nb"></div>
  1214. </div>
  1215. `
  1216. }, result => {
  1217. expect(result).toContain(`<div class="a\nb"></div>`)
  1218. done()
  1219. })
  1220. })
  1221. it('should expose ssr helpers on functional context', done => {
  1222. let called = false
  1223. renderVmWithOptions({
  1224. template: `<div><foo/></div>`,
  1225. components: {
  1226. foo: {
  1227. functional: true,
  1228. render (h, ctx) {
  1229. expect(ctx._ssrNode).toBeTruthy()
  1230. called = true
  1231. }
  1232. }
  1233. }
  1234. }, () => {
  1235. expect(called).toBe(true)
  1236. done()
  1237. })
  1238. })
  1239. it('should support ssrPrefetch option', done => {
  1240. renderVmWithOptions({
  1241. template: `
  1242. <div>{{ count }}</div>
  1243. `,
  1244. data: {
  1245. count: 0
  1246. },
  1247. ssrPrefetch () {
  1248. return new Promise((resolve) => {
  1249. setTimeout(() => {
  1250. this.count = 42
  1251. resolve()
  1252. }, 1)
  1253. })
  1254. }
  1255. }, result => {
  1256. expect(result).toContain('<div data-server-rendered="true">42</div>')
  1257. done()
  1258. })
  1259. })
  1260. it('should support ssrPrefetch option (nested)', done => {
  1261. renderVmWithOptions({
  1262. template: `
  1263. <div>
  1264. <span>{{ count }}</span>
  1265. <nested-prefetch></nested-prefetch>
  1266. </div>
  1267. `,
  1268. data: {
  1269. count: 0
  1270. },
  1271. ssrPrefetch () {
  1272. return new Promise((resolve) => {
  1273. setTimeout(() => {
  1274. this.count = 42
  1275. resolve()
  1276. }, 1)
  1277. })
  1278. },
  1279. components: {
  1280. nestedPrefetch: {
  1281. template: `
  1282. <div>{{ message }}</div>
  1283. `,
  1284. data () {
  1285. return {
  1286. message: ''
  1287. }
  1288. },
  1289. ssrPrefetch () {
  1290. return new Promise((resolve) => {
  1291. setTimeout(() => {
  1292. this.message = 'vue.js'
  1293. resolve()
  1294. }, 1)
  1295. })
  1296. }
  1297. }
  1298. }
  1299. }, result => {
  1300. expect(result).toContain('<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>')
  1301. done()
  1302. })
  1303. })
  1304. it('should support ssrPrefetch option (nested async)', done => {
  1305. renderVmWithOptions({
  1306. template: `
  1307. <div>
  1308. <span>{{ count }}</span>
  1309. <nested-prefetch></nested-prefetch>
  1310. </div>
  1311. `,
  1312. data: {
  1313. count: 0
  1314. },
  1315. ssrPrefetch () {
  1316. return new Promise((resolve) => {
  1317. setTimeout(() => {
  1318. this.count = 42
  1319. resolve()
  1320. }, 1)
  1321. })
  1322. },
  1323. components: {
  1324. nestedPrefetch (resolve) {
  1325. resolve({
  1326. template: `
  1327. <div>{{ message }}</div>
  1328. `,
  1329. data () {
  1330. return {
  1331. message: ''
  1332. }
  1333. },
  1334. ssrPrefetch () {
  1335. return new Promise((resolve) => {
  1336. setTimeout(() => {
  1337. this.message = 'vue.js'
  1338. resolve()
  1339. }, 1)
  1340. })
  1341. }
  1342. })
  1343. }
  1344. }
  1345. }, result => {
  1346. expect(result).toContain('<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>')
  1347. done()
  1348. })
  1349. })
  1350. it('should merge ssrPrefetch option', done => {
  1351. const mixin = {
  1352. data: {
  1353. message: ''
  1354. },
  1355. ssrPrefetch () {
  1356. return new Promise((resolve) => {
  1357. setTimeout(() => {
  1358. this.message = 'vue.js'
  1359. resolve()
  1360. }, 1)
  1361. })
  1362. }
  1363. }
  1364. renderVmWithOptions({
  1365. mixins: [mixin],
  1366. template: `
  1367. <div>
  1368. <span>{{ count }}</span>
  1369. <div>{{ message }}</div>
  1370. </div>
  1371. `,
  1372. data: {
  1373. count: 0
  1374. },
  1375. ssrPrefetch () {
  1376. return new Promise((resolve) => {
  1377. setTimeout(() => {
  1378. this.count = 42
  1379. resolve()
  1380. }, 1)
  1381. })
  1382. }
  1383. }, result => {
  1384. expect(result).toContain('<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>')
  1385. done()
  1386. })
  1387. })
  1388. it(`should skip ssrPrefetch option that doesn't return a promise`, done => {
  1389. renderVmWithOptions({
  1390. template: `
  1391. <div>{{ count }}</div>
  1392. `,
  1393. data: {
  1394. count: 0
  1395. },
  1396. ssrPrefetch () {
  1397. setTimeout(() => {
  1398. this.count = 42
  1399. }, 1)
  1400. }
  1401. }, result => {
  1402. expect(result).toContain('<div data-server-rendered="true">0</div>')
  1403. done()
  1404. })
  1405. })
  1406. it('should call context.rendered', done => {
  1407. let a = 0
  1408. renderToString(new Vue({
  1409. template: '<div>Hello</div>'
  1410. }), {
  1411. rendered: () => {
  1412. a = 42
  1413. }
  1414. }, (err, res) => {
  1415. expect(err).toBeNull()
  1416. expect(res).toContain('<div data-server-rendered="true">Hello</div>')
  1417. expect(a).toBe(42)
  1418. done()
  1419. })
  1420. })
  1421. it('invalid style value', done => {
  1422. renderVmWithOptions({
  1423. template: '<div :style="style"><p :style="style2"/></div>',
  1424. data: {
  1425. // all invalid, should not even have "style" attribute
  1426. style: {
  1427. opacity: {},
  1428. color: null
  1429. },
  1430. // mix of valid and invalid
  1431. style2: {
  1432. opacity: 0,
  1433. color: null
  1434. }
  1435. }
  1436. }, result => {
  1437. expect(result).toContain(
  1438. '<div data-server-rendered="true"><p style="opacity:0;"></p></div>'
  1439. )
  1440. done()
  1441. })
  1442. })
  1443. it('numeric style value', done => {
  1444. renderVmWithOptions({
  1445. template: '<div :style="style"></div>',
  1446. data: {
  1447. style: {
  1448. opacity: 0, // valid, opacity is unit-less
  1449. top: 0, // invalid, top requires unit
  1450. marginTop: '10px' // valid
  1451. }
  1452. }
  1453. }, result => {
  1454. expect(result).toContain(
  1455. '<div data-server-rendered="true" style="opacity:0;margin-top:10px;"></div>'
  1456. )
  1457. done()
  1458. })
  1459. })
  1460. })
  1461. function renderVmWithOptions (options, cb) {
  1462. renderToString(new Vue(options), (err, res) => {
  1463. expect(err).toBeNull()
  1464. cb(res)
  1465. })
  1466. }