ssr-string.spec.ts 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166
  1. // @vitest-environment node
  2. import Vue from 'vue'
  3. import VM from 'vm'
  4. import { createRenderer } from 'server/index'
  5. import { _it } from './utils'
  6. const { renderToString } = createRenderer()
  7. describe('SSR: renderToString', () => {
  8. _it('static attributes', done => {
  9. renderVmWithOptions(
  10. {
  11. template: '<div id="foo" bar="123"></div>'
  12. },
  13. result => {
  14. expect(result).toContain(
  15. '<div id="foo" bar="123" data-server-rendered="true"></div>'
  16. )
  17. done()
  18. }
  19. )
  20. })
  21. _it('unary tags', done => {
  22. renderVmWithOptions(
  23. {
  24. template: '<input value="123">'
  25. },
  26. result => {
  27. expect(result).toContain(
  28. '<input value="123" data-server-rendered="true">'
  29. )
  30. done()
  31. }
  32. )
  33. })
  34. _it('dynamic attributes', done => {
  35. renderVmWithOptions(
  36. {
  37. template: '<div qux="quux" :id="foo" :bar="baz"></div>',
  38. data: {
  39. foo: 'hi',
  40. baz: 123
  41. }
  42. },
  43. result => {
  44. expect(result).toContain(
  45. '<div qux="quux" id="hi" bar="123" data-server-rendered="true"></div>'
  46. )
  47. done()
  48. }
  49. )
  50. })
  51. _it('static class', done => {
  52. renderVmWithOptions(
  53. {
  54. template: '<div class="foo bar"></div>'
  55. },
  56. result => {
  57. expect(result).toContain(
  58. '<div data-server-rendered="true" class="foo bar"></div>'
  59. )
  60. done()
  61. }
  62. )
  63. })
  64. _it('dynamic class', done => {
  65. renderVmWithOptions(
  66. {
  67. template:
  68. '<div class="foo bar" :class="[a, { qux: hasQux, quux: hasQuux }]"></div>',
  69. data: {
  70. a: 'baz',
  71. hasQux: true,
  72. hasQuux: false
  73. }
  74. },
  75. result => {
  76. expect(result).toContain(
  77. '<div data-server-rendered="true" class="foo bar baz qux"></div>'
  78. )
  79. done()
  80. }
  81. )
  82. })
  83. _it('custom component class', done => {
  84. renderVmWithOptions(
  85. {
  86. template: '<div><cmp class="cmp"></cmp></div>',
  87. components: {
  88. cmp: {
  89. render: h => h('div', 'test')
  90. }
  91. }
  92. },
  93. result => {
  94. expect(result).toContain(
  95. '<div data-server-rendered="true"><div class="cmp">test</div></div>'
  96. )
  97. done()
  98. }
  99. )
  100. })
  101. _it('nested component class', done => {
  102. renderVmWithOptions(
  103. {
  104. template: '<cmp class="outer" :class="cls"></cmp>',
  105. data: { cls: { success: 1 } },
  106. components: {
  107. cmp: {
  108. render: h =>
  109. h('div', [
  110. h('nested', { staticClass: 'nested', class: { error: 1 } })
  111. ]),
  112. components: {
  113. nested: {
  114. render: h => h('div', { staticClass: 'inner' }, 'test')
  115. }
  116. }
  117. }
  118. }
  119. },
  120. result => {
  121. expect(result).toContain(
  122. '<div data-server-rendered="true" class="outer success">' +
  123. '<div class="inner nested error">test</div>' +
  124. '</div>'
  125. )
  126. done()
  127. }
  128. )
  129. })
  130. _it('dynamic style', done => {
  131. renderVmWithOptions(
  132. {
  133. template:
  134. '<div style="background-color:black" :style="{ fontSize: fontSize + \'px\', color: color }"></div>',
  135. data: {
  136. fontSize: 14,
  137. color: 'red'
  138. }
  139. },
  140. result => {
  141. expect(result).toContain(
  142. '<div data-server-rendered="true" style="background-color:black;font-size:14px;color:red;"></div>'
  143. )
  144. done()
  145. }
  146. )
  147. })
  148. _it('dynamic string style', done => {
  149. renderVmWithOptions(
  150. {
  151. template: '<div :style="style"></div>',
  152. data: {
  153. style: 'color:red'
  154. }
  155. },
  156. result => {
  157. expect(result).toContain(
  158. '<div data-server-rendered="true" style="color:red;"></div>'
  159. )
  160. done()
  161. }
  162. )
  163. })
  164. _it('auto-prefixed style value as array', done => {
  165. renderVmWithOptions(
  166. {
  167. template: '<div :style="style"></div>',
  168. data: {
  169. style: {
  170. display: ['-webkit-box', '-ms-flexbox', 'flex']
  171. }
  172. }
  173. },
  174. result => {
  175. expect(result).toContain(
  176. '<div data-server-rendered="true" style="display:-webkit-box;display:-ms-flexbox;display:flex;"></div>'
  177. )
  178. done()
  179. }
  180. )
  181. })
  182. _it('custom component style', done => {
  183. renderVmWithOptions(
  184. {
  185. template: '<section><comp :style="style"></comp></section>',
  186. data: {
  187. style: 'color:red'
  188. },
  189. components: {
  190. comp: {
  191. template: '<div></div>'
  192. }
  193. }
  194. },
  195. result => {
  196. expect(result).toContain(
  197. '<section data-server-rendered="true"><div style="color:red;"></div></section>'
  198. )
  199. done()
  200. }
  201. )
  202. })
  203. _it('nested custom component style', done => {
  204. renderVmWithOptions(
  205. {
  206. template: '<comp style="color: blue" :style="style"></comp>',
  207. data: {
  208. style: 'color:red'
  209. },
  210. components: {
  211. comp: {
  212. template:
  213. '<nested style="text-align: left;" :style="{fontSize:\'520rem\'}"></nested>',
  214. components: {
  215. nested: {
  216. template: '<div></div>'
  217. }
  218. }
  219. }
  220. }
  221. },
  222. result => {
  223. expect(result).toContain(
  224. '<div data-server-rendered="true" style="text-align:left;font-size:520rem;color:red;"></div>'
  225. )
  226. done()
  227. }
  228. )
  229. })
  230. _it('component style not passed to child', done => {
  231. renderVmWithOptions(
  232. {
  233. template: '<comp :style="style"></comp>',
  234. data: {
  235. style: 'color:red'
  236. },
  237. components: {
  238. comp: {
  239. template: '<div><div></div></div>'
  240. }
  241. }
  242. },
  243. result => {
  244. expect(result).toContain(
  245. '<div data-server-rendered="true" style="color:red;"><div></div></div>'
  246. )
  247. done()
  248. }
  249. )
  250. })
  251. _it('component style not passed to slot', done => {
  252. renderVmWithOptions(
  253. {
  254. template:
  255. '<comp :style="style"><span style="color:black"></span></comp>',
  256. data: {
  257. style: 'color:red'
  258. },
  259. components: {
  260. comp: {
  261. template: '<div><slot></slot></div>'
  262. }
  263. }
  264. },
  265. result => {
  266. expect(result).toContain(
  267. '<div data-server-rendered="true" style="color:red;"><span style="color:black;"></span></div>'
  268. )
  269. done()
  270. }
  271. )
  272. })
  273. _it('attrs merging on components', done => {
  274. const Test = {
  275. render: h =>
  276. h('div', {
  277. attrs: { id: 'a' }
  278. })
  279. }
  280. renderVmWithOptions(
  281. {
  282. render: h =>
  283. h(Test, {
  284. attrs: { id: 'b', name: 'c' }
  285. })
  286. },
  287. res => {
  288. expect(res).toContain(
  289. '<div id="b" data-server-rendered="true" name="c"></div>'
  290. )
  291. done()
  292. }
  293. )
  294. })
  295. _it('domProps merging on components', done => {
  296. const Test = {
  297. render: h =>
  298. h('div', {
  299. domProps: { innerHTML: 'a' }
  300. })
  301. }
  302. renderVmWithOptions(
  303. {
  304. render: h =>
  305. h(Test, {
  306. domProps: { innerHTML: 'b', value: 'c' }
  307. })
  308. },
  309. res => {
  310. expect(res).toContain(
  311. '<div data-server-rendered="true" value="c">b</div>'
  312. )
  313. done()
  314. }
  315. )
  316. })
  317. _it('v-show directive render', done => {
  318. renderVmWithOptions(
  319. {
  320. template: '<div v-show="false"><span>inner</span></div>'
  321. },
  322. res => {
  323. expect(res).toContain(
  324. '<div data-server-rendered="true" style="display:none;"><span>inner</span></div>'
  325. )
  326. done()
  327. }
  328. )
  329. })
  330. _it('v-show directive merge with style', done => {
  331. renderVmWithOptions(
  332. {
  333. template:
  334. '<div :style="[{lineHeight: 1}]" v-show="false"><span>inner</span></div>'
  335. },
  336. res => {
  337. expect(res).toContain(
  338. '<div data-server-rendered="true" style="line-height:1;display:none;"><span>inner</span></div>'
  339. )
  340. done()
  341. }
  342. )
  343. })
  344. _it('v-show directive not passed to child', done => {
  345. renderVmWithOptions(
  346. {
  347. template: '<foo v-show="false"></foo>',
  348. components: {
  349. foo: {
  350. template: '<div><span>inner</span></div>'
  351. }
  352. }
  353. },
  354. res => {
  355. expect(res).toContain(
  356. '<div data-server-rendered="true" style="display:none;"><span>inner</span></div>'
  357. )
  358. done()
  359. }
  360. )
  361. })
  362. _it('v-show directive not passed to slot', done => {
  363. renderVmWithOptions(
  364. {
  365. template: '<foo v-show="false"><span>inner</span></foo>',
  366. components: {
  367. foo: {
  368. template: '<div><slot></slot></div>'
  369. }
  370. }
  371. },
  372. res => {
  373. expect(res).toContain(
  374. '<div data-server-rendered="true" style="display:none;"><span>inner</span></div>'
  375. )
  376. done()
  377. }
  378. )
  379. })
  380. _it('v-show directive merging on components', done => {
  381. renderVmWithOptions(
  382. {
  383. template: '<foo v-show="false"></foo>',
  384. components: {
  385. foo: {
  386. render: h =>
  387. h('bar', {
  388. directives: [
  389. {
  390. name: 'show',
  391. value: true
  392. }
  393. ]
  394. }),
  395. components: {
  396. bar: {
  397. render: h => h('div', 'inner')
  398. }
  399. }
  400. }
  401. }
  402. },
  403. res => {
  404. expect(res).toContain(
  405. '<div data-server-rendered="true" style="display:none;">inner</div>'
  406. )
  407. done()
  408. }
  409. )
  410. })
  411. _it('text interpolation', done => {
  412. renderVmWithOptions(
  413. {
  414. template: '<div>{{ foo }} side {{ bar }}</div>',
  415. data: {
  416. foo: 'server',
  417. bar: '<span>rendering</span>'
  418. }
  419. },
  420. result => {
  421. expect(result).toContain(
  422. '<div data-server-rendered="true">server side &lt;span&gt;rendering&lt;/span&gt;</div>'
  423. )
  424. done()
  425. }
  426. )
  427. })
  428. _it('v-html on root', done => {
  429. renderVmWithOptions(
  430. {
  431. template: '<div v-html="text"></div>',
  432. data: {
  433. text: '<span>foo</span>'
  434. }
  435. },
  436. result => {
  437. expect(result).toContain(
  438. '<div data-server-rendered="true"><span>foo</span></div>'
  439. )
  440. done()
  441. }
  442. )
  443. })
  444. _it('v-text on root', done => {
  445. renderVmWithOptions(
  446. {
  447. template: '<div v-text="text"></div>',
  448. data: {
  449. text: '<span>foo</span>'
  450. }
  451. },
  452. result => {
  453. expect(result).toContain(
  454. '<div data-server-rendered="true">&lt;span&gt;foo&lt;/span&gt;</div>'
  455. )
  456. done()
  457. }
  458. )
  459. })
  460. _it('v-html', done => {
  461. renderVmWithOptions(
  462. {
  463. template: '<div><div v-html="text"></div></div>',
  464. data: {
  465. text: '<span>foo</span>'
  466. }
  467. },
  468. result => {
  469. expect(result).toContain(
  470. '<div data-server-rendered="true"><div><span>foo</span></div></div>'
  471. )
  472. done()
  473. }
  474. )
  475. })
  476. _it('v-html with null value', done => {
  477. renderVmWithOptions(
  478. {
  479. template: '<div><div v-html="text"></div></div>',
  480. data: {
  481. text: null
  482. }
  483. },
  484. result => {
  485. expect(result).toContain(
  486. '<div data-server-rendered="true"><div></div></div>'
  487. )
  488. done()
  489. }
  490. )
  491. })
  492. _it('v-text', done => {
  493. renderVmWithOptions(
  494. {
  495. template: '<div><div v-text="text"></div></div>',
  496. data: {
  497. text: '<span>foo</span>'
  498. }
  499. },
  500. result => {
  501. expect(result).toContain(
  502. '<div data-server-rendered="true"><div>&lt;span&gt;foo&lt;/span&gt;</div></div>'
  503. )
  504. done()
  505. }
  506. )
  507. })
  508. _it('v-text with null value', done => {
  509. renderVmWithOptions(
  510. {
  511. template: '<div><div v-text="text"></div></div>',
  512. data: {
  513. text: null
  514. }
  515. },
  516. result => {
  517. expect(result).toContain(
  518. '<div data-server-rendered="true"><div></div></div>'
  519. )
  520. done()
  521. }
  522. )
  523. })
  524. _it('child component (hoc)', done => {
  525. renderVmWithOptions(
  526. {
  527. template: '<child class="foo" :msg="msg"></child>',
  528. data: {
  529. msg: 'hello'
  530. },
  531. components: {
  532. child: {
  533. props: ['msg'],
  534. data() {
  535. return { name: 'bar' }
  536. },
  537. render() {
  538. const h = this.$createElement
  539. return h('div', { class: ['bar'] }, [`${this.msg} ${this.name}`])
  540. }
  541. }
  542. }
  543. },
  544. result => {
  545. expect(result).toContain(
  546. '<div data-server-rendered="true" class="foo bar">hello bar</div>'
  547. )
  548. done()
  549. }
  550. )
  551. })
  552. _it('has correct lifecycle during render', done => {
  553. let lifecycleCount = 1
  554. renderVmWithOptions(
  555. {
  556. template: '<div><span>{{ val }}</span><test></test></div>',
  557. data: {
  558. val: 'hi'
  559. },
  560. beforeCreate() {
  561. expect(lifecycleCount++).toBe(1)
  562. },
  563. created() {
  564. this.val = 'hello'
  565. expect(this.val).toBe('hello')
  566. expect(lifecycleCount++).toBe(2)
  567. },
  568. components: {
  569. test: {
  570. beforeCreate() {
  571. expect(lifecycleCount++).toBe(3)
  572. },
  573. created() {
  574. expect(lifecycleCount++).toBe(4)
  575. },
  576. render() {
  577. expect(lifecycleCount++).toBeGreaterThan(4)
  578. return this.$createElement('span', { class: ['b'] }, 'testAsync')
  579. }
  580. }
  581. }
  582. },
  583. result => {
  584. expect(result).toContain(
  585. '<div data-server-rendered="true">' +
  586. '<span>hello</span>' +
  587. '<span class="b">testAsync</span>' +
  588. '</div>'
  589. )
  590. done()
  591. }
  592. )
  593. })
  594. _it('computed properties', done => {
  595. renderVmWithOptions(
  596. {
  597. template: '<div>{{ b }}</div>',
  598. data: {
  599. a: {
  600. b: 1
  601. }
  602. },
  603. computed: {
  604. b() {
  605. return this.a.b + 1
  606. }
  607. },
  608. created() {
  609. this.a.b = 2
  610. expect(this.b).toBe(3)
  611. }
  612. },
  613. result => {
  614. expect(result).toContain('<div data-server-rendered="true">3</div>')
  615. done()
  616. }
  617. )
  618. })
  619. _it('renders async component', done => {
  620. renderVmWithOptions(
  621. {
  622. template: `
  623. <div>
  624. <test-async></test-async>
  625. </div>
  626. `,
  627. components: {
  628. testAsync(resolve) {
  629. setTimeout(
  630. () =>
  631. resolve({
  632. render() {
  633. return this.$createElement(
  634. 'span',
  635. { class: ['b'] },
  636. 'testAsync'
  637. )
  638. }
  639. }),
  640. 1
  641. )
  642. }
  643. }
  644. },
  645. result => {
  646. expect(result).toContain(
  647. '<div data-server-rendered="true"><span class="b">testAsync</span></div>'
  648. )
  649. done()
  650. }
  651. )
  652. })
  653. _it('renders async component (Promise, nested)', done => {
  654. const Foo = () =>
  655. Promise.resolve({
  656. render: h => h('div', [h('span', 'foo'), h(Bar)])
  657. })
  658. const Bar = () => ({
  659. component: Promise.resolve({
  660. render: h => h('span', 'bar')
  661. })
  662. })
  663. renderVmWithOptions(
  664. {
  665. render: h => h(Foo)
  666. },
  667. res => {
  668. expect(res).toContain(
  669. `<div data-server-rendered="true"><span>foo</span><span>bar</span></div>`
  670. )
  671. done()
  672. }
  673. )
  674. })
  675. _it('renders async component (ES module)', done => {
  676. const Foo = () =>
  677. Promise.resolve({
  678. __esModule: true,
  679. default: {
  680. render: h => h('div', [h('span', 'foo'), h(Bar)])
  681. }
  682. })
  683. const Bar = () => ({
  684. component: Promise.resolve({
  685. __esModule: true,
  686. default: {
  687. render: h => h('span', 'bar')
  688. }
  689. })
  690. })
  691. renderVmWithOptions(
  692. {
  693. render: h => h(Foo)
  694. },
  695. res => {
  696. expect(res).toContain(
  697. `<div data-server-rendered="true"><span>foo</span><span>bar</span></div>`
  698. )
  699. done()
  700. }
  701. )
  702. })
  703. _it('renders async component (hoc)', done => {
  704. renderVmWithOptions(
  705. {
  706. template: '<test-async></test-async>',
  707. components: {
  708. testAsync: () =>
  709. Promise.resolve({
  710. render() {
  711. return this.$createElement(
  712. 'span',
  713. { class: ['b'] },
  714. 'testAsync'
  715. )
  716. }
  717. })
  718. }
  719. },
  720. result => {
  721. expect(result).toContain(
  722. '<span data-server-rendered="true" class="b">testAsync</span>'
  723. )
  724. done()
  725. }
  726. )
  727. })
  728. _it('renders async component (functional, single node)', done => {
  729. renderVmWithOptions(
  730. {
  731. template: `
  732. <div>
  733. <test-async></test-async>
  734. </div>
  735. `,
  736. components: {
  737. testAsync(resolve) {
  738. setTimeout(
  739. () =>
  740. resolve({
  741. functional: true,
  742. render(h) {
  743. return h('span', { class: ['b'] }, 'testAsync')
  744. }
  745. }),
  746. 1
  747. )
  748. }
  749. }
  750. },
  751. result => {
  752. expect(result).toContain(
  753. '<div data-server-rendered="true"><span class="b">testAsync</span></div>'
  754. )
  755. done()
  756. }
  757. )
  758. })
  759. _it('renders async component (functional, multiple nodes)', done => {
  760. renderVmWithOptions(
  761. {
  762. template: `
  763. <div>
  764. <test-async></test-async>
  765. </div>
  766. `,
  767. components: {
  768. testAsync(resolve) {
  769. setTimeout(
  770. () =>
  771. resolve({
  772. functional: true,
  773. render(h) {
  774. return [
  775. h('span', { class: ['a'] }, 'foo'),
  776. h('span', { class: ['b'] }, 'bar')
  777. ]
  778. }
  779. }),
  780. 1
  781. )
  782. }
  783. }
  784. },
  785. result => {
  786. expect(result).toContain(
  787. '<div data-server-rendered="true">' +
  788. '<span class="a">foo</span>' +
  789. '<span class="b">bar</span>' +
  790. '</div>'
  791. )
  792. done()
  793. }
  794. )
  795. })
  796. _it('renders nested async functional component', done => {
  797. renderVmWithOptions(
  798. {
  799. template: `
  800. <div>
  801. <outer-async></outer-async>
  802. </div>
  803. `,
  804. components: {
  805. outerAsync(resolve) {
  806. setTimeout(
  807. () =>
  808. resolve({
  809. functional: true,
  810. render(h) {
  811. return h('innerAsync')
  812. }
  813. }),
  814. 1
  815. )
  816. },
  817. innerAsync(resolve) {
  818. setTimeout(
  819. () =>
  820. resolve({
  821. functional: true,
  822. render(h) {
  823. return h('span', { class: ['a'] }, 'inner')
  824. }
  825. }),
  826. 1
  827. )
  828. }
  829. }
  830. },
  831. result => {
  832. expect(result).toContain(
  833. '<div data-server-rendered="true">' +
  834. '<span class="a">inner</span>' +
  835. '</div>'
  836. )
  837. done()
  838. }
  839. )
  840. })
  841. _it('should catch async component error', done => {
  842. renderToString(
  843. new Vue({
  844. template: '<test-async></test-async>',
  845. components: {
  846. testAsync: () =>
  847. Promise.resolve({
  848. render() {
  849. throw new Error('foo')
  850. }
  851. })
  852. }
  853. }),
  854. (err, result) => {
  855. expect(err).toBeTruthy()
  856. expect(result).toBeUndefined()
  857. expect('foo').toHaveBeenWarned()
  858. done()
  859. }
  860. )
  861. })
  862. // #11963, #10391
  863. _it('renders async children passed in slots', done => {
  864. const Parent = {
  865. template: `<div><slot name="child"/></div>`
  866. }
  867. const Child = {
  868. template: `<p>child</p>`
  869. }
  870. renderVmWithOptions(
  871. {
  872. template: `
  873. <Parent>
  874. <template #child>
  875. <Child/>
  876. </template>
  877. </Parent>
  878. `,
  879. components: {
  880. Parent,
  881. Child: () => Promise.resolve(Child)
  882. }
  883. },
  884. result => {
  885. expect(result).toContain(
  886. `<div data-server-rendered="true"><p>child</p></div>`
  887. )
  888. done()
  889. }
  890. )
  891. })
  892. _it('everything together', done => {
  893. renderVmWithOptions(
  894. {
  895. template: `
  896. <div>
  897. <p class="hi">yoyo</p>
  898. <div id="ho" :class="{ red: isRed }"></div>
  899. <span>{{ test }}</span>
  900. <input :value="test">
  901. <img :src="imageUrl">
  902. <test></test>
  903. <test-async></test-async>
  904. </div>
  905. `,
  906. data: {
  907. test: 'hi',
  908. isRed: true,
  909. imageUrl: 'https://vuejs.org/images/logo.png'
  910. },
  911. components: {
  912. test: {
  913. render() {
  914. return this.$createElement('div', { class: ['a'] }, 'test')
  915. }
  916. },
  917. testAsync(resolve) {
  918. resolve({
  919. render() {
  920. return this.$createElement(
  921. 'span',
  922. { class: ['b'] },
  923. 'testAsync'
  924. )
  925. }
  926. })
  927. }
  928. }
  929. },
  930. result => {
  931. expect(result).toContain(
  932. '<div data-server-rendered="true">' +
  933. '<p class="hi">yoyo</p> ' +
  934. '<div id="ho" class="red"></div> ' +
  935. '<span>hi</span> ' +
  936. '<input value="hi"> ' +
  937. '<img src="https://vuejs.org/images/logo.png"> ' +
  938. '<div class="a">test</div> ' +
  939. '<span class="b">testAsync</span>' +
  940. '</div>'
  941. )
  942. done()
  943. }
  944. )
  945. })
  946. _it('normal attr', done => {
  947. renderVmWithOptions(
  948. {
  949. template: `
  950. <div>
  951. <span :test="'ok'">hello</span>
  952. <span :test="null">hello</span>
  953. <span :test="false">hello</span>
  954. <span :test="true">hello</span>
  955. <span :test="0">hello</span>
  956. </div>
  957. `
  958. },
  959. result => {
  960. expect(result).toContain(
  961. '<div data-server-rendered="true">' +
  962. '<span test="ok">hello</span> ' +
  963. '<span>hello</span> ' +
  964. '<span>hello</span> ' +
  965. '<span test="true">hello</span> ' +
  966. '<span test="0">hello</span>' +
  967. '</div>'
  968. )
  969. done()
  970. }
  971. )
  972. })
  973. _it('enumerated attr', done => {
  974. renderVmWithOptions(
  975. {
  976. template: `
  977. <div>
  978. <span :draggable="true">hello</span>
  979. <span :draggable="'ok'">hello</span>
  980. <span :draggable="null">hello</span>
  981. <span :draggable="false">hello</span>
  982. <span :draggable="''">hello</span>
  983. <span :draggable="'false'">hello</span>
  984. </div>
  985. `
  986. },
  987. result => {
  988. expect(result).toContain(
  989. '<div data-server-rendered="true">' +
  990. '<span draggable="true">hello</span> ' +
  991. '<span draggable="true">hello</span> ' +
  992. '<span draggable="false">hello</span> ' +
  993. '<span draggable="false">hello</span> ' +
  994. '<span draggable="true">hello</span> ' +
  995. '<span draggable="false">hello</span>' +
  996. '</div>'
  997. )
  998. done()
  999. }
  1000. )
  1001. })
  1002. _it('boolean attr', done => {
  1003. renderVmWithOptions(
  1004. {
  1005. template: `
  1006. <div>
  1007. <span :disabled="true">hello</span>
  1008. <span :disabled="'ok'">hello</span>
  1009. <span :disabled="null">hello</span>
  1010. <span :disabled="''">hello</span>
  1011. </div>
  1012. `
  1013. },
  1014. result => {
  1015. expect(result).toContain(
  1016. '<div data-server-rendered="true">' +
  1017. '<span disabled="disabled">hello</span> ' +
  1018. '<span disabled="disabled">hello</span> ' +
  1019. '<span>hello</span> ' +
  1020. '<span disabled="disabled">hello</span>' +
  1021. '</div>'
  1022. )
  1023. done()
  1024. }
  1025. )
  1026. })
  1027. _it('v-bind object', done => {
  1028. renderVmWithOptions(
  1029. {
  1030. data: {
  1031. test: { id: 'a', class: ['a', 'b'], value: 'c' }
  1032. },
  1033. template: '<input v-bind="test">'
  1034. },
  1035. result => {
  1036. expect(result).toContain(
  1037. '<input id="a" data-server-rendered="true" value="c" class="a b">'
  1038. )
  1039. done()
  1040. }
  1041. )
  1042. })
  1043. _it('custom directives on raw element', done => {
  1044. const renderer = createRenderer({
  1045. directives: {
  1046. 'class-prefixer': (node, dir) => {
  1047. if (node.data.class) {
  1048. node.data.class = `${dir.value}-${node.data.class}`
  1049. }
  1050. if (node.data.staticClass) {
  1051. node.data.staticClass = `${dir.value}-${node.data.staticClass}`
  1052. }
  1053. }
  1054. }
  1055. })
  1056. renderer.renderToString(
  1057. new Vue({
  1058. render() {
  1059. const h = this.$createElement
  1060. return h(
  1061. 'p',
  1062. {
  1063. class: 'class1',
  1064. staticClass: 'class2',
  1065. directives: [
  1066. {
  1067. name: 'class-prefixer',
  1068. value: 'my'
  1069. }
  1070. ]
  1071. },
  1072. ['hello world']
  1073. )
  1074. }
  1075. }),
  1076. (err, result) => {
  1077. expect(err).toBeNull()
  1078. expect(result).toContain(
  1079. '<p data-server-rendered="true" class="my-class2 my-class1">hello world</p>'
  1080. )
  1081. done()
  1082. }
  1083. )
  1084. })
  1085. _it('custom directives on component', done => {
  1086. const Test = {
  1087. template: '<span>hello world</span>'
  1088. }
  1089. const renderer = createRenderer({
  1090. directives: {
  1091. 'class-prefixer': (node, dir) => {
  1092. if (node.data.class) {
  1093. node.data.class = `${dir.value}-${node.data.class}`
  1094. }
  1095. if (node.data.staticClass) {
  1096. node.data.staticClass = `${dir.value}-${node.data.staticClass}`
  1097. }
  1098. }
  1099. }
  1100. })
  1101. renderer.renderToString(
  1102. new Vue({
  1103. template:
  1104. '<p><Test v-class-prefixer="\'my\'" class="class1" :class="\'class2\'" /></p>',
  1105. components: { Test }
  1106. }),
  1107. (err, result) => {
  1108. expect(err).toBeNull()
  1109. expect(result).toContain(
  1110. '<p data-server-rendered="true"><span class="my-class1 my-class2">hello world</span></p>'
  1111. )
  1112. done()
  1113. }
  1114. )
  1115. })
  1116. _it('custom directives on element root of a component', done => {
  1117. const Test = {
  1118. template:
  1119. '<span v-class-prefixer="\'my\'" class="class1" :class="\'class2\'">hello world</span>'
  1120. }
  1121. const renderer = createRenderer({
  1122. directives: {
  1123. 'class-prefixer': (node, dir) => {
  1124. if (node.data.class) {
  1125. node.data.class = `${dir.value}-${node.data.class}`
  1126. }
  1127. if (node.data.staticClass) {
  1128. node.data.staticClass = `${dir.value}-${node.data.staticClass}`
  1129. }
  1130. }
  1131. }
  1132. })
  1133. renderer.renderToString(
  1134. new Vue({
  1135. template: '<p><Test /></p>',
  1136. components: { Test }
  1137. }),
  1138. (err, result) => {
  1139. expect(err).toBeNull()
  1140. expect(result).toContain(
  1141. '<p data-server-rendered="true"><span class="my-class1 my-class2">hello world</span></p>'
  1142. )
  1143. done()
  1144. }
  1145. )
  1146. })
  1147. _it('custom directives on element with parent element', done => {
  1148. const renderer = createRenderer({
  1149. directives: {
  1150. 'class-prefixer': (node, dir) => {
  1151. if (node.data.class) {
  1152. node.data.class = `${dir.value}-${node.data.class}`
  1153. }
  1154. if (node.data.staticClass) {
  1155. node.data.staticClass = `${dir.value}-${node.data.staticClass}`
  1156. }
  1157. }
  1158. }
  1159. })
  1160. renderer.renderToString(
  1161. new Vue({
  1162. template:
  1163. '<p><span v-class-prefixer="\'my\'" class="class1" :class="\'class2\'">hello world</span></p>'
  1164. }),
  1165. (err, result) => {
  1166. expect(err).toBeNull()
  1167. expect(result).toContain(
  1168. '<p data-server-rendered="true"><span class="my-class1 my-class2">hello world</span></p>'
  1169. )
  1170. done()
  1171. }
  1172. )
  1173. })
  1174. _it(
  1175. 'should not warn for custom directives that do not have server-side implementation',
  1176. done => {
  1177. renderToString(
  1178. new Vue({
  1179. directives: {
  1180. test: {
  1181. bind() {
  1182. // noop
  1183. }
  1184. }
  1185. },
  1186. template: '<div v-test></div>'
  1187. }),
  1188. () => {
  1189. expect('Failed to resolve directive: test').not.toHaveBeenWarned()
  1190. done()
  1191. }
  1192. )
  1193. }
  1194. )
  1195. _it('_scopeId', done => {
  1196. renderVmWithOptions(
  1197. {
  1198. _scopeId: '_v-parent',
  1199. template: '<div id="foo"><p><child></child></p></div>',
  1200. components: {
  1201. child: {
  1202. _scopeId: '_v-child',
  1203. render() {
  1204. const h = this.$createElement
  1205. return h('div', null, [h('span', null, ['foo'])])
  1206. }
  1207. }
  1208. }
  1209. },
  1210. result => {
  1211. expect(result).toContain(
  1212. '<div id="foo" data-server-rendered="true" _v-parent>' +
  1213. '<p _v-parent>' +
  1214. '<div _v-child _v-parent><span _v-child>foo</span></div>' +
  1215. '</p>' +
  1216. '</div>'
  1217. )
  1218. done()
  1219. }
  1220. )
  1221. })
  1222. _it('_scopeId on slot content', done => {
  1223. renderVmWithOptions(
  1224. {
  1225. _scopeId: '_v-parent',
  1226. template: '<div><child><p>foo</p></child></div>',
  1227. components: {
  1228. child: {
  1229. _scopeId: '_v-child',
  1230. render() {
  1231. const h = this.$createElement
  1232. return h('div', null, this.$slots.default)
  1233. }
  1234. }
  1235. }
  1236. },
  1237. result => {
  1238. expect(result).toContain(
  1239. '<div data-server-rendered="true" _v-parent>' +
  1240. '<div _v-child _v-parent><p _v-child _v-parent>foo</p></div>' +
  1241. '</div>'
  1242. )
  1243. done()
  1244. }
  1245. )
  1246. })
  1247. _it('comment nodes', done => {
  1248. renderVmWithOptions(
  1249. {
  1250. template: '<div><transition><div v-if="false"></div></transition></div>'
  1251. },
  1252. result => {
  1253. expect(result).toContain(
  1254. `<div data-server-rendered="true"><!----></div>`
  1255. )
  1256. done()
  1257. }
  1258. )
  1259. })
  1260. _it('should catch error', done => {
  1261. renderToString(
  1262. new Vue({
  1263. render() {
  1264. throw new Error('oops')
  1265. }
  1266. }),
  1267. err => {
  1268. expect(err instanceof Error).toBe(true)
  1269. expect(`oops`).toHaveBeenWarned()
  1270. done()
  1271. }
  1272. )
  1273. })
  1274. _it('default value Foreign Function', () => {
  1275. const FunctionConstructor = VM.runInNewContext('Function')
  1276. const func = () => 123
  1277. const vm = new Vue({
  1278. props: {
  1279. a: {
  1280. type: FunctionConstructor,
  1281. default: func
  1282. }
  1283. },
  1284. propsData: {
  1285. a: undefined
  1286. }
  1287. })
  1288. expect(vm.a).toBe(func)
  1289. })
  1290. _it('should prevent xss in attributes', done => {
  1291. renderVmWithOptions(
  1292. {
  1293. data: {
  1294. xss: '"><script>alert(1)</script>'
  1295. },
  1296. template: `
  1297. <div>
  1298. <a :title="xss" :style="{ color: xss }" :class="[xss]">foo</a>
  1299. </div>
  1300. `
  1301. },
  1302. res => {
  1303. expect(res).not.toContain(`<script>alert(1)</script>`)
  1304. done()
  1305. }
  1306. )
  1307. })
  1308. _it('should prevent xss in attribute names', done => {
  1309. renderVmWithOptions(
  1310. {
  1311. data: {
  1312. xss: {
  1313. 'foo="bar"></div><script>alert(1)</script>': ''
  1314. }
  1315. },
  1316. template: `
  1317. <div v-bind="xss"></div>
  1318. `
  1319. },
  1320. res => {
  1321. expect(res).not.toContain(`<script>alert(1)</script>`)
  1322. done()
  1323. }
  1324. )
  1325. })
  1326. _it('should prevent xss in attribute names (optimized)', done => {
  1327. renderVmWithOptions(
  1328. {
  1329. data: {
  1330. xss: {
  1331. 'foo="bar"></div><script>alert(1)</script>': ''
  1332. }
  1333. },
  1334. template: `
  1335. <div>
  1336. <a v-bind="xss">foo</a>
  1337. </div>
  1338. `
  1339. },
  1340. res => {
  1341. expect(res).not.toContain(`<script>alert(1)</script>`)
  1342. done()
  1343. }
  1344. )
  1345. })
  1346. _it(
  1347. 'should prevent script xss with v-bind object syntax + array value',
  1348. done => {
  1349. renderVmWithOptions(
  1350. {
  1351. data: {
  1352. test: ['"><script>alert(1)</script><!--"']
  1353. },
  1354. template: `<div v-bind="{ test }"></div>`
  1355. },
  1356. res => {
  1357. expect(res).not.toContain(`<script>alert(1)</script>`)
  1358. done()
  1359. }
  1360. )
  1361. }
  1362. )
  1363. _it('v-if', done => {
  1364. renderVmWithOptions(
  1365. {
  1366. template: `
  1367. <div>
  1368. <span v-if="true">foo</span>
  1369. <span v-if="false">bar</span>
  1370. </div>
  1371. `
  1372. },
  1373. res => {
  1374. expect(res).toContain(
  1375. `<div data-server-rendered="true"><span>foo</span> <!----></div>`
  1376. )
  1377. done()
  1378. }
  1379. )
  1380. })
  1381. _it('v-for', done => {
  1382. renderVmWithOptions(
  1383. {
  1384. template: `
  1385. <div>
  1386. <span>foo</span>
  1387. <span v-for="i in 2">{{ i }}</span>
  1388. </div>
  1389. `
  1390. },
  1391. res => {
  1392. expect(res).toContain(
  1393. `<div data-server-rendered="true"><span>foo</span> <span>1</span><span>2</span></div>`
  1394. )
  1395. done()
  1396. }
  1397. )
  1398. })
  1399. _it('template v-if', done => {
  1400. renderVmWithOptions(
  1401. {
  1402. template: `
  1403. <div>
  1404. <span>foo</span>
  1405. <template v-if="true">
  1406. <span>foo</span> bar <span>baz</span>
  1407. </template>
  1408. </div>
  1409. `
  1410. },
  1411. res => {
  1412. expect(res).toContain(
  1413. `<div data-server-rendered="true"><span>foo</span> <span>foo</span> bar <span>baz</span></div>`
  1414. )
  1415. done()
  1416. }
  1417. )
  1418. })
  1419. _it('template v-for', done => {
  1420. renderVmWithOptions(
  1421. {
  1422. template: `
  1423. <div>
  1424. <span>foo</span>
  1425. <template v-for="i in 2">
  1426. <span>{{ i }}</span><span>bar</span>
  1427. </template>
  1428. </div>
  1429. `
  1430. },
  1431. res => {
  1432. expect(res).toContain(
  1433. `<div data-server-rendered="true"><span>foo</span> <span>1</span><span>bar</span><span>2</span><span>bar</span></div>`
  1434. )
  1435. done()
  1436. }
  1437. )
  1438. })
  1439. _it('with inheritAttrs: false + $attrs', done => {
  1440. renderVmWithOptions(
  1441. {
  1442. template: `<foo id="a"/>`,
  1443. components: {
  1444. foo: {
  1445. inheritAttrs: false,
  1446. template: `<div><div v-bind="$attrs"></div></div>`
  1447. }
  1448. }
  1449. },
  1450. res => {
  1451. expect(res).toBe(
  1452. `<div data-server-rendered="true"><div id="a"></div></div>`
  1453. )
  1454. done()
  1455. }
  1456. )
  1457. })
  1458. _it('should escape static strings', done => {
  1459. renderVmWithOptions(
  1460. {
  1461. template: `<div>&lt;foo&gt;</div>`
  1462. },
  1463. res => {
  1464. expect(res).toBe(`<div data-server-rendered="true">&lt;foo&gt;</div>`)
  1465. done()
  1466. }
  1467. )
  1468. })
  1469. _it('should not cache computed properties', done => {
  1470. renderVmWithOptions(
  1471. {
  1472. template: `<div>{{ foo }}</div>`,
  1473. data: () => ({ bar: 1 }),
  1474. computed: {
  1475. foo() {
  1476. return this.bar + 1
  1477. }
  1478. },
  1479. created() {
  1480. this.foo // access
  1481. this.bar++ // trigger change
  1482. }
  1483. },
  1484. res => {
  1485. expect(res).toBe(`<div data-server-rendered="true">3</div>`)
  1486. done()
  1487. }
  1488. )
  1489. })
  1490. // #8977
  1491. _it('should call computed properties with vm as first argument', done => {
  1492. renderToString(
  1493. new Vue({
  1494. data: {
  1495. firstName: 'Evan',
  1496. lastName: 'You'
  1497. },
  1498. computed: {
  1499. fullName: ({ firstName, lastName }) => `${firstName} ${lastName}`
  1500. },
  1501. template: '<div>{{ fullName }}</div>'
  1502. }),
  1503. (err, result) => {
  1504. expect(err).toBeNull()
  1505. expect(result).toContain(
  1506. '<div data-server-rendered="true">Evan You</div>'
  1507. )
  1508. done()
  1509. }
  1510. )
  1511. })
  1512. _it('return Promise', async () => {
  1513. await renderToString(
  1514. new Vue({
  1515. template: `<div>{{ foo }}</div>`,
  1516. data: { foo: 'bar' }
  1517. })
  1518. )!.then(res => {
  1519. expect(res).toBe(`<div data-server-rendered="true">bar</div>`)
  1520. })
  1521. })
  1522. _it('return Promise (error)', async () => {
  1523. await renderToString(
  1524. new Vue({
  1525. render() {
  1526. throw new Error('foobar')
  1527. }
  1528. })
  1529. )!.catch(err => {
  1530. expect('foobar').toHaveBeenWarned()
  1531. expect(err.toString()).toContain(`foobar`)
  1532. })
  1533. })
  1534. _it('should catch template compilation error', done => {
  1535. renderToString(
  1536. new Vue({
  1537. template: `<div></div><div></div>`
  1538. }),
  1539. err => {
  1540. expect(err.toString()).toContain(
  1541. 'Component template should contain exactly one root element'
  1542. )
  1543. done()
  1544. }
  1545. )
  1546. })
  1547. // #6907
  1548. _it('should not optimize root if conditions', done => {
  1549. renderVmWithOptions(
  1550. {
  1551. data: { foo: 123 },
  1552. template: `<input :type="'text'" v-model="foo">`
  1553. },
  1554. res => {
  1555. expect(res).toBe(
  1556. `<input type="text" data-server-rendered="true" value="123">`
  1557. )
  1558. done()
  1559. }
  1560. )
  1561. })
  1562. _it('render muted properly', done => {
  1563. renderVmWithOptions(
  1564. {
  1565. template: '<video muted></video>'
  1566. },
  1567. result => {
  1568. expect(result).toContain(
  1569. '<video muted="muted" data-server-rendered="true"></video>'
  1570. )
  1571. done()
  1572. }
  1573. )
  1574. })
  1575. _it('render v-model with textarea', done => {
  1576. renderVmWithOptions(
  1577. {
  1578. data: { foo: 'bar' },
  1579. template: '<div><textarea v-model="foo"></textarea></div>'
  1580. },
  1581. result => {
  1582. expect(result).toContain('<textarea>bar</textarea>')
  1583. done()
  1584. }
  1585. )
  1586. })
  1587. _it('render v-model with textarea (non-optimized)', done => {
  1588. renderVmWithOptions(
  1589. {
  1590. render(h) {
  1591. return h('textarea', {
  1592. domProps: {
  1593. value: 'foo'
  1594. }
  1595. })
  1596. }
  1597. },
  1598. result => {
  1599. expect(result).toContain(
  1600. '<textarea data-server-rendered="true">foo</textarea>'
  1601. )
  1602. done()
  1603. }
  1604. )
  1605. })
  1606. _it('render v-model with <select> (value binding)', done => {
  1607. renderVmWithOptions(
  1608. {
  1609. data: {
  1610. selected: 2,
  1611. options: [
  1612. { id: 1, label: 'one' },
  1613. { id: 2, label: 'two' }
  1614. ]
  1615. },
  1616. template: `
  1617. <div>
  1618. <select v-model="selected">
  1619. <option v-for="o in options" :value="o.id">{{ o.label }}</option>
  1620. </select>
  1621. </div>
  1622. `
  1623. },
  1624. result => {
  1625. expect(result).toContain(
  1626. '<select>' +
  1627. '<option value="1">one</option>' +
  1628. '<option selected="selected" value="2">two</option>' +
  1629. '</select>'
  1630. )
  1631. done()
  1632. }
  1633. )
  1634. })
  1635. _it('render v-model with <select> (static value)', done => {
  1636. renderVmWithOptions(
  1637. {
  1638. data: {
  1639. selected: 2
  1640. },
  1641. template: `
  1642. <div>
  1643. <select v-model="selected">
  1644. <option value="1">one</option>
  1645. <option value="2">two</option>
  1646. </select>
  1647. </div>
  1648. `
  1649. },
  1650. result => {
  1651. expect(result).toContain(
  1652. '<select>' +
  1653. '<option value="1">one</option> ' +
  1654. '<option value="2" selected="selected">two</option>' +
  1655. '</select>'
  1656. )
  1657. done()
  1658. }
  1659. )
  1660. })
  1661. _it('render v-model with <select> (text as value)', done => {
  1662. renderVmWithOptions(
  1663. {
  1664. data: {
  1665. selected: 2,
  1666. options: [
  1667. { id: 1, label: 'one' },
  1668. { id: 2, label: 'two' }
  1669. ]
  1670. },
  1671. template: `
  1672. <div>
  1673. <select v-model="selected">
  1674. <option v-for="o in options">{{ o.id }}</option>
  1675. </select>
  1676. </div>
  1677. `
  1678. },
  1679. result => {
  1680. expect(result).toContain(
  1681. '<select>' +
  1682. '<option>1</option>' +
  1683. '<option selected="selected">2</option>' +
  1684. '</select>'
  1685. )
  1686. done()
  1687. }
  1688. )
  1689. })
  1690. // #7223
  1691. _it('should not double escape attribute values', done => {
  1692. renderVmWithOptions(
  1693. {
  1694. template: `
  1695. <div>
  1696. <div id="a\nb"></div>
  1697. </div>
  1698. `
  1699. },
  1700. result => {
  1701. expect(result).toContain(`<div id="a\nb"></div>`)
  1702. done()
  1703. }
  1704. )
  1705. })
  1706. // #7859
  1707. _it('should not double escape class values', done => {
  1708. renderVmWithOptions(
  1709. {
  1710. template: `
  1711. <div>
  1712. <div class="a\nb"></div>
  1713. </div>
  1714. `
  1715. },
  1716. result => {
  1717. expect(result).toContain(`<div class="a b"></div>`)
  1718. done()
  1719. }
  1720. )
  1721. })
  1722. _it('should expose ssr helpers on functional context', done => {
  1723. let called = false
  1724. renderVmWithOptions(
  1725. {
  1726. template: `<div><foo/></div>`,
  1727. components: {
  1728. foo: {
  1729. functional: true,
  1730. render(h, ctx) {
  1731. expect(ctx._ssrNode).toBeTruthy()
  1732. called = true
  1733. }
  1734. }
  1735. }
  1736. },
  1737. () => {
  1738. expect(called).toBe(true)
  1739. done()
  1740. }
  1741. )
  1742. })
  1743. _it('should support serverPrefetch option', done => {
  1744. renderVmWithOptions(
  1745. {
  1746. template: `
  1747. <div>{{ count }}</div>
  1748. `,
  1749. data: {
  1750. count: 0
  1751. },
  1752. serverPrefetch() {
  1753. return new Promise<void>(resolve => {
  1754. setTimeout(() => {
  1755. this.count = 42
  1756. resolve()
  1757. }, 1)
  1758. })
  1759. }
  1760. },
  1761. result => {
  1762. expect(result).toContain('<div data-server-rendered="true">42</div>')
  1763. done()
  1764. }
  1765. )
  1766. })
  1767. _it('should support serverPrefetch option (nested)', done => {
  1768. renderVmWithOptions(
  1769. {
  1770. template: `
  1771. <div>
  1772. <span>{{ count }}</span>
  1773. <nested-prefetch></nested-prefetch>
  1774. </div>
  1775. `,
  1776. data: {
  1777. count: 0
  1778. },
  1779. serverPrefetch() {
  1780. return new Promise<void>(resolve => {
  1781. setTimeout(() => {
  1782. this.count = 42
  1783. resolve()
  1784. }, 1)
  1785. })
  1786. },
  1787. components: {
  1788. nestedPrefetch: {
  1789. template: `
  1790. <div>{{ message }}</div>
  1791. `,
  1792. data() {
  1793. return {
  1794. message: ''
  1795. }
  1796. },
  1797. serverPrefetch() {
  1798. return new Promise<void>(resolve => {
  1799. setTimeout(() => {
  1800. this.message = 'vue.js'
  1801. resolve()
  1802. }, 1)
  1803. })
  1804. }
  1805. }
  1806. }
  1807. },
  1808. result => {
  1809. expect(result).toContain(
  1810. '<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>'
  1811. )
  1812. done()
  1813. }
  1814. )
  1815. })
  1816. _it('should support serverPrefetch option (nested async)', done => {
  1817. renderVmWithOptions(
  1818. {
  1819. template: `
  1820. <div>
  1821. <span>{{ count }}</span>
  1822. <nested-prefetch></nested-prefetch>
  1823. </div>
  1824. `,
  1825. data: {
  1826. count: 0
  1827. },
  1828. serverPrefetch() {
  1829. return new Promise<void>(resolve => {
  1830. setTimeout(() => {
  1831. this.count = 42
  1832. resolve()
  1833. }, 1)
  1834. })
  1835. },
  1836. components: {
  1837. nestedPrefetch(resolve) {
  1838. resolve({
  1839. template: `
  1840. <div>{{ message }}</div>
  1841. `,
  1842. data() {
  1843. return {
  1844. message: ''
  1845. }
  1846. },
  1847. serverPrefetch() {
  1848. return new Promise<void>(resolve => {
  1849. setTimeout(() => {
  1850. this.message = 'vue.js'
  1851. resolve()
  1852. }, 1)
  1853. })
  1854. }
  1855. })
  1856. }
  1857. }
  1858. },
  1859. result => {
  1860. expect(result).toContain(
  1861. '<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>'
  1862. )
  1863. done()
  1864. }
  1865. )
  1866. })
  1867. _it('should merge serverPrefetch option', done => {
  1868. const mixin = {
  1869. data: {
  1870. message: ''
  1871. },
  1872. serverPrefetch() {
  1873. return new Promise<void>(resolve => {
  1874. setTimeout(() => {
  1875. this.message = 'vue.js'
  1876. resolve()
  1877. }, 1)
  1878. })
  1879. }
  1880. }
  1881. renderVmWithOptions(
  1882. {
  1883. mixins: [mixin],
  1884. template: `
  1885. <div>
  1886. <span>{{ count }}</span>
  1887. <div>{{ message }}</div>
  1888. </div>
  1889. `,
  1890. data: {
  1891. count: 0
  1892. },
  1893. serverPrefetch() {
  1894. return new Promise<void>(resolve => {
  1895. setTimeout(() => {
  1896. this.count = 42
  1897. resolve()
  1898. }, 1)
  1899. })
  1900. }
  1901. },
  1902. result => {
  1903. expect(result).toContain(
  1904. '<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>'
  1905. )
  1906. done()
  1907. }
  1908. )
  1909. })
  1910. _it(
  1911. `should skip serverPrefetch option that doesn't return a promise`,
  1912. done => {
  1913. renderVmWithOptions(
  1914. {
  1915. template: `
  1916. <div>{{ count }}</div>
  1917. `,
  1918. data: {
  1919. count: 0
  1920. },
  1921. serverPrefetch() {
  1922. setTimeout(() => {
  1923. this.count = 42
  1924. }, 1)
  1925. }
  1926. },
  1927. result => {
  1928. expect(result).toContain('<div data-server-rendered="true">0</div>')
  1929. done()
  1930. }
  1931. )
  1932. }
  1933. )
  1934. _it('should call context.rendered', done => {
  1935. let a = 0
  1936. renderToString(
  1937. new Vue({
  1938. template: '<div>Hello</div>'
  1939. }),
  1940. {
  1941. rendered: () => {
  1942. a = 42
  1943. }
  1944. },
  1945. (err, res) => {
  1946. expect(err).toBeNull()
  1947. expect(res).toContain('<div data-server-rendered="true">Hello</div>')
  1948. expect(a).toBe(42)
  1949. done()
  1950. }
  1951. )
  1952. })
  1953. _it('invalid style value', done => {
  1954. renderVmWithOptions(
  1955. {
  1956. template: '<div :style="style"><p :style="style2"/></div>',
  1957. data: {
  1958. // all invalid, should not even have "style" attribute
  1959. style: {
  1960. opacity: {},
  1961. color: null
  1962. },
  1963. // mix of valid and invalid
  1964. style2: {
  1965. opacity: 0,
  1966. color: null
  1967. }
  1968. }
  1969. },
  1970. result => {
  1971. expect(result).toContain(
  1972. '<div data-server-rendered="true"><p style="opacity:0;"></p></div>'
  1973. )
  1974. done()
  1975. }
  1976. )
  1977. })
  1978. _it('numeric style value', done => {
  1979. renderVmWithOptions(
  1980. {
  1981. template: '<div :style="style"></div>',
  1982. data: {
  1983. style: {
  1984. opacity: 0, // valid, opacity is unit-less
  1985. top: 0, // valid, top requires unit but 0 is allowed
  1986. left: 10, // invalid, left requires a unit
  1987. marginTop: '10px' // valid
  1988. }
  1989. }
  1990. },
  1991. result => {
  1992. expect(result).toContain(
  1993. '<div data-server-rendered="true" style="opacity:0;top:0;margin-top:10px;"></div>'
  1994. )
  1995. done()
  1996. }
  1997. )
  1998. })
  1999. _it('handling max stack size limit', done => {
  2000. const vueInstance = new Vue({
  2001. template: `<div class="root">
  2002. <child v-for="(x, i) in items" :key="i"></child>
  2003. </div>`,
  2004. components: {
  2005. child: {
  2006. template: '<div class="child"><span class="child">hi</span></div>'
  2007. }
  2008. },
  2009. data: {
  2010. items: Array(1000).fill(0)
  2011. }
  2012. })
  2013. renderToString(vueInstance, err => done(err))
  2014. })
  2015. _it('undefined v-model with textarea', done => {
  2016. renderVmWithOptions(
  2017. {
  2018. render(h) {
  2019. return h('div', [
  2020. h('textarea', {
  2021. domProps: {
  2022. value: null
  2023. }
  2024. })
  2025. ])
  2026. }
  2027. },
  2028. result => {
  2029. expect(result).toContain(
  2030. '<div data-server-rendered="true"><textarea></textarea></div>'
  2031. )
  2032. done()
  2033. }
  2034. )
  2035. })
  2036. _it('Options inheritAttrs in parent component', done => {
  2037. const childComponent = {
  2038. template: `<div>{{ someProp }}</div>`,
  2039. props: {
  2040. someProp: {}
  2041. }
  2042. }
  2043. const parentComponent = {
  2044. template: `<childComponent v-bind="$attrs" />`,
  2045. components: { childComponent },
  2046. inheritAttrs: false
  2047. }
  2048. renderVmWithOptions(
  2049. {
  2050. template: `
  2051. <div>
  2052. <parentComponent some-prop="some-val" />
  2053. </div>
  2054. `,
  2055. components: { parentComponent }
  2056. },
  2057. result => {
  2058. expect(result).toContain(
  2059. '<div data-server-rendered="true"><div>some-val</div></div>'
  2060. )
  2061. done()
  2062. }
  2063. )
  2064. })
  2065. })
  2066. function renderVmWithOptions(options, cb) {
  2067. renderToString(new Vue(options), (err, res) => {
  2068. expect(err).toBeNull()
  2069. cb(res)
  2070. })
  2071. }