columns.spec.ts 4.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import { describe, expect, it } from 'vitest'
  2. import {
  3. CENTER_MIN, clampWidth, computeColumns,
  4. DETAILS_DEFAULT, DETAILS_MIN, SIDEBAR_COLLAPSED, SIDEBAR_DEFAULT, SIDEBAR_MIN,
  5. } from '@deepseek-ai/dsh-client-ui-layout/src/client/columns.ts'
  6. // Numeric preference form (0 = closed); helpers keep the scenario names readable.
  7. const open = (width: number) => width
  8. const closed = (_width: number) => 0
  9. describe('clampWidth', () => {
  10. it('clamps into the range and rounds', () => {
  11. expect(clampWidth(250.4, 240, 420)).toBe(250)
  12. expect(clampWidth(100, 240, 420)).toBe(240)
  13. expect(clampWidth(9999, 240, 420)).toBe(420)
  14. })
  15. })
  16. describe('computeColumns', () => {
  17. it('step 1: everything fits at preferred widths', () => {
  18. const cols = computeColumns(1920, open(SIDEBAR_DEFAULT), open(DETAILS_DEFAULT))
  19. expect(cols).toEqual({ sidebar: 280, center: 1920 - 280 - 360, details: 360 })
  20. })
  21. it('closed sidebar keeps its compact rail while closed details contribute zero width', () => {
  22. expect(computeColumns(1920, closed(300), closed(360)))
  23. .toEqual({ sidebar: SIDEBAR_COLLAPSED, center: 1920 - SIDEBAR_COLLAPSED, details: 0 })
  24. })
  25. it('preferences beyond the clamp range are clamped before solving', () => {
  26. const cols = computeColumns(1920, open(9999), open(1))
  27. expect(cols.sidebar).toBe(420)
  28. expect(cols.details).toBe(300)
  29. expect(computeColumns(1920, open(1), open(DETAILS_DEFAULT)).sidebar).toBe(SIDEBAR_MIN)
  30. })
  31. it('step 2: details shrinks first, center pinned at min', () => {
  32. // 280 + 360 + 640 = 1280 > 1250; details concedes to 1250-280-640 = 330.
  33. const cols = computeColumns(1250, open(SIDEBAR_DEFAULT), open(DETAILS_DEFAULT))
  34. expect(cols).toEqual({ sidebar: 280, center: CENTER_MIN, details: 330 })
  35. })
  36. it('boundary: exactly at the step-1/step-2 seam', () => {
  37. const cols = computeColumns(300 + 360 + CENTER_MIN, open(300), open(360))
  38. expect(cols).toEqual({ sidebar: 300, center: CENTER_MIN, details: 360 })
  39. const one = computeColumns(300 + 360 + CENTER_MIN - 1, open(300), open(360))
  40. expect(one).toEqual({ sidebar: 300, center: CENTER_MIN, details: 359 })
  41. })
  42. it('step 3: details auto-closes when its min still starves center — sidebar holds its preference', () => {
  43. // 280 + 300 + 640 = 1220 > 1210 → details 0; sidebar untouched: center = 1210-280 = 930.
  44. const cols = computeColumns(1210, open(SIDEBAR_DEFAULT), open(DETAILS_DEFAULT))
  45. expect(cols).toEqual({ sidebar: 280, center: 930, details: 0 })
  46. })
  47. it('the sidebar never concedes: center absorbs the deficit below CENTER_MIN', () => {
  48. // 700 < 280+640: sidebar keeps 280, center takes 420 < CENTER_MIN.
  49. const cols = computeColumns(700, open(SIDEBAR_DEFAULT), closed(DETAILS_DEFAULT))
  50. expect(cols).toEqual({ sidebar: SIDEBAR_DEFAULT, center: 420, details: 0 })
  51. })
  52. it('sidebar-closed narrow window: details concedes then auto-closes', () => {
  53. const fits = computeColumns(SIDEBAR_COLLAPSED + DETAILS_MIN + CENTER_MIN, closed(300), open(DETAILS_DEFAULT))
  54. expect(fits).toEqual({ sidebar: SIDEBAR_COLLAPSED, center: CENTER_MIN, details: DETAILS_MIN })
  55. const starved = computeColumns(SIDEBAR_COLLAPSED + DETAILS_MIN + CENTER_MIN - 1, closed(300), open(DETAILS_DEFAULT))
  56. expect(starved).toEqual({
  57. sidebar: SIDEBAR_COLLAPSED,
  58. center: DETAILS_MIN + CENTER_MIN - 1,
  59. details: 0,
  60. })
  61. })
  62. it('tiny viewport: details closes, sidebar holds, center takes the remainder', () => {
  63. const cols = computeColumns(400, open(SIDEBAR_DEFAULT), open(DETAILS_DEFAULT))
  64. expect(cols.details).toBe(0)
  65. expect(cols.sidebar).toBe(SIDEBAR_DEFAULT)
  66. expect(cols.center).toBe(Math.max(0, 400 - SIDEBAR_DEFAULT))
  67. })
  68. it('recovery is pure: re-widening restores preferred widths untouched', () => {
  69. const squeezed = computeColumns(1100, open(SIDEBAR_DEFAULT), open(DETAILS_DEFAULT))
  70. expect(squeezed.details).toBe(0)
  71. const restored = computeColumns(1920, open(SIDEBAR_DEFAULT), open(DETAILS_DEFAULT))
  72. expect(restored.details).toBe(DETAILS_DEFAULT)
  73. expect(restored.sidebar).toBe(SIDEBAR_DEFAULT)
  74. })
  75. })
  76. describe('computeColumns — degenerate viewports', () => {
  77. it('sidebar closed and viewport below CENTER_MIN: details auto-closes, center takes the rest', () => {
  78. // Reaches step 3's auto-close with the compact rail sidebar.
  79. expect(computeColumns(500, closed(300), open(DETAILS_DEFAULT)))
  80. .toEqual({ sidebar: SIDEBAR_COLLAPSED, center: 500 - SIDEBAR_COLLAPSED, details: 0 })
  81. })
  82. })