@defra/interactive-map 0.0.12-alpha → 0.0.15-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/umd/im-core.js +1 -1
  4. package/package.json +9 -4
  5. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  6. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  7. package/plugins/beta/datasets/src/manifest.js +4 -4
  8. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  9. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  10. package/plugins/beta/map-styles/src/manifest.js +2 -2
  11. package/plugins/search/dist/css/index.css +1 -1
  12. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  13. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  14. package/plugins/search/src/events/fetchSuggestions.js +10 -7
  15. package/plugins/search/src/events/fetchSuggestions.test.js +4 -4
  16. package/plugins/search/src/search.scss +8 -3
  17. package/providers/beta/esri/dist/css/index.css +4 -0
  18. package/providers/beta/esri/src/esriProvider.scss +5 -0
  19. package/src/App/components/MapButton/MapButton.jsx +1 -0
  20. package/src/App/components/Panel/Panel.jsx +14 -13
  21. package/src/App/components/Panel/Panel.module.scss +1 -0
  22. package/src/App/hooks/useLayoutMeasurements.js +31 -23
  23. package/src/App/hooks/useLayoutMeasurements.test.js +39 -10
  24. package/src/App/hooks/useModalPanelBehaviour.js +85 -21
  25. package/src/App/hooks/useModalPanelBehaviour.test.js +126 -18
  26. package/src/App/hooks/useVisibleGeometry.js +7 -13
  27. package/src/App/hooks/useVisibleGeometry.test.js +72 -47
  28. package/src/App/layout/Layout.jsx +11 -6
  29. package/src/App/layout/Layout.test.jsx +0 -1
  30. package/src/App/layout/layout.module.scss +83 -10
  31. package/src/App/renderer/HtmlElementHost.jsx +10 -4
  32. package/src/App/renderer/HtmlElementHost.test.jsx +32 -11
  33. package/src/App/renderer/SlotRenderer.jsx +1 -1
  34. package/src/App/renderer/mapPanels.js +1 -2
  35. package/src/App/renderer/mapPanels.test.js +3 -3
  36. package/src/App/renderer/slotHelpers.js +2 -2
  37. package/src/App/renderer/slotHelpers.test.js +3 -3
  38. package/src/App/renderer/slots.js +11 -8
  39. package/src/App/store/AppProvider.jsx +5 -2
  40. package/src/App/store/appDispatchMiddleware.test.js +2 -2
  41. package/src/config/appConfig.js +4 -4
  42. package/src/utils/getSafeZoneInset.js +139 -39
  43. package/src/utils/getSafeZoneInset.test.js +301 -81
@@ -1,89 +1,309 @@
1
1
  import { getSafeZoneInset } from './getSafeZoneInset'
2
2
 
