parse.spec.ts 75 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754
  1. import { parse, ParserOptions, TextModes } from '../src/parse'
  2. import { ErrorCodes } from '../src/errors'
  3. import {
  4. CommentNode,
  5. ElementNode,
  6. ElementTypes,
  7. Namespaces,
  8. NodeTypes,
  9. Position,
  10. TextNode,
  11. AttributeNode,
  12. InterpolationNode
  13. } from '../src/ast'
  14. describe('compiler: parse', () => {
  15. describe('Text', () => {
  16. test('simple text', () => {
  17. const ast = parse('some text')
  18. const text = ast.children[0] as TextNode
  19. expect(text).toStrictEqual({
  20. type: NodeTypes.TEXT,
  21. content: 'some text',
  22. loc: {
  23. start: { offset: 0, line: 1, column: 1 },
  24. end: { offset: 9, line: 1, column: 10 },
  25. source: 'some text'
  26. }
  27. })
  28. })
  29. test('simple text with invalid end tag', () => {
  30. const ast = parse('some text</div>', {
  31. onError: () => {}
  32. })
  33. const text = ast.children[0] as TextNode
  34. expect(text).toStrictEqual({
  35. type: NodeTypes.TEXT,
  36. content: 'some text',
  37. loc: {
  38. start: { offset: 0, line: 1, column: 1 },
  39. end: { offset: 9, line: 1, column: 10 },
  40. source: 'some text'
  41. }
  42. })
  43. })
  44. test('text with interpolation', () => {
  45. const ast = parse('some {{ foo + bar }} text')
  46. const text1 = ast.children[0] as TextNode
  47. const text2 = ast.children[2] as TextNode
  48. expect(text1).toStrictEqual({
  49. type: NodeTypes.TEXT,
  50. content: 'some ',
  51. loc: {
  52. start: { offset: 0, line: 1, column: 1 },
  53. end: { offset: 5, line: 1, column: 6 },
  54. source: 'some '
  55. }
  56. })
  57. expect(text2).toStrictEqual({
  58. type: NodeTypes.TEXT,
  59. content: ' text',
  60. loc: {
  61. start: { offset: 20, line: 1, column: 21 },
  62. end: { offset: 25, line: 1, column: 26 },
  63. source: ' text'
  64. }
  65. })
  66. })
  67. test('text with interpolation which has `<`', () => {
  68. const ast = parse('some {{ a<b && c>d }} text')
  69. const text1 = ast.children[0] as TextNode
  70. const text2 = ast.children[2] as TextNode
  71. expect(text1).toStrictEqual({
  72. type: NodeTypes.TEXT,
  73. content: 'some ',
  74. loc: {
  75. start: { offset: 0, line: 1, column: 1 },
  76. end: { offset: 5, line: 1, column: 6 },
  77. source: 'some '
  78. }
  79. })
  80. expect(text2).toStrictEqual({
  81. type: NodeTypes.TEXT,
  82. content: ' text',
  83. loc: {
  84. start: { offset: 21, line: 1, column: 22 },
  85. end: { offset: 26, line: 1, column: 27 },
  86. source: ' text'
  87. }
  88. })
  89. })
  90. test('text with mix of tags and interpolations', () => {
  91. const ast = parse('some <span>{{ foo < bar + foo }} text</span>')
  92. const text1 = ast.children[0] as TextNode
  93. const text2 = (ast.children[1] as ElementNode).children![1] as TextNode
  94. expect(text1).toStrictEqual({
  95. type: NodeTypes.TEXT,
  96. content: 'some ',
  97. loc: {
  98. start: { offset: 0, line: 1, column: 1 },
  99. end: { offset: 5, line: 1, column: 6 },
  100. source: 'some '
  101. }
  102. })
  103. expect(text2).toStrictEqual({
  104. type: NodeTypes.TEXT,
  105. content: ' text',
  106. loc: {
  107. start: { offset: 32, line: 1, column: 33 },
  108. end: { offset: 37, line: 1, column: 38 },
  109. source: ' text'
  110. }
  111. })
  112. })
  113. test('lonly "<" don\'t separate nodes', () => {
  114. const ast = parse('a < b', {
  115. onError: err => {
  116. if (err.code !== ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME) {
  117. throw err
  118. }
  119. }
  120. })
  121. const text = ast.children[0] as TextNode
  122. expect(text).toStrictEqual({
  123. type: NodeTypes.TEXT,
  124. content: 'a < b',
  125. loc: {
  126. start: { offset: 0, line: 1, column: 1 },
  127. end: { offset: 5, line: 1, column: 6 },
  128. source: 'a < b'
  129. }
  130. })
  131. })
  132. test('lonly "{{" don\'t separate nodes', () => {
  133. const ast = parse('a {{ b', {
  134. onError: error => {
  135. if (error.code !== ErrorCodes.X_MISSING_INTERPOLATION_END) {
  136. throw error
  137. }
  138. }
  139. })
  140. const text = ast.children[0] as TextNode
  141. expect(text).toStrictEqual({
  142. type: NodeTypes.TEXT,
  143. content: 'a {{ b',
  144. loc: {
  145. start: { offset: 0, line: 1, column: 1 },
  146. end: { offset: 6, line: 1, column: 7 },
  147. source: 'a {{ b'
  148. }
  149. })
  150. })
  151. test('HTML entities compatibility in text (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).', () => {
  152. const spy = jest.fn()
  153. const ast = parse('&ampersand;', {
  154. namedCharacterReferences: { amp: '&' },
  155. onError: spy
  156. })
  157. const text = ast.children[0] as TextNode
  158. expect(text).toStrictEqual({
  159. type: NodeTypes.TEXT,
  160. content: '&ersand;',
  161. loc: {
  162. start: { offset: 0, line: 1, column: 1 },
  163. end: { offset: 11, line: 1, column: 12 },
  164. source: '&ampersand;'
  165. }
  166. })
  167. expect(spy.mock.calls).toMatchObject([
  168. [
  169. {
  170. code: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
  171. loc: {
  172. start: { offset: 4, line: 1, column: 5 }
  173. }
  174. }
  175. ]
  176. ])
  177. })
  178. test('HTML entities compatibility in attribute (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).', () => {
  179. const spy = jest.fn()
  180. const ast = parse(
  181. '<div a="&ampersand;" b="&amp;ersand;" c="&amp!"></div>',
  182. {
  183. namedCharacterReferences: { amp: '&', 'amp;': '&' },
  184. onError: spy
  185. }
  186. )
  187. const element = ast.children[0] as ElementNode
  188. const text1 = (element.props[0] as AttributeNode).value
  189. const text2 = (element.props[1] as AttributeNode).value
  190. const text3 = (element.props[2] as AttributeNode).value
  191. expect(text1).toStrictEqual({
  192. type: NodeTypes.TEXT,
  193. content: '&ampersand;',
  194. loc: {
  195. start: { offset: 7, line: 1, column: 8 },
  196. end: { offset: 20, line: 1, column: 21 },
  197. source: '"&ampersand;"'
  198. }
  199. })
  200. expect(text2).toStrictEqual({
  201. type: NodeTypes.TEXT,
  202. content: '&ersand;',
  203. loc: {
  204. start: { offset: 23, line: 1, column: 24 },
  205. end: { offset: 37, line: 1, column: 38 },
  206. source: '"&amp;ersand;"'
  207. }
  208. })
  209. expect(text3).toStrictEqual({
  210. type: NodeTypes.TEXT,
  211. content: '&!',
  212. loc: {
  213. start: { offset: 40, line: 1, column: 41 },
  214. end: { offset: 47, line: 1, column: 48 },
  215. source: '"&amp!"'
  216. }
  217. })
  218. expect(spy.mock.calls).toMatchObject([
  219. [
  220. {
  221. code: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
  222. loc: {
  223. start: { offset: 45, line: 1, column: 46 }
  224. }
  225. }
  226. ]
  227. ])
  228. })
  229. test('Some control character reference should be replaced.', () => {
  230. const spy = jest.fn()
  231. const ast = parse('&#x86;', { onError: spy })
  232. const text = ast.children[0] as TextNode
  233. expect(text).toStrictEqual({
  234. type: NodeTypes.TEXT,
  235. content: '†',
  236. loc: {
  237. start: { offset: 0, line: 1, column: 1 },
  238. end: { offset: 6, line: 1, column: 7 },
  239. source: '&#x86;'
  240. }
  241. })
  242. expect(spy.mock.calls).toMatchObject([
  243. [
  244. {
  245. code: ErrorCodes.CONTROL_CHARACTER_REFERENCE,
  246. loc: {
  247. start: { offset: 0, line: 1, column: 1 }
  248. }
  249. }
  250. ]
  251. ])
  252. })
  253. })
  254. describe('Interpolation', () => {
  255. test('simple interpolation', () => {
  256. const ast = parse('{{message}}')
  257. const interpolation = ast.children[0] as InterpolationNode
  258. expect(interpolation).toStrictEqual({
  259. type: NodeTypes.INTERPOLATION,
  260. content: {
  261. type: NodeTypes.SIMPLE_EXPRESSION,
  262. content: `message`,
  263. isStatic: false,
  264. isConstant: false,
  265. loc: {
  266. start: { offset: 2, line: 1, column: 3 },
  267. end: { offset: 9, line: 1, column: 10 },
  268. source: `message`
  269. }
  270. },
  271. loc: {
  272. start: { offset: 0, line: 1, column: 1 },
  273. end: { offset: 11, line: 1, column: 12 },
  274. source: '{{message}}'
  275. }
  276. })
  277. })
  278. test('it can have tag-like notation', () => {
  279. const ast = parse('{{ a<b }}')
  280. const interpolation = ast.children[0] as InterpolationNode
  281. expect(interpolation).toStrictEqual({
  282. type: NodeTypes.INTERPOLATION,
  283. content: {
  284. type: NodeTypes.SIMPLE_EXPRESSION,
  285. content: `a<b`,
  286. isStatic: false,
  287. isConstant: false,
  288. loc: {
  289. start: { offset: 3, line: 1, column: 4 },
  290. end: { offset: 6, line: 1, column: 7 },
  291. source: 'a<b'
  292. }
  293. },
  294. loc: {
  295. start: { offset: 0, line: 1, column: 1 },
  296. end: { offset: 9, line: 1, column: 10 },
  297. source: '{{ a<b }}'
  298. }
  299. })
  300. })
  301. test('it can have tag-like notation (2)', () => {
  302. const ast = parse('{{ a<b }}{{ c>d }}')
  303. const interpolation1 = ast.children[0] as InterpolationNode
  304. const interpolation2 = ast.children[1] as InterpolationNode
  305. expect(interpolation1).toStrictEqual({
  306. type: NodeTypes.INTERPOLATION,
  307. content: {
  308. type: NodeTypes.SIMPLE_EXPRESSION,
  309. content: `a<b`,
  310. isStatic: false,
  311. isConstant: false,
  312. loc: {
  313. start: { offset: 3, line: 1, column: 4 },
  314. end: { offset: 6, line: 1, column: 7 },
  315. source: 'a<b'
  316. }
  317. },
  318. loc: {
  319. start: { offset: 0, line: 1, column: 1 },
  320. end: { offset: 9, line: 1, column: 10 },
  321. source: '{{ a<b }}'
  322. }
  323. })
  324. expect(interpolation2).toStrictEqual({
  325. type: NodeTypes.INTERPOLATION,
  326. content: {
  327. type: NodeTypes.SIMPLE_EXPRESSION,
  328. isStatic: false,
  329. isConstant: false,
  330. content: 'c>d',
  331. loc: {
  332. start: { offset: 12, line: 1, column: 13 },
  333. end: { offset: 15, line: 1, column: 16 },
  334. source: 'c>d'
  335. }
  336. },
  337. loc: {
  338. start: { offset: 9, line: 1, column: 10 },
  339. end: { offset: 18, line: 1, column: 19 },
  340. source: '{{ c>d }}'
  341. }
  342. })
  343. })
  344. test('it can have tag-like notation (3)', () => {
  345. const ast = parse('<div>{{ "</div>" }}</div>')
  346. const element = ast.children[0] as ElementNode
  347. const interpolation = element.children[0] as InterpolationNode
  348. expect(interpolation).toStrictEqual({
  349. type: NodeTypes.INTERPOLATION,
  350. content: {
  351. type: NodeTypes.SIMPLE_EXPRESSION,
  352. isStatic: false,
  353. // The `isConstant` is the default value and will be determined in `transformExpression`.
  354. isConstant: false,
  355. content: '"</div>"',
  356. loc: {
  357. start: { offset: 8, line: 1, column: 9 },
  358. end: { offset: 16, line: 1, column: 17 },
  359. source: '"</div>"'
  360. }
  361. },
  362. loc: {
  363. start: { offset: 5, line: 1, column: 6 },
  364. end: { offset: 19, line: 1, column: 20 },
  365. source: '{{ "</div>" }}'
  366. }
  367. })
  368. })
  369. test('custom delimiters', () => {
  370. const ast = parse('<p>{msg}</p>', {
  371. delimiters: ['{', '}']
  372. })
  373. const element = ast.children[0] as ElementNode
  374. const interpolation = element.children[0] as InterpolationNode
  375. expect(interpolation).toStrictEqual({
  376. type: NodeTypes.INTERPOLATION,
  377. content: {
  378. type: NodeTypes.SIMPLE_EXPRESSION,
  379. content: `msg`,
  380. isStatic: false,
  381. isConstant: false,
  382. loc: {
  383. start: { offset: 4, line: 1, column: 5 },
  384. end: { offset: 7, line: 1, column: 8 },
  385. source: 'msg'
  386. }
  387. },
  388. loc: {
  389. start: { offset: 3, line: 1, column: 4 },
  390. end: { offset: 8, line: 1, column: 9 },
  391. source: '{msg}'
  392. }
  393. })
  394. })
  395. })
  396. describe('Comment', () => {
  397. test('empty comment', () => {
  398. const ast = parse('<!---->')
  399. const comment = ast.children[0] as CommentNode
  400. expect(comment).toStrictEqual({
  401. type: NodeTypes.COMMENT,
  402. content: '',
  403. loc: {
  404. start: { offset: 0, line: 1, column: 1 },
  405. end: { offset: 7, line: 1, column: 8 },
  406. source: '<!---->'
  407. }
  408. })
  409. })
  410. test('simple comment', () => {
  411. const ast = parse('<!--abc-->')
  412. const comment = ast.children[0] as CommentNode
  413. expect(comment).toStrictEqual({
  414. type: NodeTypes.COMMENT,
  415. content: 'abc',
  416. loc: {
  417. start: { offset: 0, line: 1, column: 1 },
  418. end: { offset: 10, line: 1, column: 11 },
  419. source: '<!--abc-->'
  420. }
  421. })
  422. })
  423. test('two comments', () => {
  424. const ast = parse('<!--abc--><!--def-->')
  425. const comment1 = ast.children[0] as CommentNode
  426. const comment2 = ast.children[1] as CommentNode
  427. expect(comment1).toStrictEqual({
  428. type: NodeTypes.COMMENT,
  429. content: 'abc',
  430. loc: {
  431. start: { offset: 0, line: 1, column: 1 },
  432. end: { offset: 10, line: 1, column: 11 },
  433. source: '<!--abc-->'
  434. }
  435. })
  436. expect(comment2).toStrictEqual({
  437. type: NodeTypes.COMMENT,
  438. content: 'def',
  439. loc: {
  440. start: { offset: 10, line: 1, column: 11 },
  441. end: { offset: 20, line: 1, column: 21 },
  442. source: '<!--def-->'
  443. }
  444. })
  445. })
  446. })
  447. describe('Element', () => {
  448. test('simple div', () => {
  449. const ast = parse('<div>hello</div>')
  450. const element = ast.children[0] as ElementNode
  451. expect(element).toStrictEqual({
  452. type: NodeTypes.ELEMENT,
  453. ns: Namespaces.HTML,
  454. tag: 'div',
  455. tagType: ElementTypes.ELEMENT,
  456. codegenNode: undefined,
  457. props: [],
  458. isSelfClosing: false,
  459. children: [
  460. {
  461. type: NodeTypes.TEXT,
  462. content: 'hello',
  463. loc: {
  464. start: { offset: 5, line: 1, column: 6 },
  465. end: { offset: 10, line: 1, column: 11 },
  466. source: 'hello'
  467. }
  468. }
  469. ],
  470. loc: {
  471. start: { offset: 0, line: 1, column: 1 },
  472. end: { offset: 16, line: 1, column: 17 },
  473. source: '<div>hello</div>'
  474. }
  475. })
  476. })
  477. test('empty', () => {
  478. const ast = parse('<div></div>')
  479. const element = ast.children[0] as ElementNode
  480. expect(element).toStrictEqual({
  481. type: NodeTypes.ELEMENT,
  482. ns: Namespaces.HTML,
  483. tag: 'div',
  484. tagType: ElementTypes.ELEMENT,
  485. codegenNode: undefined,
  486. props: [],
  487. isSelfClosing: false,
  488. children: [],
  489. loc: {
  490. start: { offset: 0, line: 1, column: 1 },
  491. end: { offset: 11, line: 1, column: 12 },
  492. source: '<div></div>'
  493. }
  494. })
  495. })
  496. test('self closing', () => {
  497. const ast = parse('<div/>after')
  498. const element = ast.children[0] as ElementNode
  499. expect(element).toStrictEqual({
  500. type: NodeTypes.ELEMENT,
  501. ns: Namespaces.HTML,
  502. tag: 'div',
  503. tagType: ElementTypes.ELEMENT,
  504. codegenNode: undefined,
  505. props: [],
  506. isSelfClosing: true,
  507. children: [],
  508. loc: {
  509. start: { offset: 0, line: 1, column: 1 },
  510. end: { offset: 6, line: 1, column: 7 },
  511. source: '<div/>'
  512. }
  513. })
  514. })
  515. test('void element', () => {
  516. const ast = parse('<img>after', {
  517. isVoidTag: tag => tag === 'img'
  518. })
  519. const element = ast.children[0] as ElementNode
  520. expect(element).toStrictEqual({
  521. type: NodeTypes.ELEMENT,
  522. ns: Namespaces.HTML,
  523. tag: 'img',
  524. tagType: ElementTypes.ELEMENT,
  525. codegenNode: undefined,
  526. props: [],
  527. isSelfClosing: false,
  528. children: [],
  529. loc: {
  530. start: { offset: 0, line: 1, column: 1 },
  531. end: { offset: 5, line: 1, column: 6 },
  532. source: '<img>'
  533. }
  534. })
  535. })
  536. test('native element with `isNativeTag`', () => {
  537. const ast = parse('<div></div><comp></comp><Comp></Comp>', {
  538. isNativeTag: tag => tag === 'div'
  539. })
  540. expect(ast.children[0]).toMatchObject({
  541. type: NodeTypes.ELEMENT,
  542. tag: 'div',
  543. tagType: ElementTypes.ELEMENT
  544. })
  545. expect(ast.children[1]).toMatchObject({
  546. type: NodeTypes.ELEMENT,
  547. tag: 'comp',
  548. tagType: ElementTypes.COMPONENT
  549. })
  550. expect(ast.children[2]).toMatchObject({
  551. type: NodeTypes.ELEMENT,
  552. tag: 'Comp',
  553. tagType: ElementTypes.COMPONENT
  554. })
  555. })
  556. test('native element without `isNativeTag`', () => {
  557. const ast = parse('<div></div><comp></comp><Comp></Comp>')
  558. expect(ast.children[0]).toMatchObject({
  559. type: NodeTypes.ELEMENT,
  560. tag: 'div',
  561. tagType: ElementTypes.ELEMENT
  562. })
  563. expect(ast.children[1]).toMatchObject({
  564. type: NodeTypes.ELEMENT,
  565. tag: 'comp',
  566. tagType: ElementTypes.ELEMENT
  567. })
  568. expect(ast.children[2]).toMatchObject({
  569. type: NodeTypes.ELEMENT,
  570. tag: 'Comp',
  571. tagType: ElementTypes.COMPONENT
  572. })
  573. })
  574. test('custom element', () => {
  575. const ast = parse('<div></div><comp></comp>', {
  576. isNativeTag: tag => tag === 'div',
  577. isCustomElement: tag => tag === 'comp'
  578. })
  579. expect(ast.children[0]).toMatchObject({
  580. type: NodeTypes.ELEMENT,
  581. tag: 'div',
  582. tagType: ElementTypes.ELEMENT
  583. })
  584. expect(ast.children[1]).toMatchObject({
  585. type: NodeTypes.ELEMENT,
  586. tag: 'comp',
  587. tagType: ElementTypes.ELEMENT
  588. })
  589. })
  590. test('attribute with no value', () => {
  591. const ast = parse('<div id></div>')
  592. const element = ast.children[0] as ElementNode
  593. expect(element).toStrictEqual({
  594. type: NodeTypes.ELEMENT,
  595. ns: Namespaces.HTML,
  596. tag: 'div',
  597. tagType: ElementTypes.ELEMENT,
  598. codegenNode: undefined,
  599. props: [
  600. {
  601. type: NodeTypes.ATTRIBUTE,
  602. name: 'id',
  603. value: undefined,
  604. loc: {
  605. start: { offset: 5, line: 1, column: 6 },
  606. end: { offset: 7, line: 1, column: 8 },
  607. source: 'id'
  608. }
  609. }
  610. ],
  611. isSelfClosing: false,
  612. children: [],
  613. loc: {
  614. start: { offset: 0, line: 1, column: 1 },
  615. end: { offset: 14, line: 1, column: 15 },
  616. source: '<div id></div>'
  617. }
  618. })
  619. })
  620. test('attribute with empty value, double quote', () => {
  621. const ast = parse('<div id=""></div>')
  622. const element = ast.children[0] as ElementNode
  623. expect(element).toStrictEqual({
  624. type: NodeTypes.ELEMENT,
  625. ns: Namespaces.HTML,
  626. tag: 'div',
  627. tagType: ElementTypes.ELEMENT,
  628. codegenNode: undefined,
  629. props: [
  630. {
  631. type: NodeTypes.ATTRIBUTE,
  632. name: 'id',
  633. value: {
  634. type: NodeTypes.TEXT,
  635. content: '',
  636. loc: {
  637. start: { offset: 8, line: 1, column: 9 },
  638. end: { offset: 10, line: 1, column: 11 },
  639. source: '""'
  640. }
  641. },
  642. loc: {
  643. start: { offset: 5, line: 1, column: 6 },
  644. end: { offset: 10, line: 1, column: 11 },
  645. source: 'id=""'
  646. }
  647. }
  648. ],
  649. isSelfClosing: false,
  650. children: [],
  651. loc: {
  652. start: { offset: 0, line: 1, column: 1 },
  653. end: { offset: 17, line: 1, column: 18 },
  654. source: '<div id=""></div>'
  655. }
  656. })
  657. })
  658. test('attribute with empty value, single quote', () => {
  659. const ast = parse("<div id=''></div>")
  660. const element = ast.children[0] as ElementNode
  661. expect(element).toStrictEqual({
  662. type: NodeTypes.ELEMENT,
  663. ns: Namespaces.HTML,
  664. tag: 'div',
  665. tagType: ElementTypes.ELEMENT,
  666. codegenNode: undefined,
  667. props: [
  668. {
  669. type: NodeTypes.ATTRIBUTE,
  670. name: 'id',
  671. value: {
  672. type: NodeTypes.TEXT,
  673. content: '',
  674. loc: {
  675. start: { offset: 8, line: 1, column: 9 },
  676. end: { offset: 10, line: 1, column: 11 },
  677. source: "''"
  678. }
  679. },
  680. loc: {
  681. start: { offset: 5, line: 1, column: 6 },
  682. end: { offset: 10, line: 1, column: 11 },
  683. source: "id=''"
  684. }
  685. }
  686. ],
  687. isSelfClosing: false,
  688. children: [],
  689. loc: {
  690. start: { offset: 0, line: 1, column: 1 },
  691. end: { offset: 17, line: 1, column: 18 },
  692. source: "<div id=''></div>"
  693. }
  694. })
  695. })
  696. test('attribute with value, double quote', () => {
  697. const ast = parse('<div id=">\'"></div>')
  698. const element = ast.children[0] as ElementNode
  699. expect(element).toStrictEqual({
  700. type: NodeTypes.ELEMENT,
  701. ns: Namespaces.HTML,
  702. tag: 'div',
  703. tagType: ElementTypes.ELEMENT,
  704. codegenNode: undefined,
  705. props: [
  706. {
  707. type: NodeTypes.ATTRIBUTE,
  708. name: 'id',
  709. value: {
  710. type: NodeTypes.TEXT,
  711. content: ">'",
  712. loc: {
  713. start: { offset: 8, line: 1, column: 9 },
  714. end: { offset: 12, line: 1, column: 13 },
  715. source: '">\'"'
  716. }
  717. },
  718. loc: {
  719. start: { offset: 5, line: 1, column: 6 },
  720. end: { offset: 12, line: 1, column: 13 },
  721. source: 'id=">\'"'
  722. }
  723. }
  724. ],
  725. isSelfClosing: false,
  726. children: [],
  727. loc: {
  728. start: { offset: 0, line: 1, column: 1 },
  729. end: { offset: 19, line: 1, column: 20 },
  730. source: '<div id=">\'"></div>'
  731. }
  732. })
  733. })
  734. test('attribute with value, single quote', () => {
  735. const ast = parse("<div id='>\"'></div>")
  736. const element = ast.children[0] as ElementNode
  737. expect(element).toStrictEqual({
  738. type: NodeTypes.ELEMENT,
  739. ns: Namespaces.HTML,
  740. tag: 'div',
  741. tagType: ElementTypes.ELEMENT,
  742. codegenNode: undefined,
  743. props: [
  744. {
  745. type: NodeTypes.ATTRIBUTE,
  746. name: 'id',
  747. value: {
  748. type: NodeTypes.TEXT,
  749. content: '>"',
  750. loc: {
  751. start: { offset: 8, line: 1, column: 9 },
  752. end: { offset: 12, line: 1, column: 13 },
  753. source: "'>\"'"
  754. }
  755. },
  756. loc: {
  757. start: { offset: 5, line: 1, column: 6 },
  758. end: { offset: 12, line: 1, column: 13 },
  759. source: "id='>\"'"
  760. }
  761. }
  762. ],
  763. isSelfClosing: false,
  764. children: [],
  765. loc: {
  766. start: { offset: 0, line: 1, column: 1 },
  767. end: { offset: 19, line: 1, column: 20 },
  768. source: "<div id='>\"'></div>"
  769. }
  770. })
  771. })
  772. test('attribute with value, unquoted', () => {
  773. const ast = parse('<div id=a/></div>')
  774. const element = ast.children[0] as ElementNode
  775. expect(element).toStrictEqual({
  776. type: NodeTypes.ELEMENT,
  777. ns: Namespaces.HTML,
  778. tag: 'div',
  779. tagType: ElementTypes.ELEMENT,
  780. codegenNode: undefined,
  781. props: [
  782. {
  783. type: NodeTypes.ATTRIBUTE,
  784. name: 'id',
  785. value: {
  786. type: NodeTypes.TEXT,
  787. content: 'a/',
  788. loc: {
  789. start: { offset: 8, line: 1, column: 9 },
  790. end: { offset: 10, line: 1, column: 11 },
  791. source: 'a/'
  792. }
  793. },
  794. loc: {
  795. start: { offset: 5, line: 1, column: 6 },
  796. end: { offset: 10, line: 1, column: 11 },
  797. source: 'id=a/'
  798. }
  799. }
  800. ],
  801. isSelfClosing: false,
  802. children: [],
  803. loc: {
  804. start: { offset: 0, line: 1, column: 1 },
  805. end: { offset: 17, line: 1, column: 18 },
  806. source: '<div id=a/></div>'
  807. }
  808. })
  809. })
  810. test('multiple attributes', () => {
  811. const ast = parse('<div id=a class="c" inert style=\'\'></div>')
  812. const element = ast.children[0] as ElementNode
  813. expect(element).toStrictEqual({
  814. type: NodeTypes.ELEMENT,
  815. ns: Namespaces.HTML,
  816. tag: 'div',
  817. tagType: ElementTypes.ELEMENT,
  818. codegenNode: undefined,
  819. props: [
  820. {
  821. type: NodeTypes.ATTRIBUTE,
  822. name: 'id',
  823. value: {
  824. type: NodeTypes.TEXT,
  825. content: 'a',
  826. loc: {
  827. start: { offset: 8, line: 1, column: 9 },
  828. end: { offset: 9, line: 1, column: 10 },
  829. source: 'a'
  830. }
  831. },
  832. loc: {
  833. start: { offset: 5, line: 1, column: 6 },
  834. end: { offset: 9, line: 1, column: 10 },
  835. source: 'id=a'
  836. }
  837. },
  838. {
  839. type: NodeTypes.ATTRIBUTE,
  840. name: 'class',
  841. value: {
  842. type: NodeTypes.TEXT,
  843. content: 'c',
  844. loc: {
  845. start: { offset: 16, line: 1, column: 17 },
  846. end: { offset: 19, line: 1, column: 20 },
  847. source: '"c"'
  848. }
  849. },
  850. loc: {
  851. start: { offset: 10, line: 1, column: 11 },
  852. end: { offset: 19, line: 1, column: 20 },
  853. source: 'class="c"'
  854. }
  855. },
  856. {
  857. type: NodeTypes.ATTRIBUTE,
  858. name: 'inert',
  859. value: undefined,
  860. loc: {
  861. start: { offset: 20, line: 1, column: 21 },
  862. end: { offset: 25, line: 1, column: 26 },
  863. source: 'inert'
  864. }
  865. },
  866. {
  867. type: NodeTypes.ATTRIBUTE,
  868. name: 'style',
  869. value: {
  870. type: NodeTypes.TEXT,
  871. content: '',
  872. loc: {
  873. start: { offset: 32, line: 1, column: 33 },
  874. end: { offset: 34, line: 1, column: 35 },
  875. source: "''"
  876. }
  877. },
  878. loc: {
  879. start: { offset: 26, line: 1, column: 27 },
  880. end: { offset: 34, line: 1, column: 35 },
  881. source: "style=''"
  882. }
  883. }
  884. ],
  885. isSelfClosing: false,
  886. children: [],
  887. loc: {
  888. start: { offset: 0, line: 1, column: 1 },
  889. end: { offset: 41, line: 1, column: 42 },
  890. source: '<div id=a class="c" inert style=\'\'></div>'
  891. }
  892. })
  893. })
  894. test('directive with no value', () => {
  895. const ast = parse('<div v-if/>')
  896. const directive = (ast.children[0] as ElementNode).props[0]
  897. expect(directive).toStrictEqual({
  898. type: NodeTypes.DIRECTIVE,
  899. name: 'if',
  900. arg: undefined,
  901. modifiers: [],
  902. exp: undefined,
  903. loc: {
  904. start: { offset: 5, line: 1, column: 6 },
  905. end: { offset: 9, line: 1, column: 10 },
  906. source: 'v-if'
  907. }
  908. })
  909. })
  910. test('directive with value', () => {
  911. const ast = parse('<div v-if="a"/>')
  912. const directive = (ast.children[0] as ElementNode).props[0]
  913. expect(directive).toStrictEqual({
  914. type: NodeTypes.DIRECTIVE,
  915. name: 'if',
  916. arg: undefined,
  917. modifiers: [],
  918. exp: {
  919. type: NodeTypes.SIMPLE_EXPRESSION,
  920. content: 'a',
  921. isStatic: false,
  922. isConstant: false,
  923. loc: {
  924. start: { offset: 11, line: 1, column: 12 },
  925. end: { offset: 12, line: 1, column: 13 },
  926. source: 'a'
  927. }
  928. },
  929. loc: {
  930. start: { offset: 5, line: 1, column: 6 },
  931. end: { offset: 13, line: 1, column: 14 },
  932. source: 'v-if="a"'
  933. }
  934. })
  935. })
  936. test('directive with argument', () => {
  937. const ast = parse('<div v-on:click/>')
  938. const directive = (ast.children[0] as ElementNode).props[0]
  939. expect(directive).toStrictEqual({
  940. type: NodeTypes.DIRECTIVE,
  941. name: 'on',
  942. arg: {
  943. type: NodeTypes.SIMPLE_EXPRESSION,
  944. content: 'click',
  945. isStatic: true,
  946. isConstant: true,
  947. loc: {
  948. source: 'click',
  949. start: {
  950. column: 11,
  951. line: 1,
  952. offset: 10
  953. },
  954. end: {
  955. column: 16,
  956. line: 1,
  957. offset: 15
  958. }
  959. }
  960. },
  961. modifiers: [],
  962. exp: undefined,
  963. loc: {
  964. start: { offset: 5, line: 1, column: 6 },
  965. end: { offset: 15, line: 1, column: 16 },
  966. source: 'v-on:click'
  967. }
  968. })
  969. })
  970. test('directive with a modifier', () => {
  971. const ast = parse('<div v-on.enter/>')
  972. const directive = (ast.children[0] as ElementNode).props[0]
  973. expect(directive).toStrictEqual({
  974. type: NodeTypes.DIRECTIVE,
  975. name: 'on',
  976. arg: undefined,
  977. modifiers: ['enter'],
  978. exp: undefined,
  979. loc: {
  980. start: { offset: 5, line: 1, column: 6 },
  981. end: { offset: 15, line: 1, column: 16 },
  982. source: 'v-on.enter'
  983. }
  984. })
  985. })
  986. test('directive with two modifiers', () => {
  987. const ast = parse('<div v-on.enter.exact/>')
  988. const directive = (ast.children[0] as ElementNode).props[0]
  989. expect(directive).toStrictEqual({
  990. type: NodeTypes.DIRECTIVE,
  991. name: 'on',
  992. arg: undefined,
  993. modifiers: ['enter', 'exact'],
  994. exp: undefined,
  995. loc: {
  996. start: { offset: 5, line: 1, column: 6 },
  997. end: { offset: 21, line: 1, column: 22 },
  998. source: 'v-on.enter.exact'
  999. }
  1000. })
  1001. })
  1002. test('directive with argument and modifiers', () => {
  1003. const ast = parse('<div v-on:click.enter.exact/>')
  1004. const directive = (ast.children[0] as ElementNode).props[0]
  1005. expect(directive).toStrictEqual({
  1006. type: NodeTypes.DIRECTIVE,
  1007. name: 'on',
  1008. arg: {
  1009. type: NodeTypes.SIMPLE_EXPRESSION,
  1010. content: 'click',
  1011. isStatic: true,
  1012. isConstant: true,
  1013. loc: {
  1014. source: 'click',
  1015. start: {
  1016. column: 11,
  1017. line: 1,
  1018. offset: 10
  1019. },
  1020. end: {
  1021. column: 16,
  1022. line: 1,
  1023. offset: 15
  1024. }
  1025. }
  1026. },
  1027. modifiers: ['enter', 'exact'],
  1028. exp: undefined,
  1029. loc: {
  1030. start: { offset: 5, line: 1, column: 6 },
  1031. end: { offset: 27, line: 1, column: 28 },
  1032. source: 'v-on:click.enter.exact'
  1033. }
  1034. })
  1035. })
  1036. test('v-bind shorthand', () => {
  1037. const ast = parse('<div :a=b />')
  1038. const directive = (ast.children[0] as ElementNode).props[0]
  1039. expect(directive).toStrictEqual({
  1040. type: NodeTypes.DIRECTIVE,
  1041. name: 'bind',
  1042. arg: {
  1043. type: NodeTypes.SIMPLE_EXPRESSION,
  1044. content: 'a',
  1045. isStatic: true,
  1046. isConstant: true,
  1047. loc: {
  1048. source: 'a',
  1049. start: {
  1050. column: 7,
  1051. line: 1,
  1052. offset: 6
  1053. },
  1054. end: {
  1055. column: 8,
  1056. line: 1,
  1057. offset: 7
  1058. }
  1059. }
  1060. },
  1061. modifiers: [],
  1062. exp: {
  1063. type: NodeTypes.SIMPLE_EXPRESSION,
  1064. content: 'b',
  1065. isStatic: false,
  1066. isConstant: false,
  1067. loc: {
  1068. start: { offset: 8, line: 1, column: 9 },
  1069. end: { offset: 9, line: 1, column: 10 },
  1070. source: 'b'
  1071. }
  1072. },
  1073. loc: {
  1074. start: { offset: 5, line: 1, column: 6 },
  1075. end: { offset: 9, line: 1, column: 10 },
  1076. source: ':a=b'
  1077. }
  1078. })
  1079. })
  1080. test('v-bind shorthand with modifier', () => {
  1081. const ast = parse('<div :a.sync=b />')
  1082. const directive = (ast.children[0] as ElementNode).props[0]
  1083. expect(directive).toStrictEqual({
  1084. type: NodeTypes.DIRECTIVE,
  1085. name: 'bind',
  1086. arg: {
  1087. type: NodeTypes.SIMPLE_EXPRESSION,
  1088. content: 'a',
  1089. isStatic: true,
  1090. isConstant: true,
  1091. loc: {
  1092. source: 'a',
  1093. start: {
  1094. column: 7,
  1095. line: 1,
  1096. offset: 6
  1097. },
  1098. end: {
  1099. column: 8,
  1100. line: 1,
  1101. offset: 7
  1102. }
  1103. }
  1104. },
  1105. modifiers: ['sync'],
  1106. exp: {
  1107. type: NodeTypes.SIMPLE_EXPRESSION,
  1108. content: 'b',
  1109. isStatic: false,
  1110. isConstant: false,
  1111. loc: {
  1112. start: { offset: 13, line: 1, column: 14 },
  1113. end: { offset: 14, line: 1, column: 15 },
  1114. source: 'b'
  1115. }
  1116. },
  1117. loc: {
  1118. start: { offset: 5, line: 1, column: 6 },
  1119. end: { offset: 14, line: 1, column: 15 },
  1120. source: ':a.sync=b'
  1121. }
  1122. })
  1123. })
  1124. test('v-on shorthand', () => {
  1125. const ast = parse('<div @a=b />')
  1126. const directive = (ast.children[0] as ElementNode).props[0]
  1127. expect(directive).toStrictEqual({
  1128. type: NodeTypes.DIRECTIVE,
  1129. name: 'on',
  1130. arg: {
  1131. type: NodeTypes.SIMPLE_EXPRESSION,
  1132. content: 'a',
  1133. isStatic: true,
  1134. isConstant: true,
  1135. loc: {
  1136. source: 'a',
  1137. start: {
  1138. column: 7,
  1139. line: 1,
  1140. offset: 6
  1141. },
  1142. end: {
  1143. column: 8,
  1144. line: 1,
  1145. offset: 7
  1146. }
  1147. }
  1148. },
  1149. modifiers: [],
  1150. exp: {
  1151. type: NodeTypes.SIMPLE_EXPRESSION,
  1152. content: 'b',
  1153. isStatic: false,
  1154. isConstant: false,
  1155. loc: {
  1156. start: { offset: 8, line: 1, column: 9 },
  1157. end: { offset: 9, line: 1, column: 10 },
  1158. source: 'b'
  1159. }
  1160. },
  1161. loc: {
  1162. start: { offset: 5, line: 1, column: 6 },
  1163. end: { offset: 9, line: 1, column: 10 },
  1164. source: '@a=b'
  1165. }
  1166. })
  1167. })
  1168. test('v-on shorthand with modifier', () => {
  1169. const ast = parse('<div @a.enter=b />')
  1170. const directive = (ast.children[0] as ElementNode).props[0]
  1171. expect(directive).toStrictEqual({
  1172. type: NodeTypes.DIRECTIVE,
  1173. name: 'on',
  1174. arg: {
  1175. type: NodeTypes.SIMPLE_EXPRESSION,
  1176. content: 'a',
  1177. isStatic: true,
  1178. isConstant: true,
  1179. loc: {
  1180. source: 'a',
  1181. start: {
  1182. column: 7,
  1183. line: 1,
  1184. offset: 6
  1185. },
  1186. end: {
  1187. column: 8,
  1188. line: 1,
  1189. offset: 7
  1190. }
  1191. }
  1192. },
  1193. modifiers: ['enter'],
  1194. exp: {
  1195. type: NodeTypes.SIMPLE_EXPRESSION,
  1196. content: 'b',
  1197. isStatic: false,
  1198. isConstant: false,
  1199. loc: {
  1200. start: { offset: 14, line: 1, column: 15 },
  1201. end: { offset: 15, line: 1, column: 16 },
  1202. source: 'b'
  1203. }
  1204. },
  1205. loc: {
  1206. start: { offset: 5, line: 1, column: 6 },
  1207. end: { offset: 15, line: 1, column: 16 },
  1208. source: '@a.enter=b'
  1209. }
  1210. })
  1211. })
  1212. test('v-slot shorthand', () => {
  1213. const ast = parse('<Comp #a="{ b }" />')
  1214. const directive = (ast.children[0] as ElementNode).props[0]
  1215. expect(directive).toStrictEqual({
  1216. type: NodeTypes.DIRECTIVE,
  1217. name: 'slot',
  1218. arg: {
  1219. type: NodeTypes.SIMPLE_EXPRESSION,
  1220. content: 'a',
  1221. isStatic: true,
  1222. isConstant: true,
  1223. loc: {
  1224. source: 'a',
  1225. start: {
  1226. column: 8,
  1227. line: 1,
  1228. offset: 7
  1229. },
  1230. end: {
  1231. column: 9,
  1232. line: 1,
  1233. offset: 8
  1234. }
  1235. }
  1236. },
  1237. modifiers: [],
  1238. exp: {
  1239. type: NodeTypes.SIMPLE_EXPRESSION,
  1240. content: '{ b }',
  1241. isStatic: false,
  1242. // The `isConstant` is the default value and will be determined in transformExpression
  1243. isConstant: false,
  1244. loc: {
  1245. start: { offset: 10, line: 1, column: 11 },
  1246. end: { offset: 15, line: 1, column: 16 },
  1247. source: '{ b }'
  1248. }
  1249. },
  1250. loc: {
  1251. start: { offset: 6, line: 1, column: 7 },
  1252. end: { offset: 16, line: 1, column: 17 },
  1253. source: '#a="{ b }"'
  1254. }
  1255. })
  1256. })
  1257. test('v-pre', () => {
  1258. const ast = parse(
  1259. `<div v-pre :id="foo"><Comp/>{{ bar }}</div>\n` +
  1260. `<div :id="foo"><Comp/>{{ bar }}</div>`
  1261. )
  1262. const divWithPre = ast.children[0] as ElementNode
  1263. expect(divWithPre.props).toMatchObject([
  1264. {
  1265. type: NodeTypes.ATTRIBUTE,
  1266. name: `:id`,
  1267. value: {
  1268. type: NodeTypes.TEXT,
  1269. content: `foo`
  1270. },
  1271. loc: {
  1272. source: `:id="foo"`,
  1273. start: {
  1274. line: 1,
  1275. column: 12
  1276. },
  1277. end: {
  1278. line: 1,
  1279. column: 21
  1280. }
  1281. }
  1282. }
  1283. ])
  1284. expect(divWithPre.children[0]).toMatchObject({
  1285. type: NodeTypes.ELEMENT,
  1286. tagType: ElementTypes.ELEMENT,
  1287. tag: `Comp`
  1288. })
  1289. expect(divWithPre.children[1]).toMatchObject({
  1290. type: NodeTypes.TEXT,
  1291. content: `{{ bar }}`
  1292. })
  1293. // should not affect siblings after it
  1294. const divWithoutPre = ast.children[1] as ElementNode
  1295. expect(divWithoutPre.props).toMatchObject([
  1296. {
  1297. type: NodeTypes.DIRECTIVE,
  1298. name: `bind`,
  1299. arg: {
  1300. type: NodeTypes.SIMPLE_EXPRESSION,
  1301. isStatic: true,
  1302. content: `id`
  1303. },
  1304. exp: {
  1305. type: NodeTypes.SIMPLE_EXPRESSION,
  1306. isStatic: false,
  1307. content: `foo`
  1308. },
  1309. loc: {
  1310. source: `:id="foo"`,
  1311. start: {
  1312. line: 2,
  1313. column: 6
  1314. },
  1315. end: {
  1316. line: 2,
  1317. column: 15
  1318. }
  1319. }
  1320. }
  1321. ])
  1322. expect(divWithoutPre.children[0]).toMatchObject({
  1323. type: NodeTypes.ELEMENT,
  1324. tagType: ElementTypes.COMPONENT,
  1325. tag: `Comp`
  1326. })
  1327. expect(divWithoutPre.children[1]).toMatchObject({
  1328. type: NodeTypes.INTERPOLATION,
  1329. content: {
  1330. type: NodeTypes.SIMPLE_EXPRESSION,
  1331. content: `bar`,
  1332. isStatic: false
  1333. }
  1334. })
  1335. })
  1336. test('end tags are case-insensitive.', () => {
  1337. const ast = parse('<div>hello</DIV>after')
  1338. const element = ast.children[0] as ElementNode
  1339. const text = element.children[0] as TextNode
  1340. expect(text).toStrictEqual({
  1341. type: NodeTypes.TEXT,
  1342. content: 'hello',
  1343. loc: {
  1344. start: { offset: 5, line: 1, column: 6 },
  1345. end: { offset: 10, line: 1, column: 11 },
  1346. source: 'hello'
  1347. }
  1348. })
  1349. })
  1350. })
  1351. test('self closing single tag', () => {
  1352. const ast = parse('<div :class="{ some: condition }" />')
  1353. expect(ast.children).toHaveLength(1)
  1354. expect(ast.children[0]).toMatchObject({ tag: 'div' })
  1355. })
  1356. test('self closing multiple tag', () => {
  1357. const ast = parse(
  1358. `<div :class="{ some: condition }" />\n` +
  1359. `<p v-bind:style="{ color: 'red' }"/>`
  1360. )
  1361. expect(ast).toMatchSnapshot()
  1362. expect(ast.children).toHaveLength(2)
  1363. expect(ast.children[0]).toMatchObject({ tag: 'div' })
  1364. expect(ast.children[1]).toMatchObject({ tag: 'p' })
  1365. })
  1366. test('valid html', () => {
  1367. const ast = parse(
  1368. `<div :class="{ some: condition }">\n` +
  1369. ` <p v-bind:style="{ color: 'red' }"/>\n` +
  1370. ` <!-- a comment with <html> inside it -->\n` +
  1371. `</div>`
  1372. )
  1373. expect(ast).toMatchSnapshot()
  1374. expect(ast.children).toHaveLength(1)
  1375. const el = ast.children[0] as any
  1376. expect(el).toMatchObject({
  1377. tag: 'div'
  1378. })
  1379. expect(el.children).toHaveLength(2)
  1380. expect(el.children[0]).toMatchObject({
  1381. tag: 'p'
  1382. })
  1383. expect(el.children[1]).toMatchObject({
  1384. type: NodeTypes.COMMENT
  1385. })
  1386. })
  1387. test('invalid html', () => {
  1388. expect(() => {
  1389. parse(`<div>\n<span>\n</div>\n</span>`)
  1390. }).toThrow('End tag was not found. (3:1)')
  1391. const spy = jest.fn()
  1392. const ast = parse(`<div>\n<span>\n</div>\n</span>`, {
  1393. onError: spy
  1394. })
  1395. expect(spy.mock.calls).toMatchObject([
  1396. [
  1397. {
  1398. code: ErrorCodes.X_MISSING_END_TAG,
  1399. loc: {
  1400. start: {
  1401. offset: 13,
  1402. line: 3,
  1403. column: 1
  1404. }
  1405. }
  1406. }
  1407. ],
  1408. [
  1409. {
  1410. code: ErrorCodes.X_INVALID_END_TAG,
  1411. loc: {
  1412. start: {
  1413. offset: 20,
  1414. line: 4,
  1415. column: 1
  1416. }
  1417. }
  1418. }
  1419. ]
  1420. ])
  1421. expect(ast).toMatchSnapshot()
  1422. })
  1423. test('parse with correct location info', () => {
  1424. const [foo, bar, but, baz] = parse(
  1425. `
  1426. foo
  1427. is {{ bar }} but {{ baz }}`.trim()
  1428. ).children
  1429. let offset = 0
  1430. expect(foo.loc.start).toEqual({ line: 1, column: 1, offset })
  1431. offset += foo.loc.source.length
  1432. expect(foo.loc.end).toEqual({ line: 2, column: 5, offset })
  1433. expect(bar.loc.start).toEqual({ line: 2, column: 5, offset })
  1434. const barInner = (bar as InterpolationNode).content
  1435. offset += 3
  1436. expect(barInner.loc.start).toEqual({ line: 2, column: 8, offset })
  1437. offset += barInner.loc.source.length
  1438. expect(barInner.loc.end).toEqual({ line: 2, column: 11, offset })
  1439. offset += 3
  1440. expect(bar.loc.end).toEqual({ line: 2, column: 14, offset })
  1441. expect(but.loc.start).toEqual({ line: 2, column: 14, offset })
  1442. offset += but.loc.source.length
  1443. expect(but.loc.end).toEqual({ line: 2, column: 19, offset })
  1444. expect(baz.loc.start).toEqual({ line: 2, column: 19, offset })
  1445. const bazInner = (baz as InterpolationNode).content
  1446. offset += 3
  1447. expect(bazInner.loc.start).toEqual({ line: 2, column: 22, offset })
  1448. offset += bazInner.loc.source.length
  1449. expect(bazInner.loc.end).toEqual({ line: 2, column: 25, offset })
  1450. offset += 3
  1451. expect(baz.loc.end).toEqual({ line: 2, column: 28, offset })
  1452. })
  1453. describe('namedCharacterReferences option', () => {
  1454. test('use the given map', () => {
  1455. const ast: any = parse('&amp;&cups;', {
  1456. namedCharacterReferences: {
  1457. 'cups;': '\u222A\uFE00' // UNION with serifs
  1458. },
  1459. onError: () => {} // Ignore errors
  1460. })
  1461. expect(ast.children.length).toBe(1)
  1462. expect(ast.children[0].type).toBe(NodeTypes.TEXT)
  1463. expect(ast.children[0].content).toBe('&amp;\u222A\uFE00')
  1464. })
  1465. })
  1466. describe('whitespace management', () => {
  1467. it('should remove whitespaces at start/end inside an element', () => {
  1468. const ast = parse(`<div> <span/> </div>`)
  1469. expect((ast.children[0] as ElementNode).children.length).toBe(1)
  1470. })
  1471. it('should remove whitespaces w/ newline between elements', () => {
  1472. const ast = parse(`<div/> \n <div/> \n <div/>`)
  1473. expect(ast.children.length).toBe(3)
  1474. expect(ast.children.every(c => c.type === NodeTypes.ELEMENT)).toBe(true)
  1475. })
  1476. it('should remove whitespaces adjacent to comments', () => {
  1477. const ast = parse(`<div/> \n <!--foo--> <div/>`)
  1478. expect(ast.children.length).toBe(3)
  1479. expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
  1480. expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
  1481. expect(ast.children[2].type).toBe(NodeTypes.ELEMENT)
  1482. })
  1483. it('should remove whitespaces w/ newline between comments and elements', () => {
  1484. const ast = parse(`<div/> \n <!--foo--> \n <div/>`)
  1485. expect(ast.children.length).toBe(3)
  1486. expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
  1487. expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
  1488. expect(ast.children[2].type).toBe(NodeTypes.ELEMENT)
  1489. })
  1490. it('should NOT remove whitespaces w/ newline between interpolations', () => {
  1491. const ast = parse(`{{ foo }} \n {{ bar }}`)
  1492. expect(ast.children.length).toBe(3)
  1493. expect(ast.children[0].type).toBe(NodeTypes.INTERPOLATION)
  1494. expect(ast.children[1]).toMatchObject({
  1495. type: NodeTypes.TEXT,
  1496. content: ' '
  1497. })
  1498. expect(ast.children[2].type).toBe(NodeTypes.INTERPOLATION)
  1499. })
  1500. it('should NOT remove whitespaces w/o newline between elements', () => {
  1501. const ast = parse(`<div/> <div/> <div/>`)
  1502. expect(ast.children.length).toBe(5)
  1503. expect(ast.children.map(c => c.type)).toMatchObject([
  1504. NodeTypes.ELEMENT,
  1505. NodeTypes.TEXT,
  1506. NodeTypes.ELEMENT,
  1507. NodeTypes.TEXT,
  1508. NodeTypes.ELEMENT
  1509. ])
  1510. })
  1511. it('should condense consecutive whitespaces in text', () => {
  1512. const ast = parse(` foo \n bar baz `)
  1513. expect((ast.children[0] as TextNode).content).toBe(` foo bar baz `)
  1514. })
  1515. })
  1516. describe('Errors', () => {
  1517. const patterns: {
  1518. [key: string]: Array<{
  1519. code: string
  1520. errors: Array<{ type: ErrorCodes; loc: Position }>
  1521. options?: Partial<ParserOptions>
  1522. }>
  1523. } = {
  1524. ABRUPT_CLOSING_OF_EMPTY_COMMENT: [
  1525. {
  1526. code: '<template><!--></template>',
  1527. errors: [
  1528. {
  1529. type: ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT,
  1530. loc: { offset: 10, line: 1, column: 11 }
  1531. }
  1532. ]
  1533. },
  1534. {
  1535. code: '<template><!---></template>',
  1536. errors: [
  1537. {
  1538. type: ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT,
  1539. loc: { offset: 10, line: 1, column: 11 }
  1540. }
  1541. ]
  1542. },
  1543. {
  1544. code: '<template><!----></template>',
  1545. errors: []
  1546. }
  1547. ],
  1548. ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE: [
  1549. {
  1550. code: '<template>&#a;</template>',
  1551. errors: [
  1552. {
  1553. type: ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
  1554. loc: { offset: 10, line: 1, column: 11 }
  1555. }
  1556. ]
  1557. },
  1558. {
  1559. code: '<template>&#xg;</template>',
  1560. errors: [
  1561. {
  1562. type: ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
  1563. loc: { offset: 10, line: 1, column: 11 }
  1564. }
  1565. ]
  1566. },
  1567. {
  1568. code: '<template>&#99;</template>',
  1569. errors: []
  1570. },
  1571. {
  1572. code: '<template>&#xff;</template>',
  1573. errors: []
  1574. },
  1575. {
  1576. code: '<template attr="&#a;"></template>',
  1577. errors: [
  1578. {
  1579. type: ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
  1580. loc: { offset: 16, line: 1, column: 17 }
  1581. }
  1582. ]
  1583. },
  1584. {
  1585. code: '<template attr="&#xg;"></template>',
  1586. errors: [
  1587. {
  1588. type: ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
  1589. loc: { offset: 16, line: 1, column: 17 }
  1590. }
  1591. ]
  1592. },
  1593. {
  1594. code: '<template attr="&#99;"></template>',
  1595. errors: []
  1596. },
  1597. {
  1598. code: '<template attr="&#xff;"></template>',
  1599. errors: []
  1600. }
  1601. ],
  1602. CDATA_IN_HTML_CONTENT: [
  1603. {
  1604. code: '<template><![CDATA[cdata]]></template>',
  1605. errors: [
  1606. {
  1607. type: ErrorCodes.CDATA_IN_HTML_CONTENT,
  1608. loc: { offset: 10, line: 1, column: 11 }
  1609. }
  1610. ]
  1611. },
  1612. {
  1613. code: '<template><svg><![CDATA[cdata]]></svg></template>',
  1614. errors: []
  1615. }
  1616. ],
  1617. CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE: [
  1618. {
  1619. code: '<template>&#1234567;</template>',
  1620. errors: [
  1621. {
  1622. type: ErrorCodes.CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE,
  1623. loc: { offset: 10, line: 1, column: 11 }
  1624. }
  1625. ]
  1626. }
  1627. ],
  1628. CONTROL_CHARACTER_REFERENCE: [
  1629. {
  1630. code: '<template>&#0003;</template>',
  1631. errors: [
  1632. {
  1633. type: ErrorCodes.CONTROL_CHARACTER_REFERENCE,
  1634. loc: { offset: 10, line: 1, column: 11 }
  1635. }
  1636. ]
  1637. },
  1638. {
  1639. code: '<template>&#x7F;</template>',
  1640. errors: [
  1641. {
  1642. type: ErrorCodes.CONTROL_CHARACTER_REFERENCE,
  1643. loc: { offset: 10, line: 1, column: 11 }
  1644. }
  1645. ]
  1646. }
  1647. ],
  1648. DUPLICATE_ATTRIBUTE: [
  1649. {
  1650. code: '<template><div id="" id=""></div></template>',
  1651. errors: [
  1652. {
  1653. type: ErrorCodes.DUPLICATE_ATTRIBUTE,
  1654. loc: { offset: 21, line: 1, column: 22 }
  1655. }
  1656. ]
  1657. }
  1658. ],
  1659. END_TAG_WITH_ATTRIBUTES: [
  1660. {
  1661. code: '<template><div></div id=""></template>',
  1662. errors: [
  1663. {
  1664. type: ErrorCodes.END_TAG_WITH_ATTRIBUTES,
  1665. loc: { offset: 21, line: 1, column: 22 }
  1666. }
  1667. ]
  1668. }
  1669. ],
  1670. END_TAG_WITH_TRAILING_SOLIDUS: [
  1671. {
  1672. code: '<template><div></div/></template>',
  1673. errors: [
  1674. {
  1675. type: ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS,
  1676. loc: { offset: 20, line: 1, column: 21 }
  1677. }
  1678. ]
  1679. }
  1680. ],
  1681. EOF_BEFORE_TAG_NAME: [
  1682. {
  1683. code: '<template><',
  1684. errors: [
  1685. {
  1686. type: ErrorCodes.EOF_BEFORE_TAG_NAME,
  1687. loc: { offset: 11, line: 1, column: 12 }
  1688. },
  1689. {
  1690. type: ErrorCodes.X_MISSING_END_TAG,
  1691. loc: { offset: 11, line: 1, column: 12 }
  1692. }
  1693. ]
  1694. },
  1695. {
  1696. code: '<template></',
  1697. errors: [
  1698. {
  1699. type: ErrorCodes.EOF_BEFORE_TAG_NAME,
  1700. loc: { offset: 12, line: 1, column: 13 }
  1701. },
  1702. {
  1703. type: ErrorCodes.X_MISSING_END_TAG,
  1704. loc: { offset: 12, line: 1, column: 13 }
  1705. }
  1706. ]
  1707. }
  1708. ],
  1709. EOF_IN_CDATA: [
  1710. {
  1711. code: '<template><svg><![CDATA[cdata',
  1712. errors: [
  1713. {
  1714. type: ErrorCodes.EOF_IN_CDATA,
  1715. loc: { offset: 29, line: 1, column: 30 }
  1716. },
  1717. {
  1718. type: ErrorCodes.X_MISSING_END_TAG,
  1719. loc: { offset: 29, line: 1, column: 30 }
  1720. },
  1721. {
  1722. type: ErrorCodes.X_MISSING_END_TAG,
  1723. loc: { offset: 29, line: 1, column: 30 }
  1724. }
  1725. ]
  1726. },
  1727. {
  1728. code: '<template><svg><![CDATA[',
  1729. errors: [
  1730. {
  1731. type: ErrorCodes.EOF_IN_CDATA,
  1732. loc: { offset: 24, line: 1, column: 25 }
  1733. },
  1734. {
  1735. type: ErrorCodes.X_MISSING_END_TAG,
  1736. loc: { offset: 24, line: 1, column: 25 }
  1737. },
  1738. {
  1739. type: ErrorCodes.X_MISSING_END_TAG,
  1740. loc: { offset: 24, line: 1, column: 25 }
  1741. }
  1742. ]
  1743. }
  1744. ],
  1745. EOF_IN_COMMENT: [
  1746. {
  1747. code: '<template><!--comment',
  1748. errors: [
  1749. {
  1750. type: ErrorCodes.EOF_IN_COMMENT,
  1751. loc: { offset: 21, line: 1, column: 22 }
  1752. },
  1753. {
  1754. type: ErrorCodes.X_MISSING_END_TAG,
  1755. loc: { offset: 21, line: 1, column: 22 }
  1756. }
  1757. ]
  1758. },
  1759. {
  1760. code: '<template><!--',
  1761. errors: [
  1762. {
  1763. type: ErrorCodes.EOF_IN_COMMENT,
  1764. loc: { offset: 14, line: 1, column: 15 }
  1765. },
  1766. {
  1767. type: ErrorCodes.X_MISSING_END_TAG,
  1768. loc: { offset: 14, line: 1, column: 15 }
  1769. }
  1770. ]
  1771. },
  1772. // Bogus comments don't throw eof-in-comment error.
  1773. // https://html.spec.whatwg.org/multipage/parsing.html#bogus-comment-state
  1774. {
  1775. code: '<template><!',
  1776. errors: [
  1777. {
  1778. type: ErrorCodes.INCORRECTLY_OPENED_COMMENT,
  1779. loc: { offset: 10, line: 1, column: 11 }
  1780. },
  1781. {
  1782. type: ErrorCodes.X_MISSING_END_TAG,
  1783. loc: { offset: 12, line: 1, column: 13 }
  1784. }
  1785. ]
  1786. },
  1787. {
  1788. code: '<template><!-',
  1789. errors: [
  1790. {
  1791. type: ErrorCodes.INCORRECTLY_OPENED_COMMENT,
  1792. loc: { offset: 10, line: 1, column: 11 }
  1793. },
  1794. {
  1795. type: ErrorCodes.X_MISSING_END_TAG,
  1796. loc: { offset: 13, line: 1, column: 14 }
  1797. }
  1798. ]
  1799. },
  1800. {
  1801. code: '<template><!abc',
  1802. errors: [
  1803. {
  1804. type: ErrorCodes.INCORRECTLY_OPENED_COMMENT,
  1805. loc: { offset: 10, line: 1, column: 11 }
  1806. },
  1807. {
  1808. type: ErrorCodes.X_MISSING_END_TAG,
  1809. loc: { offset: 15, line: 1, column: 16 }
  1810. }
  1811. ]
  1812. }
  1813. ],
  1814. EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT: [
  1815. {
  1816. code: "<script><!--console.log('hello')",
  1817. errors: [
  1818. {
  1819. type: ErrorCodes.X_MISSING_END_TAG,
  1820. loc: { offset: 32, line: 1, column: 33 }
  1821. },
  1822. {
  1823. type: ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT,
  1824. loc: { offset: 32, line: 1, column: 33 }
  1825. }
  1826. ]
  1827. },
  1828. {
  1829. code: "<script>console.log('hello')",
  1830. errors: [
  1831. {
  1832. type: ErrorCodes.X_MISSING_END_TAG,
  1833. loc: { offset: 28, line: 1, column: 29 }
  1834. }
  1835. ]
  1836. }
  1837. ],
  1838. EOF_IN_TAG: [
  1839. {
  1840. code: '<template><div',
  1841. errors: [
  1842. {
  1843. type: ErrorCodes.EOF_IN_TAG,
  1844. loc: { offset: 14, line: 1, column: 15 }
  1845. },
  1846. {
  1847. type: ErrorCodes.X_MISSING_END_TAG,
  1848. loc: { offset: 14, line: 1, column: 15 }
  1849. },
  1850. {
  1851. type: ErrorCodes.X_MISSING_END_TAG,
  1852. loc: { offset: 14, line: 1, column: 15 }
  1853. }
  1854. ]
  1855. },
  1856. {
  1857. code: '<template><div ',
  1858. errors: [
  1859. {
  1860. type: ErrorCodes.EOF_IN_TAG,
  1861. loc: { offset: 15, line: 1, column: 16 }
  1862. },
  1863. {
  1864. type: ErrorCodes.X_MISSING_END_TAG,
  1865. loc: { offset: 15, line: 1, column: 16 }
  1866. },
  1867. {
  1868. type: ErrorCodes.X_MISSING_END_TAG,
  1869. loc: { offset: 15, line: 1, column: 16 }
  1870. }
  1871. ]
  1872. },
  1873. {
  1874. code: '<template><div id',
  1875. errors: [
  1876. {
  1877. type: ErrorCodes.EOF_IN_TAG,
  1878. loc: { offset: 17, line: 1, column: 18 }
  1879. },
  1880. {
  1881. type: ErrorCodes.X_MISSING_END_TAG,
  1882. loc: { offset: 17, line: 1, column: 18 }
  1883. },
  1884. {
  1885. type: ErrorCodes.X_MISSING_END_TAG,
  1886. loc: { offset: 17, line: 1, column: 18 }
  1887. }
  1888. ]
  1889. },
  1890. {
  1891. code: '<template><div id ',
  1892. errors: [
  1893. {
  1894. type: ErrorCodes.EOF_IN_TAG,
  1895. loc: { offset: 18, line: 1, column: 19 }
  1896. },
  1897. {
  1898. type: ErrorCodes.X_MISSING_END_TAG,
  1899. loc: { offset: 18, line: 1, column: 19 }
  1900. },
  1901. {
  1902. type: ErrorCodes.X_MISSING_END_TAG,
  1903. loc: { offset: 18, line: 1, column: 19 }
  1904. }
  1905. ]
  1906. },
  1907. {
  1908. code: '<template><div id =',
  1909. errors: [
  1910. {
  1911. type: ErrorCodes.MISSING_ATTRIBUTE_VALUE,
  1912. loc: { offset: 19, line: 1, column: 20 }
  1913. },
  1914. {
  1915. type: ErrorCodes.EOF_IN_TAG,
  1916. loc: { offset: 19, line: 1, column: 20 }
  1917. },
  1918. {
  1919. type: ErrorCodes.X_MISSING_END_TAG,
  1920. loc: { offset: 19, line: 1, column: 20 }
  1921. },
  1922. {
  1923. type: ErrorCodes.X_MISSING_END_TAG,
  1924. loc: { offset: 19, line: 1, column: 20 }
  1925. }
  1926. ]
  1927. },
  1928. {
  1929. code: "<template><div id='abc",
  1930. errors: [
  1931. {
  1932. type: ErrorCodes.EOF_IN_TAG,
  1933. loc: { offset: 22, line: 1, column: 23 }
  1934. },
  1935. {
  1936. type: ErrorCodes.X_MISSING_END_TAG,
  1937. loc: { offset: 22, line: 1, column: 23 }
  1938. },
  1939. {
  1940. type: ErrorCodes.X_MISSING_END_TAG,
  1941. loc: { offset: 22, line: 1, column: 23 }
  1942. }
  1943. ]
  1944. },
  1945. {
  1946. code: '<template><div id="abc',
  1947. errors: [
  1948. {
  1949. type: ErrorCodes.EOF_IN_TAG,
  1950. loc: { offset: 22, line: 1, column: 23 }
  1951. },
  1952. {
  1953. type: ErrorCodes.X_MISSING_END_TAG,
  1954. loc: { offset: 22, line: 1, column: 23 }
  1955. },
  1956. {
  1957. type: ErrorCodes.X_MISSING_END_TAG,
  1958. loc: { offset: 22, line: 1, column: 23 }
  1959. }
  1960. ]
  1961. },
  1962. {
  1963. code: "<template><div id='abc'",
  1964. errors: [
  1965. {
  1966. type: ErrorCodes.EOF_IN_TAG,
  1967. loc: { offset: 23, line: 1, column: 24 }
  1968. },
  1969. {
  1970. type: ErrorCodes.X_MISSING_END_TAG,
  1971. loc: { offset: 23, line: 1, column: 24 }
  1972. },
  1973. {
  1974. type: ErrorCodes.X_MISSING_END_TAG,
  1975. loc: { offset: 23, line: 1, column: 24 }
  1976. }
  1977. ]
  1978. },
  1979. {
  1980. code: '<template><div id="abc"',
  1981. errors: [
  1982. {
  1983. type: ErrorCodes.EOF_IN_TAG,
  1984. loc: { offset: 23, line: 1, column: 24 }
  1985. },
  1986. {
  1987. type: ErrorCodes.X_MISSING_END_TAG,
  1988. loc: { offset: 23, line: 1, column: 24 }
  1989. },
  1990. {
  1991. type: ErrorCodes.X_MISSING_END_TAG,
  1992. loc: { offset: 23, line: 1, column: 24 }
  1993. }
  1994. ]
  1995. },
  1996. {
  1997. code: '<template><div id=abc',
  1998. errors: [
  1999. {
  2000. type: ErrorCodes.EOF_IN_TAG,
  2001. loc: { offset: 21, line: 1, column: 22 }
  2002. },
  2003. {
  2004. type: ErrorCodes.X_MISSING_END_TAG,
  2005. loc: { offset: 21, line: 1, column: 22 }
  2006. },
  2007. {
  2008. type: ErrorCodes.X_MISSING_END_TAG,
  2009. loc: { offset: 21, line: 1, column: 22 }
  2010. }
  2011. ]
  2012. },
  2013. {
  2014. code: "<template><div id='abc'/",
  2015. errors: [
  2016. {
  2017. type: ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG,
  2018. loc: { offset: 23, line: 1, column: 24 }
  2019. },
  2020. {
  2021. type: ErrorCodes.EOF_IN_TAG,
  2022. loc: { offset: 24, line: 1, column: 25 }
  2023. },
  2024. {
  2025. type: ErrorCodes.X_MISSING_END_TAG,
  2026. loc: { offset: 24, line: 1, column: 25 }
  2027. },
  2028. {
  2029. type: ErrorCodes.X_MISSING_END_TAG,
  2030. loc: { offset: 24, line: 1, column: 25 }
  2031. }
  2032. ]
  2033. },
  2034. {
  2035. code: '<template><div id="abc"/',
  2036. errors: [
  2037. {
  2038. type: ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG,
  2039. loc: { offset: 23, line: 1, column: 24 }
  2040. },
  2041. {
  2042. type: ErrorCodes.EOF_IN_TAG,
  2043. loc: { offset: 24, line: 1, column: 25 }
  2044. },
  2045. {
  2046. type: ErrorCodes.X_MISSING_END_TAG,
  2047. loc: { offset: 24, line: 1, column: 25 }
  2048. },
  2049. {
  2050. type: ErrorCodes.X_MISSING_END_TAG,
  2051. loc: { offset: 24, line: 1, column: 25 }
  2052. }
  2053. ]
  2054. },
  2055. {
  2056. code: '<template><div id=abc /',
  2057. errors: [
  2058. {
  2059. type: ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG,
  2060. loc: { offset: 22, line: 1, column: 23 }
  2061. },
  2062. {
  2063. type: ErrorCodes.EOF_IN_TAG,
  2064. loc: { offset: 23, line: 1, column: 24 }
  2065. },
  2066. {
  2067. type: ErrorCodes.X_MISSING_END_TAG,
  2068. loc: { offset: 23, line: 1, column: 24 }
  2069. },
  2070. {
  2071. type: ErrorCodes.X_MISSING_END_TAG,
  2072. loc: { offset: 23, line: 1, column: 24 }
  2073. }
  2074. ]
  2075. }
  2076. ],
  2077. INCORRECTLY_CLOSED_COMMENT: [
  2078. {
  2079. code: '<template><!--comment--!></template>',
  2080. errors: [
  2081. {
  2082. type: ErrorCodes.INCORRECTLY_CLOSED_COMMENT,
  2083. loc: { offset: 10, line: 1, column: 11 }
  2084. }
  2085. ]
  2086. }
  2087. ],
  2088. INCORRECTLY_OPENED_COMMENT: [
  2089. {
  2090. code: '<template><!></template>',
  2091. errors: [
  2092. {
  2093. type: ErrorCodes.INCORRECTLY_OPENED_COMMENT,
  2094. loc: { offset: 10, line: 1, column: 11 }
  2095. }
  2096. ]
  2097. },
  2098. {
  2099. code: '<template><!-></template>',
  2100. errors: [
  2101. {
  2102. type: ErrorCodes.INCORRECTLY_OPENED_COMMENT,
  2103. loc: { offset: 10, line: 1, column: 11 }
  2104. }
  2105. ]
  2106. },
  2107. {
  2108. code: '<template><!ELEMENT br EMPTY></template>',
  2109. errors: [
  2110. {
  2111. type: ErrorCodes.INCORRECTLY_OPENED_COMMENT,
  2112. loc: { offset: 10, line: 1, column: 11 }
  2113. }
  2114. ]
  2115. },
  2116. // Just ignore doctype.
  2117. {
  2118. code: '<!DOCTYPE html>',
  2119. errors: []
  2120. }
  2121. ],
  2122. INVALID_FIRST_CHARACTER_OF_TAG_NAME: [
  2123. {
  2124. code: '<template>a < b</template>',
  2125. errors: [
  2126. {
  2127. type: ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
  2128. loc: { offset: 13, line: 1, column: 14 }
  2129. }
  2130. ]
  2131. },
  2132. {
  2133. code: '<template><�></template>',
  2134. errors: [
  2135. {
  2136. type: ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
  2137. loc: { offset: 11, line: 1, column: 12 }
  2138. }
  2139. ]
  2140. },
  2141. {
  2142. code: '<template>a </ b</template>',
  2143. errors: [
  2144. {
  2145. type: ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
  2146. loc: { offset: 14, line: 1, column: 15 }
  2147. },
  2148. {
  2149. type: ErrorCodes.X_MISSING_END_TAG,
  2150. loc: { offset: 27, line: 1, column: 28 }
  2151. }
  2152. ]
  2153. },
  2154. {
  2155. code: '<template></�></template>',
  2156. errors: [
  2157. {
  2158. type: ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
  2159. loc: { offset: 12, line: 1, column: 13 }
  2160. }
  2161. ]
  2162. },
  2163. // Don't throw invalid-first-character-of-tag-name in interpolation
  2164. {
  2165. code: '<template>{{a < b}}</template>',
  2166. errors: []
  2167. }
  2168. ],
  2169. MISSING_ATTRIBUTE_VALUE: [
  2170. {
  2171. code: '<template><div id=></div></template>',
  2172. errors: [
  2173. {
  2174. type: ErrorCodes.MISSING_ATTRIBUTE_VALUE,
  2175. loc: { offset: 18, line: 1, column: 19 }
  2176. }
  2177. ]
  2178. },
  2179. {
  2180. code: '<template><div id= ></div></template>',
  2181. errors: [
  2182. {
  2183. type: ErrorCodes.MISSING_ATTRIBUTE_VALUE,
  2184. loc: { offset: 19, line: 1, column: 20 }
  2185. }
  2186. ]
  2187. },
  2188. {
  2189. code: '<template><div id= /></div></template>',
  2190. errors: []
  2191. }
  2192. ],
  2193. MISSING_END_TAG_NAME: [
  2194. {
  2195. code: '<template></></template>',
  2196. errors: [
  2197. {
  2198. type: ErrorCodes.MISSING_END_TAG_NAME,
  2199. loc: { offset: 12, line: 1, column: 13 }
  2200. }
  2201. ]
  2202. }
  2203. ],
  2204. MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE: [
  2205. {
  2206. code: '<template>&amp</template>',
  2207. options: { namedCharacterReferences: { amp: '&' } },
  2208. errors: [
  2209. {
  2210. type: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
  2211. loc: { offset: 14, line: 1, column: 15 }
  2212. }
  2213. ]
  2214. },
  2215. {
  2216. code: '<template>&#40</template>',
  2217. errors: [
  2218. {
  2219. type: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
  2220. loc: { offset: 14, line: 1, column: 15 }
  2221. }
  2222. ]
  2223. },
  2224. {
  2225. code: '<template>&#x40</template>',
  2226. errors: [
  2227. {
  2228. type: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
  2229. loc: { offset: 15, line: 1, column: 16 }
  2230. }
  2231. ]
  2232. }
  2233. ],
  2234. MISSING_WHITESPACE_BETWEEN_ATTRIBUTES: [
  2235. {
  2236. code: '<template><div id="foo"class="bar"></div></template>',
  2237. errors: [
  2238. {
  2239. type: ErrorCodes.MISSING_WHITESPACE_BETWEEN_ATTRIBUTES,
  2240. loc: { offset: 23, line: 1, column: 24 }
  2241. }
  2242. ]
  2243. },
  2244. // CR doesn't appear in tokenization phase, but all CR are removed in preprocessing.
  2245. // https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream
  2246. {
  2247. code: '<template><div id="foo"\r\nclass="bar"></div></template>',
  2248. errors: []
  2249. }
  2250. ],
  2251. NESTED_COMMENT: [
  2252. {
  2253. code: '<template><!--a<!--b--></template>',
  2254. errors: [
  2255. {
  2256. type: ErrorCodes.NESTED_COMMENT,
  2257. loc: { offset: 15, line: 1, column: 16 }
  2258. }
  2259. ]
  2260. },
  2261. {
  2262. code: '<template><!--a<!--b<!--c--></template>',
  2263. errors: [
  2264. {
  2265. type: ErrorCodes.NESTED_COMMENT,
  2266. loc: { offset: 15, line: 1, column: 16 }
  2267. },
  2268. {
  2269. type: ErrorCodes.NESTED_COMMENT,
  2270. loc: { offset: 20, line: 1, column: 21 }
  2271. }
  2272. ]
  2273. },
  2274. {
  2275. code: '<template><!--a<!--></template>',
  2276. errors: []
  2277. },
  2278. {
  2279. code: '<template><!--a<!--',
  2280. errors: [
  2281. {
  2282. type: ErrorCodes.EOF_IN_COMMENT,
  2283. loc: { offset: 19, line: 1, column: 20 }
  2284. },
  2285. {
  2286. type: ErrorCodes.X_MISSING_END_TAG,
  2287. loc: { offset: 19, line: 1, column: 20 }
  2288. }
  2289. ]
  2290. }
  2291. ],
  2292. NONCHARACTER_CHARACTER_REFERENCE: [
  2293. {
  2294. code: '<template>&#xFFFE;</template>',
  2295. errors: [
  2296. {
  2297. type: ErrorCodes.NONCHARACTER_CHARACTER_REFERENCE,
  2298. loc: { offset: 10, line: 1, column: 11 }
  2299. }
  2300. ]
  2301. },
  2302. {
  2303. code: '<template>&#x1FFFF;</template>',
  2304. errors: [
  2305. {
  2306. type: ErrorCodes.NONCHARACTER_CHARACTER_REFERENCE,
  2307. loc: { offset: 10, line: 1, column: 11 }
  2308. }
  2309. ]
  2310. }
  2311. ],
  2312. NULL_CHARACTER_REFERENCE: [
  2313. {
  2314. code: '<template>&#0000;</template>',
  2315. errors: [
  2316. {
  2317. type: ErrorCodes.NULL_CHARACTER_REFERENCE,
  2318. loc: { offset: 10, line: 1, column: 11 }
  2319. }
  2320. ]
  2321. }
  2322. ],
  2323. SURROGATE_CHARACTER_REFERENCE: [
  2324. {
  2325. code: '<template>&#xD800;</template>',
  2326. errors: [
  2327. {
  2328. type: ErrorCodes.SURROGATE_CHARACTER_REFERENCE,
  2329. loc: { offset: 10, line: 1, column: 11 }
  2330. }
  2331. ]
  2332. }
  2333. ],
  2334. UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME: [
  2335. {
  2336. code: "<template><div a\"bc=''></div></template>",
  2337. errors: [
  2338. {
  2339. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
  2340. loc: { offset: 16, line: 1, column: 17 }
  2341. }
  2342. ]
  2343. },
  2344. {
  2345. code: "<template><div a'bc=''></div></template>",
  2346. errors: [
  2347. {
  2348. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
  2349. loc: { offset: 16, line: 1, column: 17 }
  2350. }
  2351. ]
  2352. },
  2353. {
  2354. code: "<template><div a<bc=''></div></template>",
  2355. errors: [
  2356. {
  2357. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
  2358. loc: { offset: 16, line: 1, column: 17 }
  2359. }
  2360. ]
  2361. }
  2362. ],
  2363. UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE: [
  2364. {
  2365. code: '<template><div foo=bar"></div></template>',
  2366. errors: [
  2367. {
  2368. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
  2369. loc: { offset: 22, line: 1, column: 23 }
  2370. }
  2371. ]
  2372. },
  2373. {
  2374. code: "<template><div foo=bar'></div></template>",
  2375. errors: [
  2376. {
  2377. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
  2378. loc: { offset: 22, line: 1, column: 23 }
  2379. }
  2380. ]
  2381. },
  2382. {
  2383. code: '<template><div foo=bar<div></div></template>',
  2384. errors: [
  2385. {
  2386. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
  2387. loc: { offset: 22, line: 1, column: 23 }
  2388. }
  2389. ]
  2390. },
  2391. {
  2392. code: '<template><div foo=bar=baz></div></template>',
  2393. errors: [
  2394. {
  2395. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
  2396. loc: { offset: 22, line: 1, column: 23 }
  2397. }
  2398. ]
  2399. },
  2400. {
  2401. code: '<template><div foo=bar`></div></template>',
  2402. errors: [
  2403. {
  2404. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
  2405. loc: { offset: 22, line: 1, column: 23 }
  2406. }
  2407. ]
  2408. }
  2409. ],
  2410. UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME: [
  2411. {
  2412. code: '<template><div =foo=bar></div></template>',
  2413. errors: [
  2414. {
  2415. type: ErrorCodes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME,
  2416. loc: { offset: 15, line: 1, column: 16 }
  2417. }
  2418. ]
  2419. },
  2420. {
  2421. code: '<template><div =></div></template>',
  2422. errors: [
  2423. {
  2424. type: ErrorCodes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME,
  2425. loc: { offset: 15, line: 1, column: 16 }
  2426. }
  2427. ]
  2428. }
  2429. ],
  2430. UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME: [
  2431. {
  2432. code: '<template><?xml?></template>',
  2433. errors: [
  2434. {
  2435. type: ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
  2436. loc: { offset: 11, line: 1, column: 12 }
  2437. }
  2438. ]
  2439. }
  2440. ],
  2441. UNEXPECTED_SOLIDUS_IN_TAG: [
  2442. {
  2443. code: '<template><div a/b></div></template>',
  2444. errors: [
  2445. {
  2446. type: ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG,
  2447. loc: { offset: 16, line: 1, column: 17 }
  2448. }
  2449. ]
  2450. }
  2451. ],
  2452. UNKNOWN_NAMED_CHARACTER_REFERENCE: [
  2453. {
  2454. code: '<template>&unknown;</template>',
  2455. errors: [
  2456. {
  2457. type: ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE,
  2458. loc: { offset: 10, line: 1, column: 11 }
  2459. }
  2460. ]
  2461. }
  2462. ],
  2463. X_INVALID_END_TAG: [
  2464. {
  2465. code: '<template></div></template>',
  2466. errors: [
  2467. {
  2468. type: ErrorCodes.X_INVALID_END_TAG,
  2469. loc: { offset: 10, line: 1, column: 11 }
  2470. }
  2471. ]
  2472. },
  2473. {
  2474. code: '<template></div></div></template>',
  2475. errors: [
  2476. {
  2477. type: ErrorCodes.X_INVALID_END_TAG,
  2478. loc: { offset: 10, line: 1, column: 11 }
  2479. },
  2480. {
  2481. type: ErrorCodes.X_INVALID_END_TAG,
  2482. loc: { offset: 16, line: 1, column: 17 }
  2483. }
  2484. ]
  2485. },
  2486. {
  2487. code: "<template>{{'</div>'}}</template>",
  2488. errors: []
  2489. },
  2490. {
  2491. code: '<textarea></div></textarea>',
  2492. errors: []
  2493. },
  2494. {
  2495. code: '<svg><![CDATA[</div>]]></svg>',
  2496. errors: []
  2497. },
  2498. {
  2499. code: '<svg><!--</div>--></svg>',
  2500. errors: []
  2501. }
  2502. ],
  2503. X_MISSING_END_TAG: [
  2504. {
  2505. code: '<template><div></template>',
  2506. errors: [
  2507. {
  2508. type: ErrorCodes.X_MISSING_END_TAG,
  2509. loc: { offset: 15, line: 1, column: 16 }
  2510. }
  2511. ]
  2512. },
  2513. {
  2514. code: '<template><div>',
  2515. errors: [
  2516. {
  2517. type: ErrorCodes.X_MISSING_END_TAG,
  2518. loc: { offset: 15, line: 1, column: 16 }
  2519. },
  2520. {
  2521. type: ErrorCodes.X_MISSING_END_TAG,
  2522. loc: { offset: 15, line: 1, column: 16 }
  2523. }
  2524. ]
  2525. }
  2526. ],
  2527. X_MISSING_INTERPOLATION_END: [
  2528. {
  2529. code: '{{ foo',
  2530. errors: [
  2531. {
  2532. type: ErrorCodes.X_MISSING_INTERPOLATION_END,
  2533. loc: { offset: 0, line: 1, column: 1 }
  2534. }
  2535. ]
  2536. },
  2537. {
  2538. code: '{{',
  2539. errors: [
  2540. {
  2541. type: ErrorCodes.X_MISSING_INTERPOLATION_END,
  2542. loc: { offset: 0, line: 1, column: 1 }
  2543. }
  2544. ]
  2545. },
  2546. {
  2547. code: '{{}}',
  2548. errors: []
  2549. }
  2550. ],
  2551. X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END: [
  2552. {
  2553. code: `<div v-foo:[sef fsef] />`,
  2554. errors: [
  2555. {
  2556. type: ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END,
  2557. loc: { offset: 15, line: 1, column: 16 }
  2558. }
  2559. ]
  2560. }
  2561. ]
  2562. }
  2563. for (const key of Object.keys(patterns) as (keyof (typeof patterns))[]) {
  2564. describe(key, () => {
  2565. for (const { code, errors, options } of patterns[key]) {
  2566. test(
  2567. code.replace(
  2568. /[\r\n]/g,
  2569. c => `\\x0${c.codePointAt(0)!.toString(16)};`
  2570. ),
  2571. () => {
  2572. const spy = jest.fn()
  2573. const ast = parse(code, {
  2574. getNamespace: (tag, parent) => {
  2575. const ns = parent ? parent.ns : Namespaces.HTML
  2576. if (ns === Namespaces.HTML) {
  2577. if (tag === 'svg') {
  2578. return (Namespaces.HTML + 1) as any
  2579. }
  2580. }
  2581. return ns
  2582. },
  2583. getTextMode: tag => {
  2584. if (tag === 'textarea') {
  2585. return TextModes.RCDATA
  2586. }
  2587. if (tag === 'script') {
  2588. return TextModes.RAWTEXT
  2589. }
  2590. return TextModes.DATA
  2591. },
  2592. ...options,
  2593. onError: spy
  2594. })
  2595. expect(
  2596. spy.mock.calls.map(([err]) => ({
  2597. type: err.code,
  2598. loc: err.loc.start
  2599. }))
  2600. ).toMatchObject(errors)
  2601. expect(ast).toMatchSnapshot()
  2602. }
  2603. )
  2604. }
  2605. })
  2606. }
  2607. })
  2608. })