browser-styles.spec.ts 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. /**
  2. * WorkspaceBrowser spacing contract, asserted against the CSS text on disk:
  3. * row fills share the shell's trailing inset, the stable scrollbar counts
  4. * inside it, and flat, grouped, and search views keep their intended rhythm.
  5. */
  6. import { readFileSync } from 'node:fs'
  7. import { fileURLToPath } from 'node:url'
  8. import { describe, expect, it } from 'vitest'
  9. const css = readFileSync(fileURLToPath(new URL('../src/client/WorkspaceBrowser.module.css', import.meta.url)), 'utf8')
  10. /**
  11. * Declarations of one selector rule, keyed by property with whitespace collapsed.
  12. * Declaration order and trailing semicolons are normalized away.
  13. * @param selector - one exact selector, including a leading dot for local classes.
  14. * @returns the rule's declarations, or undefined when no such rule exists.
  15. */
  16. function declarations(selector: string): Map<string, string> | undefined {
  17. const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, ' ')
  18. for (const [, selectorList = '', body = ''] of withoutComments.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
  19. if (!selectorList.split(',').map(value => value.trim()).includes(selector)) continue
  20. const found = new Map<string, string>()
  21. for (const part of body.split(';')) {
  22. const colon = part.indexOf(':')
  23. if (colon === -1) continue
  24. found.set(part.slice(0, colon).trim(), part.slice(colon + 1).trim().replace(/\s+/g, ' '))
  25. }
  26. return found
  27. }
  28. return undefined
  29. }
  30. describe('WorkspaceBrowser.module.css list', () => {
  31. const root = declarations('.root')
  32. const listArea = declarations('.listArea')
  33. const list = declarations('.list')
  34. it('is the scrolling region', () => {
  35. expect(list).toBeDefined()
  36. expect(list!.get('overflow-y')).toBe('auto')
  37. })
  38. it('counts the themed scrollbar inside the shell trailing inset', () => {
  39. expect(root?.get('--dsh-session-list-edge-inset')).toBe('var(--dsh-sidebar-inline-padding)')
  40. expect(root?.get('--dsh-session-list-scrollbar-width')).toBe('8px')
  41. expect(root?.get('--dsh-session-list-scrollbar-offset')).toBe('2px')
  42. expect(root?.get('padding-right')).toBe('var(--dsh-session-list-edge-inset)')
  43. expect(listArea?.get('margin-right')).toBe('calc(-1 * var(--dsh-session-list-edge-inset))')
  44. expect(declarations('.fade')?.get('right')).toBe('var(--dsh-session-list-edge-inset)')
  45. expect(list?.get('margin-right')).toBe('var(--dsh-session-list-scrollbar-offset)')
  46. expect(list?.get('padding-right')).toBe([
  47. 'calc(',
  48. 'var(--dsh-session-list-edge-inset)',
  49. '- var(--dsh-session-list-scrollbar-width)',
  50. '- var(--dsh-session-list-scrollbar-offset)',
  51. ')',
  52. ].join(' '))
  53. expect(declarations('.list::-webkit-scrollbar')).toBeUndefined()
  54. })
  55. it('reserves the scrollbar whether or not the list overflows', () => {
  56. expect(list!.get('scrollbar-gutter')).toBe('stable')
  57. })
  58. it('keeps 2px between rows and 4px between workspace groups', () => {
  59. expect(declarations('.flatList > * + *')?.get('margin-top')).toBe('2px')
  60. expect(declarations(".searchTree > [role='treeitem'] + [role='treeitem']")?.get('margin-top')).toBe('2px')
  61. expect(declarations('.groupSection > * + *')?.get('margin-top')).toBe('2px')
  62. expect(declarations('.groupSection + .groupSection')?.get('margin-top')).toBe('4px')
  63. })
  64. })