corner-shape-styles.client.spec.ts 4.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. /**
  2. * Corner-shape stylesheet contract, asserted against the CSS text on disk:
  3. * corner-shape.css smooths every rounded corner to the superellipse token
  4. * strictly inside a `@supports` guard, and every effectively full-round radius
  5. * in any package stylesheet pairs `corner-shape: round` in the same rule,
  6. * because a superellipse deforms a circle into a squircle (a spinner would
  7. * visibly wobble) and squares off capsule ends.
  8. */
  9. import { readFileSync } from 'node:fs'
  10. import { fileURLToPath } from 'node:url'
  11. import { describe, expect, it } from 'vitest'
  12. import { atRuleBlock, packageStylesheets, parseRules } from './stylesheet-scan.ts'
  13. /** The support guard prelude, spelled exactly as the sheet must spell it. */
  14. const GUARD = '@supports (corner-shape: superellipse(1.5))'
  15. /** The smoothing token corner-shape.css owns. */
  16. const TOKEN = '--dsw-corner-shape'
  17. const sheetPath = fileURLToPath(new URL('../src/styles/corner-shape.css', import.meta.url))
  18. const sheetCss = readFileSync(sheetPath, 'utf8')
  19. /**
  20. * Whether a border-radius value makes the element full-round: an uncapped
  21. * fraction of the box (50%/100%) or a pill radius far above any box size.
  22. * Component-local radius indirections stay below the pill threshold, so the
  23. * check is lexical over literal components.
  24. * @param value - a border-radius declaration value.
  25. * @returns true when some component is full-round.
  26. */
  27. function isFullRound(value: string): boolean {
  28. return value.split(/\s+/).some(part =>
  29. part === '50%' || part === '100%' || (part.endsWith('px') && Number.parseFloat(part) >= 99))
  30. }
  31. describe('corner-shape.css smoothing', () => {
  32. const withoutComments = sheetCss.replace(/\/\*[\s\S]*?\*\//g, ' ')
  33. const guard = atRuleBlock(withoutComments, GUARD)
  34. it('declares the token and its application only inside the support guard', () => {
  35. // Outside the guard the declarations would be dropped as invalid anyway,
  36. // but only on engines without corner-shape; keeping everything inside the
  37. // guard states that unsupporting engines keep plain circular corners.
  38. expect(guard, GUARD).toBeDefined()
  39. const before = withoutComments.slice(0, withoutComments.indexOf(GUARD))
  40. const after = withoutComments.slice(guard!.end + 1)
  41. expect(before.trim(), `content before ${GUARD}`).toBe('')
  42. expect(after.trim(), `content after ${GUARD}`).toBe('')
  43. })
  44. it('defines the superellipse token on :root and applies it universally', () => {
  45. // corner-shape does not inherit, so only the universal selector (with the
  46. // generated ::before/::after) reaches every rounded surface.
  47. const rules = parseRules(withoutComments.slice(guard!.start + 1, guard!.end))
  48. const root = rules.find(rule => rule.selectors.includes(':root'))
  49. expect(root?.declarations).toContainEqual([TOKEN, 'superellipse(1.5)'])
  50. const universal = rules.find(rule => rule.selectors.includes('*'))
  51. expect(universal?.selectors).toEqual(['*', '*::before', '*::after'])
  52. expect(universal?.declarations).toContainEqual(['corner-shape', `var(${TOKEN})`])
  53. })
  54. })
  55. /**
  56. * Full-round rules missing the `corner-shape: round` pairing.
  57. * @param css - stylesheet text.
  58. * @returns the offending selectors, in source order.
  59. */
  60. function unpairedFullRound(css: string): string[] {
  61. return parseRules(css)
  62. .filter(rule => rule.declarations
  63. .some(([property, value]) => property === 'border-radius' && isFullRound(value)))
  64. .filter(rule => !rule.declarations
  65. .some(([property, value]) => property === 'corner-shape' && value === 'round'))
  66. .map(rule => rule.selectors.join(', '))
  67. }
  68. describe('full-round radii keep circular corners', () => {
  69. it('rejects a full-round radius without the pairing', () => {
  70. expect(unpairedFullRound('.a { border-radius: 50%; }')).toEqual(['.a'])
  71. expect(unpairedFullRound('.a { border-radius: 999px; }')).toEqual(['.a'])
  72. expect(unpairedFullRound('.a { border-radius: 50%; corner-shape: round; }')).toEqual([])
  73. })
  74. it('pairs corner-shape: round with every full-round border-radius under packages/', () => {
  75. // The universal superellipse reaches every element, so each circle and
  76. // pill states its own arc back; a new one without the pairing regresses
  77. // silently on supporting engines only, which no jsdom test renders.
  78. const unpaired = packageStylesheets().flatMap(file =>
  79. unpairedFullRound(readFileSync(file, 'utf8')).map(selectors => `${file} ${selectors}`))
  80. expect(unpaired).toEqual([])
  81. })
  82. })