measure.client.spec.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. // @vitest-environment jsdom
  2. /**
  3. * The DOM side of the room rule, over a hand-built pane tree: which rectangles
  4. * and computed styles feed `halvesFit`, and what an unmeasured pane reads as.
  5. */
  6. import { afterEach, describe, expect, it } from 'vitest'
  7. import { fitOf, measurePaneFits, paneElements, sameFits } from '../src/components/measure.ts'
  8. import type { HalvesFit, Rect } from '../src/engine/geometry.ts'
  9. import { asPane } from './fixtures.client.ts'
  10. /** Give one element a fixed rectangle, as a browser's layout would. */
  11. function lay(element: HTMLElement, rect: Rect): void {
  12. element.getBoundingClientRect = (): DOMRect => ({
  13. ...rect, top: rect.y, left: rect.x, right: rect.x + rect.width, bottom: rect.y + rect.height, toJSON: () => ({}),
  14. })
  15. }
  16. /** A pane element with the parts the measurement reads; `width` is the pane's, `fixed` the strip's controls. */
  17. function pane(id: string, width: number, fixed = 104): HTMLElement {
  18. const element = document.createElement('section')
  19. element.dataset.dockkitPane = id
  20. lay(element, { x: 0, y: 0, width, height: 600 })
  21. const strip = document.createElement('div')
  22. strip.dataset.dockkitStrip = id
  23. lay(strip, { x: 1, y: 1, width: width - 2, height: 36 })
  24. const chips = document.createElement('div')
  25. chips.dataset.dockkitStripTabs = id
  26. lay(chips, { x: 1, y: 1, width: 60, height: 34 })
  27. const fill = document.createElement('div')
  28. fill.dataset.dockkitStripFill = ''
  29. lay(fill, { x: 61, y: 1, width: Math.max(0, width - 2 - 60 - fixed), height: 34 })
  30. strip.append(chips, fill)
  31. element.append(strip)
  32. return element
  33. }
  34. /** A chip with the given inline box styles, appended to `strip`'s chip box. */
  35. function chip(host: HTMLElement, style: Partial<CSSStyleDeclaration>): HTMLElement {
  36. const element = document.createElement('div')
  37. element.dataset.dockkitTab = 't'
  38. Object.assign(element.style, style)
  39. host.querySelector('[data-dockkit-strip-tabs]')?.append(element)
  40. return element
  41. }
  42. /** A surface root in the document, so computed styles resolve. */
  43. function surface(): HTMLElement {
  44. const root = document.createElement('div')
  45. document.body.append(root)
  46. return root
  47. }
  48. afterEach(() => { document.body.replaceChildren() })
  49. describe('measurePaneFits', () => {
  50. it('keys every pane by the id it carries, and reads an unlaid pane as fitting', () => {
  51. const root = surface()
  52. const bare = document.createElement('section')
  53. bare.dataset.dockkitPane = 'bare'
  54. root.append(pane('wide', 520), bare)
  55. expect(paneElements(root).map(([id]) => id)).toEqual(['wide', 'bare'])
  56. const fits = measurePaneFits(root)
  57. expect(fits.get(asPane('wide'))).toEqual({ row: true, column: true })
  58. expect(fits.get(asPane('bare'))).toEqual({ row: true, column: true })
  59. })
  60. // 308px: halves of 152px inside the borders, against 104px of controls plus one chip.
  61. it('reads the chip minimum from a rendered chip\'s computed style, padding included for a content box', () => {
  62. const root = surface()
  63. const narrow = pane('p', 308)
  64. root.append(narrow)
  65. // No chip rendered: the stylesheet's 100px, so 204 > 152.
  66. expect(measurePaneFits(root).get(asPane('p'))?.row).toBe(false)
  67. // 44px of content plus 4px + 4px of padding is 52: 156 > 152.
  68. const rendered = chip(narrow, { minWidth: '44px', paddingLeft: '4px', paddingRight: '4px', boxSizing: 'content-box' })
  69. expect(measurePaneFits(root).get(asPane('p'))?.row).toBe(false)
  70. // The same declaration as a border box is the whole footprint: 148 fits.
  71. rendered.style.boxSizing = 'border-box'
  72. expect(measurePaneFits(root).get(asPane('p'))?.row).toBe(true)
  73. // Unstyled or zero minimum: back to the stylesheet's figure.
  74. rendered.style.minWidth = '0px'
  75. expect(measurePaneFits(root).get(asPane('p'))?.row).toBe(false)
  76. rendered.style.minWidth = ''
  77. expect(measurePaneFits(root).get(asPane('p'))?.row).toBe(false)
  78. })
  79. // 416px: halves of 204px against 204 with the stylesheet's zero divider, 202 with an 8px one.
  80. it('reads the divider\'s thickness from a rendered divider, and the stylesheet\'s before one exists', () => {
  81. const root = surface()
  82. root.append(pane('p', 416))
  83. expect(measurePaneFits(root).get(asPane('p'))?.row).toBe(true)
  84. const divider = document.createElement('div')
  85. divider.dataset.dockkitDivider = 's:0'
  86. root.append(divider)
  87. lay(divider, { x: 0, y: 0, width: 8, height: 600 })
  88. expect(measurePaneFits(root).get(asPane('p'))?.row).toBe(false)
  89. // A divider without layout yet falls back too.
  90. lay(divider, { x: 0, y: 0, width: 0, height: 0 })
  91. expect(measurePaneFits(root).get(asPane('p'))?.row).toBe(true)
  92. })
  93. })
  94. describe('fitOf', () => {
  95. it('answers from the map, and takes an unmeasured pane to fit', () => {
  96. const blocked: HalvesFit = { row: false, column: true }
  97. const fits = new Map([[asPane('p'), blocked]])
  98. expect(fitOf(fits, asPane('p'))).toBe(blocked)
  99. expect(fitOf(fits, asPane('q'))).toEqual({ row: true, column: true })
  100. })
  101. })
  102. describe('sameFits', () => {
  103. const fit: HalvesFit = { row: true, column: false }
  104. it('agrees only when both name the same panes with the same readings', () => {
  105. expect(sameFits(new Map([[asPane('a'), fit]]), new Map([[asPane('a'), { ...fit }]]))).toBe(true)
  106. expect(sameFits(new Map([[asPane('a'), fit]]), new Map())).toBe(false)
  107. expect(sameFits(new Map([[asPane('a'), fit]]), new Map([[asPane('b'), fit]]))).toBe(false)
  108. expect(sameFits(new Map([[asPane('a'), fit]]), new Map([[asPane('a'), { row: false, column: false }]]))).toBe(false)
  109. expect(sameFits(new Map([[asPane('a'), fit]]), new Map([[asPane('a'), { row: true, column: true }]]))).toBe(false)
  110. })
  111. })