3
- describe('getSafeZoneInset', () => {
4
- let mainRef, insetRef, rightRef, footerRef, actionsRef
5
- let originalGetComputedStyle
6
-
7
- beforeAll(() => { originalGetComputedStyle = window.getComputedStyle })
8
- afterAll(() => { window.getComputedStyle = originalGetComputedStyle })
9
-
10
- beforeEach(() => {
11
- mainRef = { current: { offsetWidth: 800, offsetHeight: 600, offsetLeft: 0 } }
12
- insetRef = { current: { offsetWidth: 100, offsetHeight: 50, offsetTop: 50, offsetLeft: 20 } }
13
- rightRef = { current: { offsetWidth: 50, offsetLeft: 0 } }
14
- footerRef = { current: { offsetTop: 550 } }
15
- actionsRef = { current: { offsetTop: 520 } }
16
-
17
- // Mock CSS var --divider-gap = 10
18
- window.getComputedStyle = jest.fn().mockReturnValue({ getPropertyValue: () => '10' })
19
- })
20
-
21
- const runScenario = ({ isLandscape, insetHeight }) => {
22
- insetRef.current.offsetHeight = insetHeight
23
-
24
- // Manipulate dimensions to influence landscape heuristic
25
- if (isLandscape) {
26
- mainRef.current.offsetWidth = 1000
27
- insetRef.current.offsetWidth = 400
28
- } else {
29
- mainRef.current.offsetWidth = 600
30
- insetRef.current.offsetWidth = 100
31
- }
3
+ const MAIN_WIDTH = 900
4
+ const MAIN_HEIGHT = 600
5
+ const LEFT_LEFT = 10
6
+ const LEFT_WIDTH = 40
7
+ const LEFT_TOP = 60
8
+ const RIGHT_WIDTH = 40
9
+ const ACTIONS_TOP = 540
10
+ const FOOTER_TOP = 560
11
+ const EARLY_ACTIONS_TOP = 500
12
+ const GAP = 8
32
13
 
33
- return getSafeZoneInset({ mainRef, insetRef, rightRef, footerRef, actionsRef })
34
- }
14
+ // Base insets: main.offsetLeft=0 in tests
15
+ const BASE_LEFT = LEFT_LEFT + LEFT_WIDTH + GAP // 58
16
+ const BASE_RIGHT = LEFT_LEFT + RIGHT_WIDTH + GAP // 58
17
+ const BASE_TOP = LEFT_TOP // 60
18
+ const BASE_BOTTOM = (MAIN_HEIGHT - ACTIONS_TOP) + GAP // 68
19
+
20
+ // Height threshold: availableHeight / RATIO = (600-60-68)/2 = 236
21
+ const ABOVE_THRESHOLD = 240
22
+ const BELOW_THRESHOLD = 230
23
+ const COMBINED_ABOVE = 120 // two × 120 + gap = 248 > 236
24
+ const COMBINED_BELOW = 100 // two × 100 + gap = 208 < 236
25
+
26
+ // Width threshold: availableWidth / RATIO = (900-58-58)/2 = 392
27
+ const ABOVE_W_THRESHOLD = 400
28
+ const COMBINED_ABOVE_W = 200 // two × 200 = 400 > 392 → triggers combined
29
+ const COMBINED_BELOW_W = 180 // two × 180 = 360 < 392 → does not trigger
30
+ const PANEL_H_TALL = 150
31
+ const PANEL_H_SHORT = 100
32
+
33
+ const ABOVE_CAP_TOP = 330 // 60+330+8=398 > CAP_HEIGHT ≈ 389.3 → capped
34
+ const FOOTER_INSET = MAIN_HEIGHT - FOOTER_TOP + GAP // 48
35
+ const ABOVE_CAP_BOTTOM = 342 // 48+342+8=398 > CAP_HEIGHT → capped
36
+
37
+ const PANEL_W_STANDARD = 200
38
+ const PANEL_W_WIDE = 250
39
+ const PANEL_W_NARROW = 100
40
+ const PANEL_W_XLARGE = 600 // 10+600+8=618 > CAP_WIDTH ≈ 589.3 → capped
41
+
42
+ const leftInset = w => LEFT_LEFT + w + GAP
43
+ const rightInset = w => LEFT_LEFT + w + GAP
44
+ const topInset = h => BASE_TOP + h + GAP
45
+
46
+ const MAX_RATIO = 3
47
+ const CAP_WIDTH = (MAIN_WIDTH - 2 * GAP) * (MAX_RATIO - 1) / MAX_RATIO
48
+ const CAP_HEIGHT = (MAIN_HEIGHT - 2 * GAP) * (MAX_RATIO - 1) / MAX_RATIO
49
+
50
+ // ─── Setup ──────────────────────────────────────────────────────────────────
35
51
 
36
- it('topOffset adds 0 when portrait and height = 0', () => {
37
- const result = runScenario({ isLandscape: false, insetHeight: 0 })
38
- expect(result.top).toBe(insetRef.current.offsetTop)
39
- })
40
-
41
- it('landscape returns left = rightOffset when there is enough room', () => {
42
- const result = runScenario({ isLandscape: true, insetHeight: 50 })
43
- expect(result.top).toBe(insetRef.current.offsetTop)
44
- expect(result.left).toBe(80) // rightOffset = 20 + 50 + 10
45
- expect(result.left).toBe(result.right) // left equals returned rightOffset
46
- })
47
-
48
- it('landscape returns left = rightOffset even when inset height = 0', () => {
49
- const result = runScenario({ isLandscape: true, insetHeight: 0 })
50
- expect(result.top).toBe(insetRef.current.offsetTop)
51
- expect(result.left).toBe(80)
52
- expect(result.left).toBe(result.right)
53
- })
54
-
55
- it('portrait shifts inset below itself when it does NOT have enough vertical room', () => {
56
- // Force a portrait overflow case
57
- mainRef.current.offsetWidth = 200
58
- insetRef.current.offsetWidth = 100
59
- insetRef.current.offsetHeight = 50
60
- insetRef.current.offsetTop = 50
61
- window.getComputedStyle = jest.fn().mockReturnValue({ getPropertyValue: () => '10' })
62
-
63
- const result = getSafeZoneInset({ mainRef, insetRef, rightRef, footerRef, actionsRef })
64
-
65
- // topOffset = 50 + 50 + 10 = 110
66
- expect(result.top).toBe(110)
67
- // left = rightOffset = 20 + 50 + 10 = 80
68
- expect(result.left).toBe(80)
69
- expect(result.right).toBe(80)
70
- })
71
-
72
- /**
73
- * Test to ensure coverage for the safety guardrail (Line 29).
74
- * Validates that the function returns undefined if React refs are
75
- * not yet attached to DOM elements.
76
- */
77
- it('returns undefined if any ref.current is null (unattached)', () => {
78
- const unattachedRefs = {
79
- mainRef: { current: null },
80
- insetRef: { current: null },
81
- rightRef: { current: null },
82
- actionsRef: { current: null },
83
- footerRef: { current: null }
52
+ let mainRef, leftRef, rightRef, actionsRef, footerRef
53
+
54
+ beforeAll(() => {
55
+ globalThis.getComputedStyle = jest.fn().mockReturnValue({ getPropertyValue: () => String(GAP) })
56
+ })
57
+
58
+ const colRef = (offsetWidth, offsetLeft, offsetTop) => {
59
+ const buttonGroup = { offsetWidth }
60
+ return {
61
+ current: {
62
+ offsetWidth,
63
+ offsetLeft,
64
+ offsetTop,
65
+ querySelector: (sel) => sel === '.im-c-button-group' ? buttonGroup : null
84
66
  }
67
+ }
68
+ }
69
+
70
+ beforeEach(() => {
71
+ mainRef = { current: { offsetWidth: MAIN_WIDTH, offsetHeight: MAIN_HEIGHT, offsetLeft: 0 } }
72
+ leftRef = colRef(LEFT_WIDTH, LEFT_LEFT, LEFT_TOP)
73
+ rightRef = colRef(RIGHT_WIDTH)
74
+ actionsRef = { current: { offsetTop: ACTIONS_TOP } }
75
+ footerRef = { current: { offsetTop: FOOTER_TOP } }
76
+ })
77
+
78
+ const base = () => ({ mainRef, leftRef, rightRef, actionsRef, footerRef })
79
+
80
+ // Slot container where an .im-c-panel is the first element child.
81
+ const panel = (offsetWidth, offsetHeight) => {
82
+ const panelEl = { offsetWidth, offsetHeight, classList: { contains: (c) => c === 'im-c-panel' } }
83
+ return { current: { firstElementChild: panelEl } }
84
+ }
85
+
86
+ // Slot container where a button precedes the panel (panel should be ignored).
87
+ const panelAfterButton = () => ({ current: { firstElementChild: { classList: { contains: () => false } } } })
88
+
89
+ // ─── Missing refs ────────────────────────────────────────────────────────────
90
+
91
+ describe('getSafeZoneInset — missing refs', () => {
92
+ it('returns undefined when mainRef.current is null', () => {
93
+ expect(getSafeZoneInset({ ...base(), mainRef: { current: null } })).toBeUndefined()
94
+ })
95
+ it('returns undefined when leftRef.current is null', () => {
96
+ expect(getSafeZoneInset({ ...base(), leftRef: { current: null } })).toBeUndefined()
97
+ })
98
+ it('returns undefined when actionsRef is undefined', () => {
99
+ expect(getSafeZoneInset({ mainRef, leftRef, rightRef, footerRef })).toBeUndefined()
100
+ })
101
+ })
102
+
103
+ // ─── Base structural insets ──────────────────────────────────────────────────
104
+
105
+ describe('getSafeZoneInset — base structural insets', () => {
106
+ it('returns base insets when no panel refs are provided', () => {
107
+ expect(getSafeZoneInset(base())).toEqual({
108
+ left: BASE_LEFT, right: BASE_RIGHT, top: BASE_TOP, bottom: BASE_BOTTOM
109
+ })
110
+ })
111
+ it('ignores a panel that is not the first element in its slot (buttons precede it)', () => {
112
+ expect(getSafeZoneInset({ ...base(), leftTopRef: panelAfterButton() }).left).toBe(BASE_LEFT)
113
+ })
114
+ it('ignores a slot container with no children', () => {
115
+ expect(getSafeZoneInset({ ...base(), leftTopRef: { current: { firstElementChild: null } } }).left).toBe(BASE_LEFT)
116
+ })
117
+ it('ignores a panel with zero width', () => {
118
+ expect(getSafeZoneInset({ ...base(), leftTopRef: panel(0, ABOVE_THRESHOLD) }).left).toBe(BASE_LEFT)
119
+ })
120
+ it('uses zero button width when column ref has no button group', () => {
121
+ const noGroupLeft = { current: { offsetWidth: 0, offsetLeft: LEFT_LEFT, offsetTop: LEFT_TOP, querySelector: () => null } }
122
+ const noGroupRight = { current: { offsetWidth: 0, offsetLeft: 0, offsetTop: 0, querySelector: () => null } }
123
+ expect(getSafeZoneInset({ mainRef, leftRef: noGroupLeft, rightRef: noGroupRight, actionsRef, footerRef })).toEqual({
124
+ left: LEFT_LEFT + GAP, right: LEFT_LEFT + GAP, top: LEFT_TOP, bottom: BASE_BOTTOM
125
+ })
126
+ })
127
+ it('returns base insets when all panel slots are empty (height 0)', () => {
128
+ expect(getSafeZoneInset({
129
+ ...base(),
130
+ leftTopRef: panel(PANEL_W_STANDARD, 0),
131
+ leftBottomRef: panel(PANEL_W_STANDARD, 0),
132
+ rightTopRef: panel(PANEL_W_STANDARD, 0),
133
+ rightBottomRef: panel(PANEL_W_STANDARD, 0)
134
+ })).toEqual({ left: BASE_LEFT, right: BASE_RIGHT, top: BASE_TOP, bottom: BASE_BOTTOM })
135
+ })
136
+ it('uses max of actions and footer for base bottom', () => {
137
+ actionsRef.current.offsetTop = EARLY_ACTIONS_TOP
138
+ expect(getSafeZoneInset(base()).bottom).toBe(MAIN_HEIGHT - EARLY_ACTIONS_TOP + GAP)
139
+ })
140
+ })
141
+
142
+ // ─── Left edge ───────────────────────────────────────────────────────────────
143
+
144
+ describe('getSafeZoneInset — left edge', () => {
145
+ it('does not trigger when single panel height is below threshold', () => {
146
+ expect(getSafeZoneInset({ ...base(), leftTopRef: panel(PANEL_W_STANDARD, BELOW_THRESHOLD) }).left).toBe(BASE_LEFT)
147
+ })
148
+ it('triggers when single panel height exceeds threshold', () => {
149
+ expect(getSafeZoneInset({ ...base(), leftTopRef: panel(PANEL_W_STANDARD, ABOVE_THRESHOLD) }).left)
150
+ .toBe(leftInset(PANEL_W_STANDARD))
151
+ })
152
+ it('triggers when combined height of two panels exceeds threshold', () => {
153
+ expect(getSafeZoneInset({
154
+ ...base(),
155
+ leftTopRef: panel(PANEL_W_STANDARD, COMBINED_ABOVE),
156
+ leftBottomRef: panel(PANEL_W_NARROW, COMBINED_ABOVE)
157
+ }).left).toBe(leftInset(PANEL_W_STANDARD))
158
+ })
159
+ it('does not trigger when combined height is below threshold', () => {
160
+ expect(getSafeZoneInset({
161
+ ...base(),
162
+ leftTopRef: panel(PANEL_W_STANDARD, COMBINED_BELOW),
163
+ leftBottomRef: panel(PANEL_W_STANDARD, COMBINED_BELOW)
164
+ }).left).toBe(BASE_LEFT)
165
+ })
166
+ it('uses the wider panel for the inset amount', () => {
167
+ expect(getSafeZoneInset({
168
+ ...base(),
169
+ leftTopRef: panel(PANEL_W_WIDE, COMBINED_ABOVE),
170
+ leftBottomRef: panel(PANEL_W_NARROW, COMBINED_ABOVE)
171
+ }).left).toBe(leftInset(PANEL_W_WIDE))
172
+ })
173
+ })
174
+
175
+ // ─── Right edge ──────────────────────────────────────────────────────────────
176
+
177
+ describe('getSafeZoneInset — right edge', () => {
178
+ it('triggers when combined height of right-column panels exceeds threshold', () => {
179
+ expect(getSafeZoneInset({
180
+ ...base(),
181
+ rightTopRef: panel(PANEL_W_STANDARD, COMBINED_ABOVE),
182
+ rightBottomRef: panel(PANEL_W_NARROW, COMBINED_ABOVE)
183
+ }).right).toBe(rightInset(PANEL_W_STANDARD))
184
+ })
185
+ it('does not trigger when combined height is below threshold', () => {
186
+ expect(getSafeZoneInset({
187
+ ...base(),
188
+ rightTopRef: panel(PANEL_W_STANDARD, COMBINED_BELOW),
189
+ rightBottomRef: panel(PANEL_W_STANDARD, COMBINED_BELOW)
190
+ }).right).toBe(BASE_RIGHT)
191
+ })
192
+ })
193
+
194
+ // ─── Top edge ────────────────────────────────────────────────────────────────
195
+ // Trigger is WIDTH-based. A narrow panel must not add top padding even if tall.
196
+
197
+ describe('getSafeZoneInset — top edge', () => {
198
+ it('does not trigger when top panel is narrow, even if tall', () => {
199
+ // PANEL_W_STANDARD (200) < width threshold (392)
200
+ expect(getSafeZoneInset({ ...base(), rightTopRef: panel(PANEL_W_STANDARD, ABOVE_THRESHOLD) }).top).toBe(BASE_TOP)
201
+ })
202
+ it('triggers when a top panel width exceeds threshold', () => {
203
+ expect(getSafeZoneInset({ ...base(), leftTopRef: panel(ABOVE_W_THRESHOLD, ABOVE_THRESHOLD) }).top)
204
+ .toBe(topInset(ABOVE_THRESHOLD))
205
+ })
206
+ it('column-primary wide-and-tall top panel triggers left inset, not top', () => {
207
+ // panel(400,330): h/availableH≈0.699 > w/availableW≈0.510 → column-primary
208
+ const result = getSafeZoneInset({ ...base(), leftTopRef: panel(ABOVE_W_THRESHOLD, ABOVE_CAP_TOP) })
209
+ expect(result.left).toBe(leftInset(ABOVE_W_THRESHOLD))
210
+ expect(result.top).toBe(BASE_TOP)
211
+ })
212
+ it('when top panels have mixed primaries, each contributes to its own edge', () => {
213
+ // tl(400,100): row-primary → top; tr(400,330): column-primary → right
214
+ const result = getSafeZoneInset({
215
+ ...base(),
216
+ leftTopRef: panel(ABOVE_W_THRESHOLD, COMBINED_BELOW),
217
+ rightTopRef: panel(ABOVE_W_THRESHOLD, ABOVE_CAP_TOP)
218
+ })
219
+ expect(result.top).toBe(topInset(COMBINED_BELOW))
220
+ expect(result.right).toBe(rightInset(ABOVE_W_THRESHOLD))
221
+ })
222
+ it('triggers when combined width of two top panels exceeds threshold; uses max height', () => {
223
+ // each COMBINED_ABOVE_W (200) < threshold (392), but 200+200=400 > 392
224
+ expect(getSafeZoneInset({
225
+ ...base(),
226
+ leftTopRef: panel(COMBINED_ABOVE_W, PANEL_H_TALL),
227
+ rightTopRef: panel(COMBINED_ABOVE_W, PANEL_H_SHORT)
228
+ }).top).toBe(topInset(PANEL_H_TALL))
229
+ })
230
+ it('does not trigger when both top panels are below combined width threshold', () => {
231
+ expect(getSafeZoneInset({
232
+ ...base(),
233
+ leftTopRef: panel(COMBINED_BELOW_W, ABOVE_THRESHOLD),
234
+ rightTopRef: panel(COMBINED_BELOW_W, ABOVE_THRESHOLD)
235
+ }).top).toBe(BASE_TOP)
236
+ })
237
+ })
238
+
239
+ // ─── Bottom edge ─────────────────────────────────────────────────────────────
240
+
241
+ describe('getSafeZoneInset — bottom edge', () => {
242
+ it('does not trigger when bottom panel is narrow, even if tall', () => {
243
+ expect(getSafeZoneInset({ ...base(), rightBottomRef: panel(PANEL_W_STANDARD, ABOVE_THRESHOLD) }).bottom).toBe(BASE_BOTTOM)
244
+ })
245
+ it('triggers when a bottom panel width exceeds threshold', () => {
246
+ expect(getSafeZoneInset({ ...base(), leftBottomRef: panel(ABOVE_W_THRESHOLD, ABOVE_THRESHOLD) }).bottom)
247
+ .toBe(Math.min(FOOTER_INSET + ABOVE_THRESHOLD + GAP, CAP_HEIGHT))
248
+ })
249
+ it('column-primary wide-and-tall bottom panel triggers left inset, not bottom', () => {
250
+ // panel(400,342): h/availableH≈0.724 > w/availableW≈0.510 → column-primary
251
+ const result = getSafeZoneInset({ ...base(), leftBottomRef: panel(ABOVE_W_THRESHOLD, ABOVE_CAP_BOTTOM) })
252
+ expect(result.left).toBe(leftInset(ABOVE_W_THRESHOLD))
253
+ expect(result.bottom).toBe(BASE_BOTTOM)
254
+ })
255
+ it('triggers when combined width of two bottom panels exceeds threshold; uses max height', () => {
256
+ expect(getSafeZoneInset({
257
+ ...base(),
258
+ leftBottomRef: panel(COMBINED_ABOVE_W, PANEL_H_TALL),
259
+ rightBottomRef: panel(COMBINED_ABOVE_W, PANEL_H_SHORT)
260
+ }).bottom).toBe(Math.min(FOOTER_INSET + PANEL_H_TALL + GAP, CAP_HEIGHT))
261
+ })
262
+ it('does not trigger when both bottom panels are below combined width threshold', () => {
263
+ expect(getSafeZoneInset({
264
+ ...base(),
265
+ leftBottomRef: panel(COMBINED_BELOW_W, ABOVE_THRESHOLD),
266
+ rightBottomRef: panel(COMBINED_BELOW_W, ABOVE_THRESHOLD)
267
+ }).bottom).toBe(BASE_BOTTOM)
268
+ })
269
+ })
270
+
271
+ // ─── MAX_RATIO cap ────────────────────────────────────────────────────────────
85
272
 
86
- const result = getSafeZoneInset(unattachedRefs)
87
- expect(result).toBeUndefined()
273
+ describe('getSafeZoneInset MAX_RATIO cap', () => {
274
+ it('caps left inset at (MAX_RATIO-1)/MAX_RATIO of usable width', () => {
275
+ expect(getSafeZoneInset({ ...base(), leftTopRef: panel(PANEL_W_XLARGE, PANEL_W_XLARGE) }).left).toBe(CAP_WIDTH)
276
+ })
277
+ it('caps right inset at (MAX_RATIO-1)/MAX_RATIO of usable width', () => {
278
+ expect(getSafeZoneInset({ ...base(), rightTopRef: panel(PANEL_W_XLARGE, PANEL_W_XLARGE) }).right).toBe(CAP_WIDTH)
279
+ })
280
+ })
281
+
282
+ // ─── Corner panel independence ────────────────────────────────────────────────
283
+
284
+ describe('getSafeZoneInset — corner panel independence', () => {
285
+ it('narrow-but-tall corner panel triggers side inset only (not top)', () => {
286
+ // PANEL_W_STANDARD (200): tall enough for right, too narrow for top (< 392)
287
+ const result = getSafeZoneInset({ ...base(), rightTopRef: panel(PANEL_W_STANDARD, ABOVE_THRESHOLD) })
288
+ expect(result.right).toBe(rightInset(PANEL_W_STANDARD))
289
+ expect(result.top).toBe(BASE_TOP)
290
+ })
291
+ it('wide-and-tall corner panel triggers only its primary (row) edge inset', () => {
292
+ // panel(400, 240): w/availableW≈0.510, h/availableH≈0.508 → row-primary → top only
293
+ const result = getSafeZoneInset({ ...base(), leftTopRef: panel(ABOVE_W_THRESHOLD, ABOVE_THRESHOLD) })
294
+ expect(result.left).toBe(BASE_LEFT)
295
+ expect(result.top).toBe(topInset(ABOVE_THRESHOLD))
296
+ })
297
+ it('two wide panels in the same column collectively trigger left but not top or bottom', () => {
298
+ // Each h=COMBINED_ABOVE (120) < hThreshold individually, combined 248 > 236
299
+ // Each w=ABOVE_W_THRESHOLD (400) > wThreshold → left column triggers, excluding from top/bottom
300
+ const result = getSafeZoneInset({
301
+ ...base(),
302
+ leftTopRef: panel(ABOVE_W_THRESHOLD, COMBINED_ABOVE),
303
+ leftBottomRef: panel(ABOVE_W_THRESHOLD, COMBINED_ABOVE)
304
+ })
305
+ expect(result.left).toBe(leftInset(ABOVE_W_THRESHOLD))
306
+ expect(result.top).toBe(BASE_TOP)
307
+ expect(result.bottom).toBe(BASE_BOTTOM)
88
308
  })
89
309
  })