abbreviation.spec.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. /**
  2. * @vitest-environment jsdom
  3. */
  4. import { compile } from '../src'
  5. const parser: DOMParser = new DOMParser()
  6. function parseHTML(html: string): string {
  7. return parser.parseFromString(html, 'text/html').body.innerHTML
  8. }
  9. function getCompiledTemplates(template: string): string[] {
  10. const { ast } = compile(template)
  11. return Array.from(ast.template.keys())
  12. }
  13. function checkAbbr(
  14. template: string,
  15. abbreviation: string | string[],
  16. expected: string | string[],
  17. ): void {
  18. const abbreviations = Array.isArray(abbreviation)
  19. ? abbreviation
  20. : [abbreviation]
  21. const expectations = Array.isArray(expected) ? expected : [expected]
  22. // verify compiler generates the abbreviated templates
  23. expect(getCompiledTemplates(template)).toEqual(abbreviations)
  24. // verify browser parses each abbreviation back to expected HTML
  25. abbreviations.forEach((abbr, i) => {
  26. expect(parseHTML(abbr)).toBe(expectations[i])
  27. })
  28. }
  29. test('template abbreviation', () => {
  30. // basic - last child can omit closing tag
  31. checkAbbr('<div>hello</div>', '<div>hello', '<div>hello</div>')
  32. checkAbbr(
  33. '<div><div>hello</div></div>',
  34. '<div><div>hello',
  35. '<div><div>hello</div></div>',
  36. )
  37. // non-last child needs closing tag
  38. checkAbbr(
  39. '<div><span>foo</span><span></span></div>',
  40. '<div><span>foo</span><span>',
  41. '<div><span>foo</span><span></span></div>',
  42. )
  43. checkAbbr(
  44. '<div><hr><div></div></div>',
  45. '<div><hr><div>',
  46. '<div><hr><div></div></div>',
  47. )
  48. checkAbbr(
  49. '<div><div></div><hr></div>',
  50. '<div><div></div><hr>',
  51. '<div><div></div><hr></div>',
  52. )
  53. // multi-root: each root generates its own template
  54. checkAbbr(
  55. '<span></span>hello',
  56. ['<span>', 'hello'],
  57. ['<span></span>', 'hello'],
  58. )
  59. })
  60. test('formatting tags', () => {
  61. // formatting tags on rightmost path can omit closing tag
  62. checkAbbr('<div><b>bold</b></div>', '<div><b>bold', '<div><b>bold</b></div>')
  63. checkAbbr(
  64. '<div><i><b>text</b></i></div>',
  65. '<div><i><b>text',
  66. '<div><i><b>text</b></i></div>',
  67. )
  68. // formatting tags NOT on rightmost path need closing tag
  69. checkAbbr(
  70. '<div><b>bold</b><span></span></div>',
  71. '<div><b>bold</b><span>',
  72. '<div><b>bold</b><span></span></div>',
  73. )
  74. checkAbbr(
  75. '<div><b>1</b><b>2</b></div>',
  76. '<div><b>1</b><b>2',
  77. '<div><b>1</b><b>2</b></div>',
  78. )
  79. })
  80. test('same-name nested tags', () => {
  81. // same-name on rightmost path can omit
  82. checkAbbr(
  83. '<div><div>inner</div></div>',
  84. '<div><div>inner',
  85. '<div><div>inner</div></div>',
  86. )
  87. // same-name NOT on rightmost path needs closing tag
  88. checkAbbr(
  89. '<div><div>a</div><div>b</div></div>',
  90. '<div><div>a</div><div>b',
  91. '<div><div>a</div><div>b</div></div>',
  92. )
  93. checkAbbr(
  94. '<span><span>1</span><span>2</span></span>',
  95. '<span><span>1</span><span>2',
  96. '<span><span>1</span><span>2</span></span>',
  97. )
  98. })
  99. test('void tags', () => {
  100. // void tags never need closing tags
  101. checkAbbr('<div><br></div>', '<div><br>', '<div><br></div>')
  102. checkAbbr('<div><hr></div>', '<div><hr>', '<div><hr></div>')
  103. checkAbbr('<div><input></div>', '<div><input>', '<div><input></div>')
  104. checkAbbr('<div><img></div>', '<div><img>', '<div><img></div>')
  105. })
  106. test('deeply nested', () => {
  107. checkAbbr(
  108. '<div><div><div><span>deep</span></div></div></div>',
  109. '<div><div><div><span>deep',
  110. '<div><div><div><span>deep</span></div></div></div>',
  111. )
  112. checkAbbr(
  113. '<div><div><span>a</span><span>b</span></div></div>',
  114. '<div><div><span>a</span><span>b',
  115. '<div><div><span>a</span><span>b</span></div></div>',
  116. )
  117. })
  118. test('always close tags', () => {
  119. // button always needs closing tag unless on rightmost path
  120. checkAbbr(
  121. '<div><button>click</button></div>',
  122. '<div><button>click',
  123. '<div><button>click</button></div>',
  124. )
  125. checkAbbr(
  126. '<div><button>click</button><span>sibling</span></div>',
  127. '<div><button>click</button><span>sibling',
  128. '<div><button>click</button><span>sibling</span></div>',
  129. )
  130. // select always needs closing tag unless rightmost
  131. checkAbbr(
  132. '<div><select></select></div>',
  133. '<div><select>',
  134. '<div><select></select></div>',
  135. )
  136. checkAbbr(
  137. '<div><select></select><span>sibling</span></div>',
  138. '<div><select></select><span>sibling',
  139. '<div><select></select><span>sibling</span></div>',
  140. )
  141. // table always needs closing tag unless rightmost
  142. checkAbbr(
  143. '<div><table></table></div>',
  144. '<div><table>',
  145. '<div><table></table></div>',
  146. )
  147. checkAbbr(
  148. '<div><table></table><span>sibling</span></div>',
  149. '<div><table></table><span>sibling',
  150. '<div><table></table><span>sibling</span></div>',
  151. )
  152. // textarea always needs closing tag unless rightmost
  153. checkAbbr(
  154. '<div><textarea></textarea></div>',
  155. '<div><textarea>',
  156. '<div><textarea></textarea></div>',
  157. )
  158. checkAbbr(
  159. '<div><textarea></textarea><span>sibling</span></div>',
  160. '<div><textarea></textarea><span>sibling',
  161. '<div><textarea></textarea><span>sibling</span></div>',
  162. )
  163. // template always needs closing tag unless rightmost
  164. checkAbbr(
  165. '<div><template></template></div>',
  166. '<div><template>',
  167. '<div><template></template></div>',
  168. )
  169. checkAbbr(
  170. '<div><template></template><span>sibling</span></div>',
  171. '<div><template></template><span>sibling',
  172. '<div><template></template><span>sibling</span></div>',
  173. )
  174. // script always needs closing tag unless rightmost
  175. checkAbbr(
  176. '<div><script></script></div>',
  177. '<div><script>',
  178. '<div><script></script></div>',
  179. )
  180. checkAbbr(
  181. '<div><script></script><span>sibling</span></div>',
  182. '<div><script></script><span>sibling',
  183. '<div><script></script><span>sibling</span></div>',
  184. )
  185. // without always-close elements, normal abbreviation should work
  186. checkAbbr(
  187. '<div><form><input></form></div>',
  188. '<div><form><input>',
  189. '<div><form><input></form></div>',
  190. )
  191. })
  192. test('inline/block ancestor relationships', () => {
  193. // Inline element containing block element with sibling after inline
  194. // The block element must close because inline ancestor needs to close
  195. checkAbbr(
  196. '<div><span><div>text</div></span><p>after</p></div>',
  197. '<div><span><div>text</div></span><p>after',
  198. '<div><span><div>text</div></span><p>after</p></div>',
  199. )
  200. // Same situation but deeper nesting
  201. checkAbbr(
  202. '<div><span><p>text</p></span><span>after</span></div>',
  203. '<div><span><p>text</p></span><span>after',
  204. '<div><span><p>text</p></span><span>after</span></div>',
  205. )
  206. // Inline containing block on rightmost path - can omit
  207. checkAbbr(
  208. '<div><span><div>text</div></span></div>',
  209. '<div><span><div>text',
  210. '<div><span><div>text</div></span></div>',
  211. )
  212. // Normal case - no inline/block issue
  213. checkAbbr('<div><p>text</p></div>', '<div><p>text', '<div><p>text</p></div>')
  214. // Sibling after parent but no inline/block issue
  215. checkAbbr(
  216. '<div><div><p>text</p></div><span>after</span></div>',
  217. '<div><div><p>text</div><span>after',
  218. '<div><div><p>text</p></div><span>after</span></div>',
  219. )
  220. // Multi-level inline nesting with block inside
  221. // Outer span is not rightmost -> Needs close -> Inner block needs close
  222. checkAbbr(
  223. '<div><span><b><div>text</div></b></span><p>after</p></div>',
  224. '<div><span><b><div>text</div></b></span><p>after',
  225. '<div><span><b><div>text</div></b></span><p>after</p></div>',
  226. )
  227. // Mixed nesting: div > span > div > span > div
  228. // The middle div is inside a span that needs closing (because of outer structure)
  229. // Both inner divs need closing because they are inside spans that need closing
  230. checkAbbr(
  231. '<div><span><div><span><div>text</div></span></div></span><p>after</p></div>',
  232. '<div><span><div><span><div>text</div></div></span><p>after',
  233. '<div><span><div><span><div>text</div></span></div></span><p>after</p></div>',
  234. )
  235. })