parser.spec.ts 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149
  1. import { parse } from 'compiler/parser/index'
  2. import { extend } from 'shared/util'
  3. import { baseOptions } from 'web/compiler/options'
  4. import { isIE, isEdge } from 'core/util/env'
  5. describe('parser', () => {
  6. it('simple element', () => {
  7. const ast = parse('<h1>hello world</h1>', baseOptions)
  8. expect(ast.tag).toBe('h1')
  9. expect(ast.plain).toBe(true)
  10. expect(ast.children[0].text).toBe('hello world')
  11. })
  12. it('interpolation in element', () => {
  13. const ast = parse('<h1>{{msg}}</h1>', baseOptions)
  14. expect(ast.tag).toBe('h1')
  15. expect(ast.plain).toBe(true)
  16. expect(ast.children[0].expression).toBe('_s(msg)')
  17. })
  18. it('child elements', () => {
  19. const ast = parse('<ul><li>hello world</li></ul>', baseOptions)
  20. expect(ast.tag).toBe('ul')
  21. expect(ast.plain).toBe(true)
  22. expect(ast.children[0].tag).toBe('li')
  23. expect(ast.children[0].plain).toBe(true)
  24. expect(ast.children[0].children[0].text).toBe('hello world')
  25. expect(ast.children[0].parent).toBe(ast)
  26. })
  27. it('unary element', () => {
  28. const ast = parse('<hr>', baseOptions)
  29. expect(ast.tag).toBe('hr')
  30. expect(ast.plain).toBe(true)
  31. expect(ast.children.length).toBe(0)
  32. })
  33. it('svg element', () => {
  34. const ast = parse('<svg><text>hello world</text></svg>', baseOptions)
  35. expect(ast.tag).toBe('svg')
  36. expect(ast.ns).toBe('svg')
  37. expect(ast.plain).toBe(true)
  38. expect(ast.children[0].tag).toBe('text')
  39. expect(ast.children[0].children[0].text).toBe('hello world')
  40. expect(ast.children[0].parent).toBe(ast)
  41. })
  42. it('camelCase element', () => {
  43. const ast = parse(
  44. '<MyComponent><p>hello world</p></MyComponent>',
  45. baseOptions
  46. )
  47. expect(ast.tag).toBe('MyComponent')
  48. expect(ast.plain).toBe(true)
  49. expect(ast.children[0].tag).toBe('p')
  50. expect(ast.children[0].plain).toBe(true)
  51. expect(ast.children[0].children[0].text).toBe('hello world')
  52. expect(ast.children[0].parent).toBe(ast)
  53. })
  54. it('forbidden element', () => {
  55. // style
  56. const styleAst = parse('<style>error { color: red; }</style>', baseOptions)
  57. expect(styleAst.tag).toBe('style')
  58. expect(styleAst.plain).toBe(true)
  59. expect(styleAst.forbidden).toBe(true)
  60. expect(styleAst.children[0].text).toBe('error { color: red; }')
  61. expect(
  62. 'Templates should only be responsible for mapping the state'
  63. ).toHaveBeenWarned()
  64. // script
  65. const scriptAst = parse(
  66. '<script type="text/javascript">alert("hello world!")</script>',
  67. baseOptions
  68. )
  69. expect(scriptAst.tag).toBe('script')
  70. expect(scriptAst.plain).toBe(false)
  71. expect(scriptAst.forbidden).toBe(true)
  72. expect(scriptAst.children[0].text).toBe('alert("hello world!")')
  73. expect(
  74. 'Templates should only be responsible for mapping the state'
  75. ).toHaveBeenWarned()
  76. })
  77. it('not contain root element', () => {
  78. parse('hello world', baseOptions)
  79. expect(
  80. 'Component template requires a root element, rather than just text'
  81. ).toHaveBeenWarned()
  82. })
  83. it('warn text before root element', () => {
  84. parse('before root {{ interpolation }}<div></div>', baseOptions)
  85. expect(
  86. 'text "before root {{ interpolation }}" outside root element will be ignored.'
  87. ).toHaveBeenWarned()
  88. })
  89. it('warn text after root element', () => {
  90. parse('<div></div>after root {{ interpolation }}', baseOptions)
  91. expect(
  92. 'text "after root {{ interpolation }}" outside root element will be ignored.'
  93. ).toHaveBeenWarned()
  94. })
  95. it('warn multiple root elements', () => {
  96. parse('<div></div><div></div>', baseOptions)
  97. expect(
  98. 'Component template should contain exactly one root element'
  99. ).toHaveBeenWarned()
  100. })
  101. it('remove duplicate whitespace text nodes caused by comments', () => {
  102. const ast = parse(`<div><a></a> <!----> <a></a></div>`, baseOptions)
  103. expect(ast.children.length).toBe(3)
  104. expect(ast.children[0].tag).toBe('a')
  105. expect(ast.children[1].text).toBe(' ')
  106. expect(ast.children[2].tag).toBe('a')
  107. })
  108. it('remove text nodes between v-if conditions', () => {
  109. const ast = parse(
  110. `<div><div v-if="1"></div> <div v-else-if="2"></div> <div v-else></div> <span></span></div>`,
  111. baseOptions
  112. )
  113. expect(ast.children.length).toBe(3)
  114. expect(ast.children[0].tag).toBe('div')
  115. expect(ast.children[0].ifConditions.length).toBe(3)
  116. expect(ast.children[1].text).toBe(' ') // text
  117. expect(ast.children[2].tag).toBe('span')
  118. })
  119. it('warn non whitespace text between v-if conditions', () => {
  120. parse(`<div><div v-if="1"></div> foo <div v-else></div></div>`, baseOptions)
  121. expect(
  122. `text "foo" between v-if and v-else(-if) will be ignored`
  123. ).toHaveBeenWarned()
  124. })
  125. it('not warn 2 root elements with v-if and v-else', () => {
  126. parse('<div v-if="1"></div><div v-else></div>', baseOptions)
  127. expect(
  128. 'Component template should contain exactly one root element'
  129. ).not.toHaveBeenWarned()
  130. })
  131. it('not warn 3 root elements with v-if, v-else-if and v-else', () => {
  132. parse(
  133. '<div v-if="1"></div><div v-else-if="2"></div><div v-else></div>',
  134. baseOptions
  135. )
  136. expect(
  137. 'Component template should contain exactly one root element'
  138. ).not.toHaveBeenWarned()
  139. })
  140. it('not warn 2 root elements with v-if and v-else on separate lines', () => {
  141. parse(
  142. `
  143. <div v-if="1"></div>
  144. <div v-else></div>
  145. `,
  146. baseOptions
  147. )
  148. expect(
  149. 'Component template should contain exactly one root element'
  150. ).not.toHaveBeenWarned()
  151. })
  152. it('not warn 3 or more root elements with v-if, v-else-if and v-else on separate lines', () => {
  153. parse(
  154. `
  155. <div v-if="1"></div>
  156. <div v-else-if="2"></div>
  157. <div v-else></div>
  158. `,
  159. baseOptions
  160. )
  161. expect(
  162. 'Component template should contain exactly one root element'
  163. ).not.toHaveBeenWarned()
  164. parse(
  165. `
  166. <div v-if="1"></div>
  167. <div v-else-if="2"></div>
  168. <div v-else-if="3"></div>
  169. <div v-else-if="4"></div>
  170. <div v-else></div>
  171. `,
  172. baseOptions
  173. )
  174. expect(
  175. 'Component template should contain exactly one root element'
  176. ).not.toHaveBeenWarned()
  177. })
  178. it('generate correct ast for 2 root elements with v-if and v-else on separate lines', () => {
  179. const ast = parse(
  180. `
  181. <div v-if="1"></div>
  182. <p v-else></p>
  183. `,
  184. baseOptions
  185. )
  186. expect(ast.tag).toBe('div')
  187. expect(ast.ifConditions[1].block.tag).toBe('p')
  188. })
  189. it('generate correct ast for 3 or more root elements with v-if and v-else on separate lines', () => {
  190. const ast = parse(
  191. `
  192. <div v-if="1"></div>
  193. <span v-else-if="2"></span>
  194. <p v-else></p>
  195. `,
  196. baseOptions
  197. )
  198. expect(ast.tag).toBe('div')
  199. expect(ast.ifConditions[0].block.tag).toBe('div')
  200. expect(ast.ifConditions[1].block.tag).toBe('span')
  201. expect(ast.ifConditions[2].block.tag).toBe('p')
  202. const astMore = parse(
  203. `
  204. <div v-if="1"></div>
  205. <span v-else-if="2"></span>
  206. <div v-else-if="3"></div>
  207. <span v-else-if="4"></span>
  208. <p v-else></p>
  209. `,
  210. baseOptions
  211. )
  212. expect(astMore.tag).toBe('div')
  213. expect(astMore.ifConditions[0].block.tag).toBe('div')
  214. expect(astMore.ifConditions[1].block.tag).toBe('span')
  215. expect(astMore.ifConditions[2].block.tag).toBe('div')
  216. expect(astMore.ifConditions[3].block.tag).toBe('span')
  217. expect(astMore.ifConditions[4].block.tag).toBe('p')
  218. })
  219. it('warn 2 root elements with v-if', () => {
  220. parse('<div v-if="1"></div><div v-if="2"></div>', baseOptions)
  221. expect(
  222. 'Component template should contain exactly one root element'
  223. ).toHaveBeenWarned()
  224. })
  225. it('warn 3 root elements with v-if and v-else on first 2', () => {
  226. parse('<div v-if="1"></div><div v-else></div><div></div>', baseOptions)
  227. expect(
  228. 'Component template should contain exactly one root element'
  229. ).toHaveBeenWarned()
  230. })
  231. it('warn 3 root elements with v-if and v-else-if on first 2', () => {
  232. parse('<div v-if="1"></div><div v-else-if></div><div></div>', baseOptions)
  233. expect(
  234. 'Component template should contain exactly one root element'
  235. ).toHaveBeenWarned()
  236. })
  237. it('warn 4 root elements with v-if, v-else-if and v-else on first 2', () => {
  238. parse(
  239. '<div v-if="1"></div><div v-else-if></div><div v-else></div><div></div>',
  240. baseOptions
  241. )
  242. expect(
  243. 'Component template should contain exactly one root element'
  244. ).toHaveBeenWarned()
  245. })
  246. it('warn 2 root elements with v-if and v-else with v-for on 2nd', () => {
  247. parse(
  248. '<div v-if="1"></div><div v-else v-for="i in [1]"></div>',
  249. baseOptions
  250. )
  251. expect(
  252. 'Cannot use v-for on stateful component root element because it renders multiple elements'
  253. ).toHaveBeenWarned()
  254. })
  255. it('warn 2 root elements with v-if and v-else-if with v-for on 2nd', () => {
  256. parse(
  257. '<div v-if="1"></div><div v-else-if="2" v-for="i in [1]"></div>',
  258. baseOptions
  259. )
  260. expect(
  261. 'Cannot use v-for on stateful component root element because it renders multiple elements'
  262. ).toHaveBeenWarned()
  263. })
  264. it('warn <template> as root element', () => {
  265. parse('<template></template>', baseOptions)
  266. expect('Cannot use <template> as component root element').toHaveBeenWarned()
  267. })
  268. it('warn <slot> as root element', () => {
  269. parse('<slot></slot>', baseOptions)
  270. expect('Cannot use <slot> as component root element').toHaveBeenWarned()
  271. })
  272. it('warn v-for on root element', () => {
  273. parse('<div v-for="item in items"></div>', baseOptions)
  274. expect(
  275. 'Cannot use v-for on stateful component root element'
  276. ).toHaveBeenWarned()
  277. })
  278. it('warn <template> key', () => {
  279. parse(
  280. '<div><template v-for="i in 10" :key="i"></template></div>',
  281. baseOptions
  282. )
  283. expect('<template> cannot be keyed').toHaveBeenWarned()
  284. })
  285. it('warn the child of the <transition-group> component has sequential index', () => {
  286. parse(
  287. `
  288. <div>
  289. <transition-group>
  290. <i v-for="(o, i) of arr" :key="i"></i>
  291. </transition-group>
  292. </div>
  293. `,
  294. baseOptions
  295. )
  296. expect(
  297. 'Do not use v-for index as key on <transition-group> children'
  298. ).toHaveBeenWarned()
  299. })
  300. it('v-pre directive', () => {
  301. const ast = parse(
  302. '<div v-pre id="message1"><p>{{msg}}</p></div>',
  303. baseOptions
  304. )
  305. expect(ast.pre).toBe(true)
  306. expect(ast.attrs[0].name).toBe('id')
  307. expect(ast.attrs[0].value).toBe('"message1"')
  308. expect(ast.children[0].children[0].text).toBe('{{msg}}')
  309. })
  310. it('v-pre directive should leave template in DOM', () => {
  311. const ast = parse(
  312. '<div v-pre id="message1"><template id="template1"><p>{{msg}}</p></template></div>',
  313. baseOptions
  314. )
  315. expect(ast.pre).toBe(true)
  316. expect(ast.attrs[0].name).toBe('id')
  317. expect(ast.attrs[0].value).toBe('"message1"')
  318. expect(ast.children[0].attrs[0].name).toBe('id')
  319. expect(ast.children[0].attrs[0].value).toBe('"template1"')
  320. })
  321. it('v-for directive basic syntax', () => {
  322. const ast = parse('<ul><li v-for="item in items"></li></ul>', baseOptions)
  323. const liAst = ast.children[0]
  324. expect(liAst.for).toBe('items')
  325. expect(liAst.alias).toBe('item')
  326. })
  327. it('v-for directive iteration syntax', () => {
  328. const ast = parse(
  329. '<ul><li v-for="(item, index) in items"></li></ul>',
  330. baseOptions
  331. )
  332. const liAst = ast.children[0]
  333. expect(liAst.for).toBe('items')
  334. expect(liAst.alias).toBe('item')
  335. expect(liAst.iterator1).toBe('index')
  336. expect(liAst.iterator2).toBeUndefined()
  337. })
  338. it('v-for directive iteration syntax (multiple)', () => {
  339. const ast = parse(
  340. '<ul><li v-for="(item, key, index) in items"></li></ul>',
  341. baseOptions
  342. )
  343. const liAst = ast.children[0]
  344. expect(liAst.for).toBe('items')
  345. expect(liAst.alias).toBe('item')
  346. expect(liAst.iterator1).toBe('key')
  347. expect(liAst.iterator2).toBe('index')
  348. })
  349. it('v-for directive key', () => {
  350. const ast = parse(
  351. '<ul><li v-for="item in items" :key="item.uid"></li></ul>',
  352. baseOptions
  353. )
  354. const liAst = ast.children[0]
  355. expect(liAst.for).toBe('items')
  356. expect(liAst.alias).toBe('item')
  357. expect(liAst.key).toBe('item.uid')
  358. })
  359. it('v-for directive destructuring', () => {
  360. let ast = parse('<ul><li v-for="{ foo } in items"></li></ul>', baseOptions)
  361. let liAst = ast.children[0]
  362. expect(liAst.for).toBe('items')
  363. expect(liAst.alias).toBe('{ foo }')
  364. // with paren
  365. ast = parse('<ul><li v-for="({ foo }) in items"></li></ul>', baseOptions)
  366. liAst = ast.children[0]
  367. expect(liAst.for).toBe('items')
  368. expect(liAst.alias).toBe('{ foo }')
  369. // multi-var destructuring
  370. ast = parse(
  371. '<ul><li v-for="{ foo, bar, baz } in items"></li></ul>',
  372. baseOptions
  373. )
  374. liAst = ast.children[0]
  375. expect(liAst.for).toBe('items')
  376. expect(liAst.alias).toBe('{ foo, bar, baz }')
  377. // multi-var destructuring with paren
  378. ast = parse(
  379. '<ul><li v-for="({ foo, bar, baz }) in items"></li></ul>',
  380. baseOptions
  381. )
  382. liAst = ast.children[0]
  383. expect(liAst.for).toBe('items')
  384. expect(liAst.alias).toBe('{ foo, bar, baz }')
  385. // with index
  386. ast = parse('<ul><li v-for="({ foo }, i) in items"></li></ul>', baseOptions)
  387. liAst = ast.children[0]
  388. expect(liAst.for).toBe('items')
  389. expect(liAst.alias).toBe('{ foo }')
  390. expect(liAst.iterator1).toBe('i')
  391. // with key + index
  392. ast = parse(
  393. '<ul><li v-for="({ foo }, i, j) in items"></li></ul>',
  394. baseOptions
  395. )
  396. liAst = ast.children[0]
  397. expect(liAst.for).toBe('items')
  398. expect(liAst.alias).toBe('{ foo }')
  399. expect(liAst.iterator1).toBe('i')
  400. expect(liAst.iterator2).toBe('j')
  401. // multi-var destructuring with index
  402. ast = parse(
  403. '<ul><li v-for="({ foo, bar, baz }, i) in items"></li></ul>',
  404. baseOptions
  405. )
  406. liAst = ast.children[0]
  407. expect(liAst.for).toBe('items')
  408. expect(liAst.alias).toBe('{ foo, bar, baz }')
  409. expect(liAst.iterator1).toBe('i')
  410. // array
  411. ast = parse('<ul><li v-for="[ foo ] in items"></li></ul>', baseOptions)
  412. liAst = ast.children[0]
  413. expect(liAst.for).toBe('items')
  414. expect(liAst.alias).toBe('[ foo ]')
  415. // multi-array
  416. ast = parse(
  417. '<ul><li v-for="[ foo, bar, baz ] in items"></li></ul>',
  418. baseOptions
  419. )
  420. liAst = ast.children[0]
  421. expect(liAst.for).toBe('items')
  422. expect(liAst.alias).toBe('[ foo, bar, baz ]')
  423. // array with paren
  424. ast = parse('<ul><li v-for="([ foo ]) in items"></li></ul>', baseOptions)
  425. liAst = ast.children[0]
  426. expect(liAst.for).toBe('items')
  427. expect(liAst.alias).toBe('[ foo ]')
  428. // multi-array with paren
  429. ast = parse(
  430. '<ul><li v-for="([ foo, bar, baz ]) in items"></li></ul>',
  431. baseOptions
  432. )
  433. liAst = ast.children[0]
  434. expect(liAst.for).toBe('items')
  435. expect(liAst.alias).toBe('[ foo, bar, baz ]')
  436. // array with index
  437. ast = parse('<ul><li v-for="([ foo ], i) in items"></li></ul>', baseOptions)
  438. liAst = ast.children[0]
  439. expect(liAst.for).toBe('items')
  440. expect(liAst.alias).toBe('[ foo ]')
  441. expect(liAst.iterator1).toBe('i')
  442. // array with key + index
  443. ast = parse(
  444. '<ul><li v-for="([ foo ], i, j) in items"></li></ul>',
  445. baseOptions
  446. )
  447. liAst = ast.children[0]
  448. expect(liAst.for).toBe('items')
  449. expect(liAst.alias).toBe('[ foo ]')
  450. expect(liAst.iterator1).toBe('i')
  451. expect(liAst.iterator2).toBe('j')
  452. // multi-array with paren
  453. ast = parse(
  454. '<ul><li v-for="([ foo, bar, baz ]) in items"></li></ul>',
  455. baseOptions
  456. )
  457. liAst = ast.children[0]
  458. expect(liAst.for).toBe('items')
  459. expect(liAst.alias).toBe('[ foo, bar, baz ]')
  460. // multi-array with index
  461. ast = parse(
  462. '<ul><li v-for="([ foo, bar, baz ], i) in items"></li></ul>',
  463. baseOptions
  464. )
  465. liAst = ast.children[0]
  466. expect(liAst.for).toBe('items')
  467. expect(liAst.alias).toBe('[ foo, bar, baz ]')
  468. expect(liAst.iterator1).toBe('i')
  469. // nested
  470. ast = parse(
  471. '<ul><li v-for="({ foo, bar: { baz }, qux: [ n ] }, i, j) in items"></li></ul>',
  472. baseOptions
  473. )
  474. liAst = ast.children[0]
  475. expect(liAst.for).toBe('items')
  476. expect(liAst.alias).toBe('{ foo, bar: { baz }, qux: [ n ] }')
  477. expect(liAst.iterator1).toBe('i')
  478. expect(liAst.iterator2).toBe('j')
  479. // array nested
  480. ast = parse(
  481. '<ul><li v-for="([ foo, { bar }, baz ], i, j) in items"></li></ul>',
  482. baseOptions
  483. )
  484. liAst = ast.children[0]
  485. expect(liAst.for).toBe('items')
  486. expect(liAst.alias).toBe('[ foo, { bar }, baz ]')
  487. expect(liAst.iterator1).toBe('i')
  488. expect(liAst.iterator2).toBe('j')
  489. })
  490. it('v-for directive invalid syntax', () => {
  491. parse('<ul><li v-for="item into items"></li></ul>', baseOptions)
  492. expect('Invalid v-for expression').toHaveBeenWarned()
  493. })
  494. it('v-if directive syntax', () => {
  495. const ast = parse('<p v-if="show">hello world</p>', baseOptions)
  496. expect(ast.if).toBe('show')
  497. expect(ast.ifConditions[0].exp).toBe('show')
  498. })
  499. it('v-else-if directive syntax', () => {
  500. const ast = parse(
  501. '<div><p v-if="show">hello</p><span v-else-if="2">elseif</span><p v-else>world</p></div>',
  502. baseOptions
  503. )
  504. const ifAst = ast.children[0]
  505. const conditionsAst = ifAst.ifConditions
  506. expect(conditionsAst.length).toBe(3)
  507. expect(conditionsAst[1].block.children[0].text).toBe('elseif')
  508. expect(conditionsAst[1].block.parent).toBe(ast)
  509. expect(conditionsAst[2].block.children[0].text).toBe('world')
  510. expect(conditionsAst[2].block.parent).toBe(ast)
  511. })
  512. it('v-else directive syntax', () => {
  513. const ast = parse(
  514. '<div><p v-if="show">hello</p><p v-else>world</p></div>',
  515. baseOptions
  516. )
  517. const ifAst = ast.children[0]
  518. const conditionsAst = ifAst.ifConditions
  519. expect(conditionsAst.length).toBe(2)
  520. expect(conditionsAst[1].block.children[0].text).toBe('world')
  521. expect(conditionsAst[1].block.parent).toBe(ast)
  522. })
  523. it('v-else-if directive invalid syntax', () => {
  524. parse('<div><p v-else-if="1">world</p></div>', baseOptions)
  525. expect('v-else-if="1" used on element').toHaveBeenWarned()
  526. })
  527. it('v-else directive invalid syntax', () => {
  528. parse('<div><p v-else>world</p></div>', baseOptions)
  529. expect('v-else used on element').toHaveBeenWarned()
  530. })
  531. it('v-once directive syntax', () => {
  532. const ast = parse('<p v-once>world</p>', baseOptions)
  533. expect(ast.once).toBe(true)
  534. })
  535. it('slot tag single syntax', () => {
  536. const ast = parse('<div><slot></slot></div>', baseOptions)
  537. expect(ast.children[0].tag).toBe('slot')
  538. expect(ast.children[0].slotName).toBeUndefined()
  539. })
  540. it('slot tag named syntax', () => {
  541. const ast = parse(
  542. '<div><slot name="one">hello world</slot></div>',
  543. baseOptions
  544. )
  545. expect(ast.children[0].tag).toBe('slot')
  546. expect(ast.children[0].slotName).toBe('"one"')
  547. })
  548. it('slot target', () => {
  549. const ast = parse('<p slot="one">hello world</p>', baseOptions)
  550. expect(ast.slotTarget).toBe('"one"')
  551. })
  552. it('component properties', () => {
  553. const ast = parse('<my-component :msg="hello"></my-component>', baseOptions)
  554. expect(ast.attrs[0].name).toBe('msg')
  555. expect(ast.attrs[0].value).toBe('hello')
  556. })
  557. it('component "is" attribute', () => {
  558. const ast = parse(
  559. '<my-component is="component1"></my-component>',
  560. baseOptions
  561. )
  562. expect(ast.component).toBe('"component1"')
  563. })
  564. it('component "inline-template" attribute', () => {
  565. const ast = parse(
  566. '<my-component inline-template>hello world</my-component>',
  567. baseOptions
  568. )
  569. expect(ast.inlineTemplate).toBe(true)
  570. })
  571. it('class binding', () => {
  572. // static
  573. const ast1 = parse('<p class="class1">hello world</p>', baseOptions)
  574. expect(ast1.staticClass).toBe('"class1"')
  575. // dynamic
  576. const ast2 = parse('<p :class="class1">hello world</p>', baseOptions)
  577. expect(ast2.classBinding).toBe('class1')
  578. // interpolation warning
  579. parse('<p class="{{error}}">hello world</p>', baseOptions)
  580. expect(
  581. 'Interpolation inside attributes has been removed'
  582. ).toHaveBeenWarned()
  583. })
  584. it('style binding', () => {
  585. const ast = parse('<p :style="error">hello world</p>', baseOptions)
  586. expect(ast.styleBinding).toBe('error')
  587. })
  588. it('attribute with v-bind', () => {
  589. const ast = parse(
  590. '<input type="text" name="field1" :value="msg">',
  591. baseOptions
  592. )
  593. expect(ast.attrsList[0].name).toBe('type')
  594. expect(ast.attrsList[0].value).toBe('text')
  595. expect(ast.attrsList[1].name).toBe('name')
  596. expect(ast.attrsList[1].value).toBe('field1')
  597. expect(ast.attrsMap['type']).toBe('text')
  598. expect(ast.attrsMap['name']).toBe('field1')
  599. expect(ast.attrs[0].name).toBe('type')
  600. expect(ast.attrs[0].value).toBe('"text"')
  601. expect(ast.attrs[1].name).toBe('name')
  602. expect(ast.attrs[1].value).toBe('"field1"')
  603. expect(ast.props[0].name).toBe('value')
  604. expect(ast.props[0].value).toBe('msg')
  605. })
  606. it('empty v-bind expression', () => {
  607. parse('<div :empty-msg=""></div>', baseOptions)
  608. expect(
  609. 'The value for a v-bind expression cannot be empty. Found in "v-bind:empty-msg"'
  610. ).toHaveBeenWarned()
  611. })
  612. if (process.env.VBIND_PROP_SHORTHAND) {
  613. it('v-bind.prop shorthand syntax', () => {
  614. const ast = parse('<div .id="foo"></div>', baseOptions)
  615. expect(ast.props).toEqual([{ name: 'id', value: 'foo', dynamic: false }])
  616. })
  617. it('v-bind.prop shorthand syntax w/ modifiers', () => {
  618. const ast = parse('<div .id.mod="foo"></div>', baseOptions)
  619. expect(ast.props).toEqual([{ name: 'id', value: 'foo', dynamic: false }])
  620. })
  621. it('v-bind.prop shorthand dynamic argument', () => {
  622. const ast = parse('<div .[id]="foo"></div>', baseOptions)
  623. expect(ast.props).toEqual([{ name: 'id', value: 'foo', dynamic: true }])
  624. })
  625. }
  626. // This only works for string templates.
  627. // In-DOM templates will be malformed before Vue can parse it.
  628. describe('parse and warn invalid dynamic arguments', () => {
  629. ;[
  630. `<div v-bind:['foo' + bar]="baz"/>`,
  631. `<div :['foo' + bar]="baz"/>`,
  632. `<div @['foo' + bar]="baz"/>`,
  633. `<foo #['foo' + bar]="baz"/>`,
  634. `<div :['foo' + bar].some.mod="baz"/>`
  635. ].forEach(template => {
  636. it(template, () => {
  637. const ast = parse(template, baseOptions)
  638. expect(`Invalid dynamic argument expression`).toHaveBeenWarned()
  639. })
  640. })
  641. })
  642. // #9781
  643. it('multiple dynamic slot names without warning', () => {
  644. const ast = parse(
  645. `<my-component>
  646. <template #[foo]>foo</template>
  647. <template #[data]="scope">scope</template>
  648. <template #[bar]>bar</template>
  649. </my-component>`,
  650. baseOptions
  651. )
  652. expect(`Invalid dynamic argument expression`).not.toHaveBeenWarned()
  653. expect(ast.scopedSlots.foo).not.toBeUndefined()
  654. expect(ast.scopedSlots.data).not.toBeUndefined()
  655. expect(ast.scopedSlots.bar).not.toBeUndefined()
  656. expect(ast.scopedSlots.foo.type).toBe(1)
  657. expect(ast.scopedSlots.data.type).toBe(1)
  658. expect(ast.scopedSlots.bar.type).toBe(1)
  659. expect(ast.scopedSlots.foo.attrsMap['#[foo]']).toBe('')
  660. expect(ast.scopedSlots.bar.attrsMap['#[bar]']).toBe('')
  661. expect(ast.scopedSlots.data.attrsMap['#[data]']).toBe('scope')
  662. })
  663. // #6887
  664. it('special case static attribute that must be props', () => {
  665. const ast = parse('<video muted></video>', baseOptions)
  666. expect(ast.attrs[0].name).toBe('muted')
  667. expect(ast.attrs[0].value).toBe('""')
  668. expect(ast.props[0].name).toBe('muted')
  669. expect(ast.props[0].value).toBe('true')
  670. })
  671. it('attribute with v-on', () => {
  672. const ast = parse(
  673. '<input type="text" name="field1" :value="msg" @input="onInput">',
  674. baseOptions
  675. )
  676. expect(ast.events.input.value).toBe('onInput')
  677. })
  678. it('attribute with directive', () => {
  679. const ast = parse(
  680. '<input type="text" name="field1" :value="msg" v-validate:field1="required">',
  681. baseOptions
  682. )
  683. expect(ast.directives[0].name).toBe('validate')
  684. expect(ast.directives[0].value).toBe('required')
  685. expect(ast.directives[0].arg).toBe('field1')
  686. })
  687. it('attribute with modified directive', () => {
  688. const ast = parse(
  689. '<input type="text" name="field1" :value="msg" v-validate.on.off>',
  690. baseOptions
  691. )
  692. expect(ast.directives[0].modifiers.on).toBe(true)
  693. expect(ast.directives[0].modifiers.off).toBe(true)
  694. })
  695. it('literal attribute', () => {
  696. // basic
  697. const ast1 = parse(
  698. '<input type="text" name="field1" value="hello world">',
  699. baseOptions
  700. )
  701. expect(ast1.attrsList[0].name).toBe('type')
  702. expect(ast1.attrsList[0].value).toBe('text')
  703. expect(ast1.attrsList[1].name).toBe('name')
  704. expect(ast1.attrsList[1].value).toBe('field1')
  705. expect(ast1.attrsList[2].name).toBe('value')
  706. expect(ast1.attrsList[2].value).toBe('hello world')
  707. expect(ast1.attrsMap['type']).toBe('text')
  708. expect(ast1.attrsMap['name']).toBe('field1')
  709. expect(ast1.attrsMap['value']).toBe('hello world')
  710. expect(ast1.attrs[0].name).toBe('type')
  711. expect(ast1.attrs[0].value).toBe('"text"')
  712. expect(ast1.attrs[1].name).toBe('name')
  713. expect(ast1.attrs[1].value).toBe('"field1"')
  714. expect(ast1.attrs[2].name).toBe('value')
  715. expect(ast1.attrs[2].value).toBe('"hello world"')
  716. // interpolation warning
  717. parse('<input type="text" name="field1" value="{{msg}}">', baseOptions)
  718. expect(
  719. 'Interpolation inside attributes has been removed'
  720. ).toHaveBeenWarned()
  721. })
  722. if (!isIE && !isEdge) {
  723. it('duplicate attribute', () => {
  724. parse('<p class="class1" class="class1">hello world</p>', baseOptions)
  725. expect('duplicate attribute').toHaveBeenWarned()
  726. })
  727. }
  728. it('custom delimiter', () => {
  729. const ast = parse(
  730. '<p>{msg}</p>',
  731. extend({ delimiters: ['{', '}'] }, baseOptions)
  732. )
  733. expect(ast.children[0].expression).toBe('_s(msg)')
  734. })
  735. it('not specified getTagNamespace option', () => {
  736. const options = extend({}, baseOptions)
  737. delete options.getTagNamespace
  738. const ast = parse('<svg><text>hello world</text></svg>', options)
  739. expect(ast.tag).toBe('svg')
  740. expect(ast.ns).toBeUndefined()
  741. })
  742. it('not specified mustUseProp', () => {
  743. const options = extend({}, baseOptions)
  744. delete options.mustUseProp
  745. const ast = parse('<input type="text" name="field1" :value="msg">', options)
  746. expect(ast.props).toBeUndefined()
  747. })
  748. it('use prop when prop modifier was explicitly declared', () => {
  749. const ast = parse(
  750. '<component is="textarea" :value.prop="val" />',
  751. baseOptions
  752. )
  753. expect(ast.attrs).toBeUndefined()
  754. expect(ast.props.length).toBe(1)
  755. expect(ast.props[0].name).toBe('value')
  756. expect(ast.props[0].value).toBe('val')
  757. })
  758. it('pre/post transforms', () => {
  759. const options = extend({}, baseOptions)
  760. const spy1 = vi.fn()
  761. const spy2 = vi.fn()
  762. options.modules = options.modules.concat([
  763. {
  764. preTransformNode(el) {
  765. spy1(el.tag)
  766. },
  767. postTransformNode(el) {
  768. expect(el.attrs.length).toBe(1)
  769. spy2(el.tag)
  770. }
  771. }
  772. ])
  773. parse('<img v-pre src="hi">', options)
  774. expect(spy1).toHaveBeenCalledWith('img')
  775. expect(spy2).toHaveBeenCalledWith('img')
  776. })
  777. it('preserve whitespace in <pre> tag', function () {
  778. const options = extend({}, baseOptions)
  779. const ast = parse(
  780. '<pre><code> \n<span>hi</span>\n </code><span> </span></pre>',
  781. options
  782. )
  783. const code = ast.children[0]
  784. expect(code.children[0].type).toBe(3)
  785. expect(code.children[0].text).toBe(' \n')
  786. expect(code.children[2].type).toBe(3)
  787. expect(code.children[2].text).toBe('\n ')
  788. const span = ast.children[1]
  789. expect(span.children[0].type).toBe(3)
  790. expect(span.children[0].text).toBe(' ')
  791. })
  792. // #5992
  793. it('ignore the first newline in <pre> tag', function () {
  794. const options = extend({}, baseOptions)
  795. const ast = parse(
  796. '<div><pre>\nabc</pre>\ndef<pre>\n\nabc</pre></div>',
  797. options
  798. )
  799. const pre = ast.children[0]
  800. expect(pre.children[0].type).toBe(3)
  801. expect(pre.children[0].text).toBe('abc')
  802. const text = ast.children[1]
  803. expect(text.type).toBe(3)
  804. expect(text.text).toBe('\ndef')
  805. const pre2 = ast.children[2]
  806. expect(pre2.children[0].type).toBe(3)
  807. expect(pre2.children[0].text).toBe('\nabc')
  808. })
  809. it('keep first newline after unary tag in <pre>', () => {
  810. const options = extend({}, baseOptions)
  811. const ast = parse('<pre>abc<input>\ndef</pre>', options)
  812. expect(ast.children[1].type).toBe(1)
  813. expect(ast.children[1].tag).toBe('input')
  814. expect(ast.children[2].type).toBe(3)
  815. expect(ast.children[2].text).toBe('\ndef')
  816. })
  817. it('forgivingly handle < in plain text', () => {
  818. const options = extend({}, baseOptions)
  819. const ast = parse('<p>1 < 2 < 3</p>', options)
  820. expect(ast.tag).toBe('p')
  821. expect(ast.children.length).toBe(1)
  822. expect(ast.children[0].type).toBe(3)
  823. expect(ast.children[0].text).toBe('1 < 2 < 3')
  824. })
  825. it('IE conditional comments', () => {
  826. const options = extend({}, baseOptions)
  827. const ast = parse(
  828. `
  829. <div>
  830. <!--[if lte IE 8]>
  831. <p>Test 1</p>
  832. <![endif]-->
  833. </div>
  834. `,
  835. options
  836. )
  837. expect(ast.tag).toBe('div')
  838. expect(ast.children.length).toBe(0)
  839. })
  840. it('parse content in textarea as text', () => {
  841. const options = extend({}, baseOptions)
  842. const whitespace = parse(
  843. `
  844. <textarea>
  845. <p>Test 1</p>
  846. test2
  847. </textarea>
  848. `,
  849. options
  850. )
  851. expect(whitespace.tag).toBe('textarea')
  852. expect(whitespace.children.length).toBe(1)
  853. expect(whitespace.children[0].type).toBe(3)
  854. // textarea is whitespace sensitive
  855. expect(whitespace.children[0].text).toBe(` <p>Test 1</p>
  856. test2
  857. `)
  858. const comment = parse('<textarea><!--comment--></textarea>', options)
  859. expect(comment.tag).toBe('textarea')
  860. expect(comment.children.length).toBe(1)
  861. expect(comment.children[0].type).toBe(3)
  862. expect(comment.children[0].text).toBe('<!--comment-->')
  863. })
  864. // #5526
  865. it('should not decode text in script tags', () => {
  866. const options = extend({}, baseOptions)
  867. const ast = parse(
  868. `<script type="x/template">&gt;<foo>&lt;</script>`,
  869. options
  870. )
  871. expect(ast.children[0].text).toBe(`&gt;<foo>&lt;`)
  872. })
  873. it('should ignore comments', () => {
  874. const options = extend({}, baseOptions)
  875. const ast = parse(`<div>123<!--comment here--></div>`, options)
  876. expect(ast.tag).toBe('div')
  877. expect(ast.children.length).toBe(1)
  878. expect(ast.children[0].type).toBe(3)
  879. expect(ast.children[0].text).toBe('123')
  880. })
  881. it('should kept comments', () => {
  882. const options = extend(
  883. {
  884. comments: true
  885. },
  886. baseOptions
  887. )
  888. const ast = parse(`<div>123<!--comment here--></div>`, options)
  889. expect(ast.tag).toBe('div')
  890. expect(ast.children.length).toBe(2)
  891. expect(ast.children[0].type).toBe(3)
  892. expect(ast.children[0].text).toBe('123')
  893. expect(ast.children[1].type).toBe(3) // parse comment with ASTText
  894. expect(ast.children[1].isComment).toBe(true) // parse comment with ASTText
  895. expect(ast.children[1].text).toBe('comment here')
  896. })
  897. // #9407
  898. it('should parse templates with comments anywhere', () => {
  899. const options = extend(
  900. {
  901. comments: true
  902. },
  903. baseOptions
  904. )
  905. const ast = parse(`<!--comment here--><div>123</div>`, options)
  906. expect(ast.tag).toBe('div')
  907. expect(ast.children.length).toBe(1)
  908. })
  909. // #8103
  910. it('should allow CRLFs in string interpolations', () => {
  911. const ast = parse(`<p>{{\r\nmsg\r\n}}</p>`, baseOptions)
  912. expect(ast.children[0].expression).toBe('_s(msg)')
  913. })
  914. it('preserveWhitespace: false', () => {
  915. const options = extend(
  916. {
  917. preserveWhitespace: false
  918. },
  919. baseOptions
  920. )
  921. const ast = parse(
  922. '<p>\n Welcome to <b>Vue.js</b> <i>world</i> \n <span>.\n Have fun!\n</span></p>',
  923. options
  924. )
  925. expect(ast.tag).toBe('p')
  926. expect(ast.children.length).toBe(4)
  927. expect(ast.children[0].type).toBe(3)
  928. expect(ast.children[0].text).toBe('\n Welcome to ')
  929. expect(ast.children[1].tag).toBe('b')
  930. expect(ast.children[1].children[0].text).toBe('Vue.js')
  931. expect(ast.children[2].tag).toBe('i')
  932. expect(ast.children[2].children[0].text).toBe('world')
  933. expect(ast.children[3].tag).toBe('span')
  934. expect(ast.children[3].children[0].text).toBe('.\n Have fun!\n')
  935. })
  936. const condenseOptions = extend(
  937. {
  938. whitespace: 'condense',
  939. // should be ignored when whitespace is specified
  940. preserveWhitespace: false
  941. },
  942. baseOptions
  943. )
  944. it(`whitespace: 'condense'`, () => {
  945. const options = extend({}, condenseOptions)
  946. const ast = parse(
  947. '<p>\n Welcome to <b>Vue.js</b> <i>world</i> \n <span>.\n Have fun!\n</span></p>',
  948. options
  949. )
  950. expect(ast.tag).toBe('p')
  951. expect(ast.children.length).toBe(5)
  952. expect(ast.children[0].type).toBe(3)
  953. expect(ast.children[0].text).toBe(' Welcome to ')
  954. expect(ast.children[1].tag).toBe('b')
  955. expect(ast.children[1].children[0].text).toBe('Vue.js')
  956. expect(ast.children[2].type).toBe(3)
  957. // should condense inline whitespace into single space
  958. expect(ast.children[2].text).toBe(' ')
  959. expect(ast.children[3].tag).toBe('i')
  960. expect(ast.children[3].children[0].text).toBe('world')
  961. // should have removed the whitespace node between tags that contains newlines
  962. expect(ast.children[4].tag).toBe('span')
  963. expect(ast.children[4].children[0].text).toBe('. Have fun! ')
  964. })
  965. it(`maintains &nbsp; with whitespace: 'condense'`, () => {
  966. const options = extend({}, condenseOptions)
  967. const ast = parse('<span>&nbsp;</span>', options)
  968. const code = ast.children[0]
  969. expect(code.type).toBe(3)
  970. expect(code.text).toBe('\xA0')
  971. })
  972. it(`preserve whitespace in <pre> tag with whitespace: 'condense'`, function () {
  973. const options = extend({}, condenseOptions)
  974. const ast = parse(
  975. '<pre><code> \n<span>hi</span>\n </code><span> </span></pre>',
  976. options
  977. )
  978. const code = ast.children[0]
  979. expect(code.children[0].type).toBe(3)
  980. expect(code.children[0].text).toBe(' \n')
  981. expect(code.children[2].type).toBe(3)
  982. expect(code.children[2].text).toBe('\n ')
  983. const span = ast.children[1]
  984. expect(span.children[0].type).toBe(3)
  985. expect(span.children[0].text).toBe(' ')
  986. })
  987. it(`ignore the first newline in <pre> tag with whitespace: 'condense'`, function () {
  988. const options = extend({}, condenseOptions)
  989. const ast = parse(
  990. '<div><pre>\nabc</pre>\ndef<pre>\n\nabc</pre></div>',
  991. options
  992. )
  993. const pre = ast.children[0]
  994. expect(pre.children[0].type).toBe(3)
  995. expect(pre.children[0].text).toBe('abc')
  996. const text = ast.children[1]
  997. expect(text.type).toBe(3)
  998. expect(text.text).toBe(' def')
  999. const pre2 = ast.children[2]
  1000. expect(pre2.children[0].type).toBe(3)
  1001. expect(pre2.children[0].text).toBe('\nabc')
  1002. })
  1003. it(`keep first newline after unary tag in <pre> with whitespace: 'condense'`, () => {
  1004. const options = extend({}, condenseOptions)
  1005. const ast = parse('<pre>abc<input>\ndef</pre>', options)
  1006. expect(ast.children[1].type).toBe(1)
  1007. expect(ast.children[1].tag).toBe('input')
  1008. expect(ast.children[2].type).toBe(3)
  1009. expect(ast.children[2].text).toBe('\ndef')
  1010. })
  1011. // #10152
  1012. it('not warn when scoped slot used inside of dynamic component on regular element', () => {
  1013. parse(
  1014. `
  1015. <div>
  1016. <div is="customComp" v-slot="slotProps"></div>
  1017. <div :is="'customComp'" v-slot="slotProps"></div>
  1018. <div v-bind:is="'customComp'" v-slot="slotProps"></div>
  1019. </div>
  1020. `,
  1021. baseOptions
  1022. )
  1023. expect(
  1024. 'v-slot can only be used on components or <template>'
  1025. ).not.toHaveBeenWarned()
  1026. parse(
  1027. `<div is="customComp"><template v-slot="slotProps"></template></div>`,
  1028. baseOptions
  1029. )
  1030. expect(
  1031. `<template v-slot> can only appear at the root level inside the receiving the component`
  1032. ).not.toHaveBeenWarned()
  1033. })
  1034. })