ssr-string.spec.js 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604
  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('renders nested async functional component', done => {
  606. renderVmWithOptions({
  607. template: `
  608. <div>
  609. <outer-async></outer-async>
  610. </div>
  611. `,
  612. components: {
  613. outerAsync (resolve) {
  614. setTimeout(() => resolve({
  615. functional: true,
  616. render (h) {
  617. return h('innerAsync')
  618. }
  619. }), 1)
  620. },
  621. innerAsync (resolve) {
  622. setTimeout(() => resolve({
  623. functional: true,
  624. render (h) {
  625. return h('span', { class: ['a'] }, 'inner')
  626. },
  627. }), 1)
  628. }
  629. }
  630. }, result => {
  631. expect(result).toContain(
  632. '<div data-server-rendered="true">' +
  633. '<span class="a">inner</span>' +
  634. '</div>'
  635. )
  636. done()
  637. })
  638. })
  639. it('should catch async component error', done => {
  640. Vue.config.silent = true
  641. renderToString(new Vue({
  642. template: '<test-async></test-async>',
  643. components: {
  644. testAsync: () => Promise.resolve({
  645. render () {
  646. throw new Error('foo')
  647. }
  648. })
  649. }
  650. }), (err, result) => {
  651. Vue.config.silent = false
  652. expect(err).toBeTruthy()
  653. expect(result).toBeUndefined()
  654. done()
  655. })
  656. })
  657. it('everything together', done => {
  658. renderVmWithOptions({
  659. template: `
  660. <div>
  661. <p class="hi">yoyo</p>
  662. <div id="ho" :class="{ red: isRed }"></div>
  663. <span>{{ test }}</span>
  664. <input :value="test">
  665. <img :src="imageUrl">
  666. <test></test>
  667. <test-async></test-async>
  668. </div>
  669. `,
  670. data: {
  671. test: 'hi',
  672. isRed: true,
  673. imageUrl: 'https://vuejs.org/images/logo.png'
  674. },
  675. components: {
  676. test: {
  677. render () {
  678. return this.$createElement('div', { class: ['a'] }, 'test')
  679. }
  680. },
  681. testAsync (resolve) {
  682. resolve({
  683. render () {
  684. return this.$createElement('span', { class: ['b'] }, 'testAsync')
  685. }
  686. })
  687. }
  688. }
  689. }, result => {
  690. expect(result).toContain(
  691. '<div data-server-rendered="true">' +
  692. '<p class="hi">yoyo</p> ' +
  693. '<div id="ho" class="red"></div> ' +
  694. '<span>hi</span> ' +
  695. '<input value="hi"> ' +
  696. '<img src="https://vuejs.org/images/logo.png"> ' +
  697. '<div class="a">test</div> ' +
  698. '<span class="b">testAsync</span>' +
  699. '</div>'
  700. )
  701. done()
  702. })
  703. })
  704. it('normal attr', done => {
  705. renderVmWithOptions({
  706. template: `
  707. <div>
  708. <span :test="'ok'">hello</span>
  709. <span :test="null">hello</span>
  710. <span :test="false">hello</span>
  711. <span :test="true">hello</span>
  712. <span :test="0">hello</span>
  713. </div>
  714. `
  715. }, result => {
  716. expect(result).toContain(
  717. '<div data-server-rendered="true">' +
  718. '<span test="ok">hello</span> ' +
  719. '<span>hello</span> ' +
  720. '<span>hello</span> ' +
  721. '<span test="true">hello</span> ' +
  722. '<span test="0">hello</span>' +
  723. '</div>'
  724. )
  725. done()
  726. })
  727. })
  728. it('enumerated attr', done => {
  729. renderVmWithOptions({
  730. template: `
  731. <div>
  732. <span :draggable="true">hello</span>
  733. <span :draggable="'ok'">hello</span>
  734. <span :draggable="null">hello</span>
  735. <span :draggable="false">hello</span>
  736. <span :draggable="''">hello</span>
  737. <span :draggable="'false'">hello</span>
  738. </div>
  739. `
  740. }, result => {
  741. expect(result).toContain(
  742. '<div data-server-rendered="true">' +
  743. '<span draggable="true">hello</span> ' +
  744. '<span draggable="true">hello</span> ' +
  745. '<span draggable="false">hello</span> ' +
  746. '<span draggable="false">hello</span> ' +
  747. '<span draggable="true">hello</span> ' +
  748. '<span draggable="false">hello</span>' +
  749. '</div>'
  750. )
  751. done()
  752. })
  753. })
  754. it('boolean attr', done => {
  755. renderVmWithOptions({
  756. template: `
  757. <div>
  758. <span :disabled="true">hello</span>
  759. <span :disabled="'ok'">hello</span>
  760. <span :disabled="null">hello</span>
  761. <span :disabled="''">hello</span>
  762. </div>
  763. `
  764. }, result => {
  765. expect(result).toContain(
  766. '<div data-server-rendered="true">' +
  767. '<span disabled="disabled">hello</span> ' +
  768. '<span disabled="disabled">hello</span> ' +
  769. '<span>hello</span> ' +
  770. '<span disabled="disabled">hello</span>' +
  771. '</div>'
  772. )
  773. done()
  774. })
  775. })
  776. it('v-bind object', done => {
  777. renderVmWithOptions({
  778. data: {
  779. test: { id: 'a', class: ['a', 'b'], value: 'c' }
  780. },
  781. template: '<input v-bind="test">'
  782. }, result => {
  783. expect(result).toContain('<input id="a" data-server-rendered="true" value="c" class="a b">')
  784. done()
  785. })
  786. })
  787. it('custom directives', done => {
  788. const renderer = createRenderer({
  789. directives: {
  790. 'class-prefixer': (node, dir) => {
  791. if (node.data.class) {
  792. node.data.class = `${dir.value}-${node.data.class}`
  793. }
  794. if (node.data.staticClass) {
  795. node.data.staticClass = `${dir.value}-${node.data.staticClass}`
  796. }
  797. }
  798. }
  799. })
  800. renderer.renderToString(new Vue({
  801. render () {
  802. const h = this.$createElement
  803. return h('p', {
  804. class: 'class1',
  805. staticClass: 'class2',
  806. directives: [{
  807. name: 'class-prefixer',
  808. value: 'my'
  809. }]
  810. }, ['hello world'])
  811. }
  812. }), (err, result) => {
  813. expect(err).toBeNull()
  814. expect(result).toContain('<p data-server-rendered="true" class="my-class2 my-class1">hello world</p>')
  815. done()
  816. })
  817. })
  818. it('should not warn for custom directives that do not have server-side implementation', done => {
  819. renderToString(new Vue({
  820. directives: {
  821. test: {
  822. bind() {
  823. // noop
  824. }
  825. }
  826. },
  827. template: '<div v-test></div>',
  828. }), () => {
  829. expect('Failed to resolve directive: test').not.toHaveBeenWarned()
  830. done()
  831. })
  832. })
  833. it('_scopeId', done => {
  834. renderVmWithOptions({
  835. _scopeId: '_v-parent',
  836. template: '<div id="foo"><p><child></child></p></div>',
  837. components: {
  838. child: {
  839. _scopeId: '_v-child',
  840. render () {
  841. const h = this.$createElement
  842. return h('div', null, [h('span', null, ['foo'])])
  843. }
  844. }
  845. }
  846. }, result => {
  847. expect(result).toContain(
  848. '<div id="foo" data-server-rendered="true" _v-parent>' +
  849. '<p _v-parent>' +
  850. '<div _v-child _v-parent><span _v-child>foo</span></div>' +
  851. '</p>' +
  852. '</div>'
  853. )
  854. done()
  855. })
  856. })
  857. it('_scopeId on slot content', done => {
  858. renderVmWithOptions({
  859. _scopeId: '_v-parent',
  860. template: '<div><child><p>foo</p></child></div>',
  861. components: {
  862. child: {
  863. _scopeId: '_v-child',
  864. render () {
  865. const h = this.$createElement
  866. return h('div', null, this.$slots.default)
  867. }
  868. }
  869. }
  870. }, result => {
  871. expect(result).toContain(
  872. '<div data-server-rendered="true" _v-parent>' +
  873. '<div _v-child _v-parent><p _v-child _v-parent>foo</p></div>' +
  874. '</div>'
  875. )
  876. done()
  877. })
  878. })
  879. it('comment nodes', done => {
  880. renderVmWithOptions({
  881. template: '<div><transition><div v-if="false"></div></transition></div>'
  882. }, result => {
  883. expect(result).toContain(`<div data-server-rendered="true"><!----></div>`)
  884. done()
  885. })
  886. })
  887. it('should catch error', done => {
  888. Vue.config.silent = true
  889. renderToString(new Vue({
  890. render () {
  891. throw new Error('oops')
  892. }
  893. }), err => {
  894. expect(err instanceof Error).toBe(true)
  895. Vue.config.silent = false
  896. done()
  897. })
  898. })
  899. it('default value Foreign Function', () => {
  900. const FunctionConstructor = VM.runInNewContext('Function')
  901. const func = () => 123
  902. const vm = new Vue({
  903. props: {
  904. a: {
  905. type: FunctionConstructor,
  906. default: func
  907. }
  908. },
  909. propsData: {
  910. a: undefined
  911. }
  912. })
  913. expect(vm.a).toBe(func)
  914. })
  915. it('should prevent xss in attributes', done => {
  916. renderVmWithOptions({
  917. data: {
  918. xss: '"><script>alert(1)</script>'
  919. },
  920. template: `
  921. <div>
  922. <a :title="xss" :style="{ color: xss }" :class="[xss]">foo</a>
  923. </div>
  924. `
  925. }, res => {
  926. expect(res).not.toContain(`<script>alert(1)</script>`)
  927. done()
  928. })
  929. })
  930. it('should prevent xss in attribute names', done => {
  931. renderVmWithOptions({
  932. data: {
  933. xss: {
  934. 'foo="bar"></div><script>alert(1)</script>': ''
  935. }
  936. },
  937. template: `
  938. <div v-bind="xss"></div>
  939. `
  940. }, res => {
  941. expect(res).not.toContain(`<script>alert(1)</script>`)
  942. done()
  943. })
  944. })
  945. it('should prevent xss in attribute names (optimized)', done => {
  946. renderVmWithOptions({
  947. data: {
  948. xss: {
  949. 'foo="bar"></div><script>alert(1)</script>': ''
  950. }
  951. },
  952. template: `
  953. <div>
  954. <a v-bind="xss">foo</a>
  955. </div>
  956. `
  957. }, res => {
  958. expect(res).not.toContain(`<script>alert(1)</script>`)
  959. done()
  960. })
  961. })
  962. it('should prevent script xss with v-bind object syntax + array value', done => {
  963. renderVmWithOptions({
  964. data: {
  965. test: ['"><script>alert(1)</script><!--"']
  966. },
  967. template: `<div v-bind="{ test }"></div>`
  968. }, res => {
  969. expect(res).not.toContain(`<script>alert(1)</script>`)
  970. done()
  971. })
  972. })
  973. it('v-if', done => {
  974. renderVmWithOptions({
  975. template: `
  976. <div>
  977. <span v-if="true">foo</span>
  978. <span v-if="false">bar</span>
  979. </div>
  980. `
  981. }, res => {
  982. expect(res).toContain(`<div data-server-rendered="true"><span>foo</span> <!----></div>`)
  983. done()
  984. })
  985. })
  986. it('v-for', done => {
  987. renderVmWithOptions({
  988. template: `
  989. <div>
  990. <span>foo</span>
  991. <span v-for="i in 2">{{ i }}</span>
  992. </div>
  993. `
  994. }, res => {
  995. expect(res).toContain(`<div data-server-rendered="true"><span>foo</span> <span>1</span><span>2</span></div>`)
  996. done()
  997. })
  998. })
  999. it('template v-if', done => {
  1000. renderVmWithOptions({
  1001. template: `
  1002. <div>
  1003. <span>foo</span>
  1004. <template v-if="true">
  1005. <span>foo</span> bar <span>baz</span>
  1006. </template>
  1007. </div>
  1008. `
  1009. }, res => {
  1010. expect(res).toContain(`<div data-server-rendered="true"><span>foo</span> <span>foo</span> bar <span>baz</span></div>`)
  1011. done()
  1012. })
  1013. })
  1014. it('template v-for', done => {
  1015. renderVmWithOptions({
  1016. template: `
  1017. <div>
  1018. <span>foo</span>
  1019. <template v-for="i in 2">
  1020. <span>{{ i }}</span><span>bar</span>
  1021. </template>
  1022. </div>
  1023. `
  1024. }, res => {
  1025. expect(res).toContain(`<div data-server-rendered="true"><span>foo</span> <span>1</span><span>bar</span><span>2</span><span>bar</span></div>`)
  1026. done()
  1027. })
  1028. })
  1029. it('with inheritAttrs: false + $attrs', done => {
  1030. renderVmWithOptions({
  1031. template: `<foo id="a"/>`,
  1032. components: {
  1033. foo: {
  1034. inheritAttrs: false,
  1035. template: `<div><div v-bind="$attrs"></div></div>`
  1036. }
  1037. }
  1038. }, res => {
  1039. expect(res).toBe(`<div data-server-rendered="true"><div id="a"></div></div>`)
  1040. done()
  1041. })
  1042. })
  1043. it('should escape static strings', done => {
  1044. renderVmWithOptions({
  1045. template: `<div>&lt;foo&gt;</div>`
  1046. }, res => {
  1047. expect(res).toBe(`<div data-server-rendered="true">&lt;foo&gt;</div>`)
  1048. done()
  1049. })
  1050. })
  1051. it('should not cache computed properties', done => {
  1052. renderVmWithOptions({
  1053. template: `<div>{{ foo }}</div>`,
  1054. data: () => ({ bar: 1 }),
  1055. computed: {
  1056. foo () { return this.bar + 1 }
  1057. },
  1058. created () {
  1059. this.foo // access
  1060. this.bar++ // trigger change
  1061. }
  1062. }, res => {
  1063. expect(res).toBe(`<div data-server-rendered="true">3</div>`)
  1064. done()
  1065. })
  1066. })
  1067. // #8977
  1068. it('should call computed properties with vm as first argument', done => {
  1069. renderToString(new Vue({
  1070. data: {
  1071. firstName: 'Evan',
  1072. lastName: 'You'
  1073. },
  1074. computed: {
  1075. fullName: ({ firstName, lastName }) => `${firstName} ${lastName}`,
  1076. },
  1077. template: '<div>{{ fullName }}</div>',
  1078. }), (err, result) => {
  1079. expect(err).toBeNull()
  1080. expect(result).toContain('<div data-server-rendered="true">Evan You</div>')
  1081. done()
  1082. })
  1083. })
  1084. it('return Promise', done => {
  1085. renderToString(new Vue({
  1086. template: `<div>{{ foo }}</div>`,
  1087. data: { foo: 'bar' }
  1088. })).then(res => {
  1089. expect(res).toBe(`<div data-server-rendered="true">bar</div>`)
  1090. done()
  1091. })
  1092. })
  1093. it('return Promise (error)', done => {
  1094. Vue.config.silent = true
  1095. renderToString(new Vue({
  1096. render () {
  1097. throw new Error('foobar')
  1098. }
  1099. })).catch(err => {
  1100. expect(err.toString()).toContain(`foobar`)
  1101. Vue.config.silent = false
  1102. done()
  1103. })
  1104. })
  1105. it('should catch template compilation error', done => {
  1106. renderToString(new Vue({
  1107. template: `<div></div><div></div>`
  1108. }), (err) => {
  1109. expect(err.toString()).toContain('Component template should contain exactly one root element')
  1110. done()
  1111. })
  1112. })
  1113. // #6907
  1114. it('should not optimize root if conditions', done => {
  1115. renderVmWithOptions({
  1116. data: { foo: 123 },
  1117. template: `<input :type="'text'" v-model="foo">`
  1118. }, res => {
  1119. expect(res).toBe(`<input type="text" data-server-rendered="true" value="123">`)
  1120. done()
  1121. })
  1122. })
  1123. it('render muted properly', done => {
  1124. renderVmWithOptions({
  1125. template: '<video muted></video>'
  1126. }, result => {
  1127. expect(result).toContain('<video muted="muted" data-server-rendered="true"></video>')
  1128. done()
  1129. })
  1130. })
  1131. it('render v-model with textarea', done => {
  1132. renderVmWithOptions({
  1133. data: { foo: 'bar' },
  1134. template: '<div><textarea v-model="foo"></textarea></div>'
  1135. }, result => {
  1136. expect(result).toContain('<textarea>bar</textarea>')
  1137. done()
  1138. })
  1139. })
  1140. it('render v-model with textarea (non-optimized)', done => {
  1141. renderVmWithOptions({
  1142. render (h) {
  1143. return h('textarea', {
  1144. domProps: {
  1145. value: 'foo'
  1146. }
  1147. })
  1148. }
  1149. }, result => {
  1150. expect(result).toContain('<textarea data-server-rendered="true">foo</textarea>')
  1151. done()
  1152. })
  1153. })
  1154. it('render v-model with <select> (value binding)', done => {
  1155. renderVmWithOptions({
  1156. data: {
  1157. selected: 2,
  1158. options: [
  1159. { id: 1, label: 'one' },
  1160. { id: 2, label: 'two' }
  1161. ]
  1162. },
  1163. template: `
  1164. <div>
  1165. <select v-model="selected">
  1166. <option v-for="o in options" :value="o.id">{{ o.label }}</option>
  1167. </select>
  1168. </div>
  1169. `
  1170. }, result => {
  1171. expect(result).toContain(
  1172. '<select>' +
  1173. '<option value="1">one</option>' +
  1174. '<option selected="selected" value="2">two</option>' +
  1175. '</select>'
  1176. )
  1177. done()
  1178. })
  1179. })
  1180. it('render v-model with <select> (static value)', done => {
  1181. renderVmWithOptions({
  1182. data: {
  1183. selected: 2
  1184. },
  1185. template: `
  1186. <div>
  1187. <select v-model="selected">
  1188. <option value="1">one</option>
  1189. <option value="2">two</option>
  1190. </select>
  1191. </div>
  1192. `
  1193. }, result => {
  1194. expect(result).toContain(
  1195. '<select>' +
  1196. '<option value="1">one</option> ' +
  1197. '<option value="2" selected="selected">two</option>' +
  1198. '</select>'
  1199. )
  1200. done()
  1201. })
  1202. })
  1203. it('render v-model with <select> (text as value)', done => {
  1204. renderVmWithOptions({
  1205. data: {
  1206. selected: 2,
  1207. options: [
  1208. { id: 1, label: 'one' },
  1209. { id: 2, label: 'two' }
  1210. ]
  1211. },
  1212. template: `
  1213. <div>
  1214. <select v-model="selected">
  1215. <option v-for="o in options">{{ o.id }}</option>
  1216. </select>
  1217. </div>
  1218. `
  1219. }, result => {
  1220. expect(result).toContain(
  1221. '<select>' +
  1222. '<option>1</option>' +
  1223. '<option selected="selected">2</option>' +
  1224. '</select>'
  1225. )
  1226. done()
  1227. })
  1228. })
  1229. // #7223
  1230. it('should not double escape attribute values', done => {
  1231. renderVmWithOptions({
  1232. template: `
  1233. <div>
  1234. <div id="a\nb"></div>
  1235. </div>
  1236. `
  1237. }, result => {
  1238. expect(result).toContain(`<div id="a\nb"></div>`)
  1239. done()
  1240. })
  1241. })
  1242. // #7859
  1243. it('should not double escape class values', done => {
  1244. renderVmWithOptions({
  1245. template: `
  1246. <div>
  1247. <div class="a\nb"></div>
  1248. </div>
  1249. `
  1250. }, result => {
  1251. expect(result).toContain(`<div class="a\nb"></div>`)
  1252. done()
  1253. })
  1254. })
  1255. it('should expose ssr helpers on functional context', done => {
  1256. let called = false
  1257. renderVmWithOptions({
  1258. template: `<div><foo/></div>`,
  1259. components: {
  1260. foo: {
  1261. functional: true,
  1262. render (h, ctx) {
  1263. expect(ctx._ssrNode).toBeTruthy()
  1264. called = true
  1265. }
  1266. }
  1267. }
  1268. }, () => {
  1269. expect(called).toBe(true)
  1270. done()
  1271. })
  1272. })
  1273. it('should support serverPrefetch option', done => {
  1274. renderVmWithOptions({
  1275. template: `
  1276. <div>{{ count }}</div>
  1277. `,
  1278. data: {
  1279. count: 0
  1280. },
  1281. serverPrefetch () {
  1282. return new Promise((resolve) => {
  1283. setTimeout(() => {
  1284. this.count = 42
  1285. resolve()
  1286. }, 1)
  1287. })
  1288. }
  1289. }, result => {
  1290. expect(result).toContain('<div data-server-rendered="true">42</div>')
  1291. done()
  1292. })
  1293. })
  1294. it('should support serverPrefetch option (nested)', done => {
  1295. renderVmWithOptions({
  1296. template: `
  1297. <div>
  1298. <span>{{ count }}</span>
  1299. <nested-prefetch></nested-prefetch>
  1300. </div>
  1301. `,
  1302. data: {
  1303. count: 0
  1304. },
  1305. serverPrefetch () {
  1306. return new Promise((resolve) => {
  1307. setTimeout(() => {
  1308. this.count = 42
  1309. resolve()
  1310. }, 1)
  1311. })
  1312. },
  1313. components: {
  1314. nestedPrefetch: {
  1315. template: `
  1316. <div>{{ message }}</div>
  1317. `,
  1318. data () {
  1319. return {
  1320. message: ''
  1321. }
  1322. },
  1323. serverPrefetch () {
  1324. return new Promise((resolve) => {
  1325. setTimeout(() => {
  1326. this.message = 'vue.js'
  1327. resolve()
  1328. }, 1)
  1329. })
  1330. }
  1331. }
  1332. }
  1333. }, result => {
  1334. expect(result).toContain('<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>')
  1335. done()
  1336. })
  1337. })
  1338. it('should support serverPrefetch option (nested async)', done => {
  1339. renderVmWithOptions({
  1340. template: `
  1341. <div>
  1342. <span>{{ count }}</span>
  1343. <nested-prefetch></nested-prefetch>
  1344. </div>
  1345. `,
  1346. data: {
  1347. count: 0
  1348. },
  1349. serverPrefetch () {
  1350. return new Promise((resolve) => {
  1351. setTimeout(() => {
  1352. this.count = 42
  1353. resolve()
  1354. }, 1)
  1355. })
  1356. },
  1357. components: {
  1358. nestedPrefetch (resolve) {
  1359. resolve({
  1360. template: `
  1361. <div>{{ message }}</div>
  1362. `,
  1363. data () {
  1364. return {
  1365. message: ''
  1366. }
  1367. },
  1368. serverPrefetch () {
  1369. return new Promise((resolve) => {
  1370. setTimeout(() => {
  1371. this.message = 'vue.js'
  1372. resolve()
  1373. }, 1)
  1374. })
  1375. }
  1376. })
  1377. }
  1378. }
  1379. }, result => {
  1380. expect(result).toContain('<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>')
  1381. done()
  1382. })
  1383. })
  1384. it('should merge serverPrefetch option', done => {
  1385. const mixin = {
  1386. data: {
  1387. message: ''
  1388. },
  1389. serverPrefetch () {
  1390. return new Promise((resolve) => {
  1391. setTimeout(() => {
  1392. this.message = 'vue.js'
  1393. resolve()
  1394. }, 1)
  1395. })
  1396. }
  1397. }
  1398. renderVmWithOptions({
  1399. mixins: [mixin],
  1400. template: `
  1401. <div>
  1402. <span>{{ count }}</span>
  1403. <div>{{ message }}</div>
  1404. </div>
  1405. `,
  1406. data: {
  1407. count: 0
  1408. },
  1409. serverPrefetch () {
  1410. return new Promise((resolve) => {
  1411. setTimeout(() => {
  1412. this.count = 42
  1413. resolve()
  1414. }, 1)
  1415. })
  1416. }
  1417. }, result => {
  1418. expect(result).toContain('<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>')
  1419. done()
  1420. })
  1421. })
  1422. it(`should skip serverPrefetch option that doesn't return a promise`, done => {
  1423. renderVmWithOptions({
  1424. template: `
  1425. <div>{{ count }}</div>
  1426. `,
  1427. data: {
  1428. count: 0
  1429. },
  1430. serverPrefetch () {
  1431. setTimeout(() => {
  1432. this.count = 42
  1433. }, 1)
  1434. }
  1435. }, result => {
  1436. expect(result).toContain('<div data-server-rendered="true">0</div>')
  1437. done()
  1438. })
  1439. })
  1440. it('should call context.rendered', done => {
  1441. let a = 0
  1442. renderToString(new Vue({
  1443. template: '<div>Hello</div>'
  1444. }), {
  1445. rendered: () => {
  1446. a = 42
  1447. }
  1448. }, (err, res) => {
  1449. expect(err).toBeNull()
  1450. expect(res).toContain('<div data-server-rendered="true">Hello</div>')
  1451. expect(a).toBe(42)
  1452. done()
  1453. })
  1454. })
  1455. it('invalid style value', done => {
  1456. renderVmWithOptions({
  1457. template: '<div :style="style"><p :style="style2"/></div>',
  1458. data: {
  1459. // all invalid, should not even have "style" attribute
  1460. style: {
  1461. opacity: {},
  1462. color: null
  1463. },
  1464. // mix of valid and invalid
  1465. style2: {
  1466. opacity: 0,
  1467. color: null
  1468. }
  1469. }
  1470. }, result => {
  1471. expect(result).toContain(
  1472. '<div data-server-rendered="true"><p style="opacity:0;"></p></div>'
  1473. )
  1474. done()
  1475. })
  1476. })
  1477. it('numeric style value', done => {
  1478. renderVmWithOptions({
  1479. template: '<div :style="style"></div>',
  1480. data: {
  1481. style: {
  1482. opacity: 0, // valid, opacity is unit-less
  1483. top: 0, // valid, top requires unit but 0 is allowed
  1484. left: 10, // invalid, left requires a unit
  1485. marginTop: '10px' // valid
  1486. }
  1487. }
  1488. }, result => {
  1489. expect(result).toContain(
  1490. '<div data-server-rendered="true" style="opacity:0;top:0;margin-top:10px;"></div>'
  1491. )
  1492. done()
  1493. })
  1494. })
  1495. it('handling max stack size limit', done => {
  1496. const vueInstance = new Vue({
  1497. template: `<div class="root">
  1498. <child v-for="(x, i) in items" :key="i"></child>
  1499. </div>`,
  1500. components: {
  1501. child: {
  1502. template: '<div class="child"><span class="child">hi</span></div>'
  1503. }
  1504. },
  1505. data: {
  1506. items: Array(1000).fill(0)
  1507. }
  1508. })
  1509. renderToString(vueInstance, err => done(err))
  1510. })
  1511. })
  1512. function renderVmWithOptions (options, cb) {
  1513. renderToString(new Vue(options), (err, res) => {
  1514. expect(err).toBeNull()
  1515. cb(res)
  1516. })
  1517. }