@bespokeagentics/microdots-host 0.1.0 → 0.1.1

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.
@@ -0,0 +1,151 @@
1
+ import { beforeEach, describe, expect, test } from 'vitest'
2
+
3
+ import {
4
+ ensureSection,
5
+ ensureSlot,
6
+ pruneGeneratedContainers,
7
+ } from './slotDom.ts'
8
+
9
+ /**
10
+ * Containers for placements the build never knew about.
11
+ *
12
+ * Two properties carry this module, and both fail silently when broken:
13
+ *
14
+ * 1. **A generated slot carries its id.** Foldkit dies without one and
15
+ * `Runtime.embed` swallows the defect, so the dot renders nothing on a clean
16
+ * console with a green suite.
17
+ * 2. **Existing markup is never regenerated over.** The demo host's sections
18
+ * carry a heading and a description that exist nowhere in the topology;
19
+ * replacing them would quietly turn a showcase into bare boxes, and no
20
+ * assertion about mounting would notice.
21
+ */
22
+
23
+ const SECTION_CLASS = 'generated-section'
24
+ const SLOT_CLASS = 'generated-slot'
25
+
26
+ const generatedRoot = (): HTMLElement => {
27
+ const root = document.createElement('div')
28
+ root.id = 'generated-sections'
29
+ document.body.appendChild(root)
30
+ return root
31
+ }
32
+
33
+ /** A hand-written section, copy and all — what must never be overwritten. */
34
+ const writtenSection = (sectionId: string, slotId: string): HTMLElement => {
35
+ const section = document.createElement('section')
36
+ section.id = sectionId
37
+ section.innerHTML = `<div><h2>Readout view</h2><p>Public readout surface.</p></div><div id="${slotId}"></div>`
38
+ document.body.appendChild(section)
39
+ return section
40
+ }
41
+
42
+ beforeEach(() => {
43
+ document.body.replaceChildren()
44
+ })
45
+
46
+ describe('ensureSection', () => {
47
+ test('creates a section carrying its declared id', () => {
48
+ const root = generatedRoot()
49
+
50
+ const section = ensureSection('section-new', root, SECTION_CLASS)
51
+
52
+ expect(section.id).toBe('section-new')
53
+ expect(section.tagName).toBe('SECTION')
54
+ expect(section.parentElement).toBe(root)
55
+ expect(section.dataset['generated']).toBe('true')
56
+ })
57
+
58
+ test('the id is what lets `activate` hide it — without it the dot renders on every route', () => {
59
+ const root = generatedRoot()
60
+ ensureSection('section-new', root, SECTION_CLASS)
61
+
62
+ expect(document.getElementById('section-new')).not.toBeNull()
63
+ })
64
+
65
+ test('returns hand-written markup UNTOUCHED — the copy survives', () => {
66
+ const root = generatedRoot()
67
+ const written = writtenSection('section-readout-view', 'readout-view-slot')
68
+
69
+ const found = ensureSection('section-readout-view', root, SECTION_CLASS)
70
+
71
+ expect(found).toBe(written)
72
+ expect(found.querySelector('h2')?.textContent).toBe('Readout view')
73
+ expect(found.dataset['generated']).toBeUndefined()
74
+ expect(root.childElementCount).toBe(0)
75
+ })
76
+
77
+ test('is idempotent — a second call adds nothing', () => {
78
+ const root = generatedRoot()
79
+
80
+ const first = ensureSection('section-new', root, SECTION_CLASS)
81
+ const second = ensureSection('section-new', root, SECTION_CLASS)
82
+
83
+ expect(second).toBe(first)
84
+ expect(root.childElementCount).toBe(1)
85
+ })
86
+ })
87
+
88
+ describe('ensureSlot', () => {
89
+ test('THE INVARIANT: a created slot carries its id', () => {
90
+ const root = generatedRoot()
91
+ const section = ensureSection('section-new', root, SECTION_CLASS)
92
+
93
+ const slot = ensureSlot('new-slot', section, SLOT_CLASS)
94
+
95
+ // Foldkit dies without this, and `Runtime.embed` forks the runtime so the
96
+ // defect never reaches the console.
97
+ expect(slot.id).toBe('new-slot')
98
+ expect(document.getElementById('new-slot')).toBe(slot)
99
+ expect(slot.parentElement).toBe(section)
100
+ })
101
+
102
+ test('returns a hand-written slot untouched, and does not move it', () => {
103
+ const root = generatedRoot()
104
+ const written = writtenSection('section-readout-view', 'readout-view-slot')
105
+ const section = ensureSection('section-readout-view', root, SECTION_CLASS)
106
+
107
+ const slot = ensureSlot('readout-view-slot', section, SLOT_CLASS)
108
+
109
+ expect(slot).toBe(written.querySelector('#readout-view-slot'))
110
+ expect(slot.dataset['generated']).toBeUndefined()
111
+ })
112
+
113
+ test('is idempotent — a second call does not add a second slot', () => {
114
+ const root = generatedRoot()
115
+ const section = ensureSection('section-new', root, SECTION_CLASS)
116
+
117
+ const first = ensureSlot('new-slot', section, SLOT_CLASS)
118
+ const second = ensureSlot('new-slot', section, SLOT_CLASS)
119
+
120
+ expect(second).toBe(first)
121
+ expect(section.childElementCount).toBe(1)
122
+ })
123
+ })
124
+
125
+ describe('pruneGeneratedContainers', () => {
126
+ test('removes a generated slot and its section once the dot is unmounted', () => {
127
+ const root = generatedRoot()
128
+ const section = ensureSection('section-new', root, SECTION_CLASS)
129
+ const slot = ensureSlot('new-slot', section, SLOT_CLASS)
130
+ slot.appendChild(document.createElement('new-dot'))
131
+
132
+ // Still mounted: nothing is pruned.
133
+ expect(pruneGeneratedContainers(root)).toBe(0)
134
+
135
+ // The unmount removed the MicroDot's element, leaving the scaffolding.
136
+ slot.replaceChildren()
137
+
138
+ expect(pruneGeneratedContainers(root)).toBe(2)
139
+ expect(document.getElementById('new-slot')).toBeNull()
140
+ expect(document.getElementById('section-new')).toBeNull()
141
+ })
142
+
143
+ test('never touches hand-written markup, however empty', () => {
144
+ const root = generatedRoot()
145
+ const written = writtenSection('section-readout-view', 'readout-view-slot')
146
+
147
+ expect(pruneGeneratedContainers(root)).toBe(0)
148
+ expect(written.isConnected).toBe(true)
149
+ expect(document.getElementById('readout-view-slot')).not.toBeNull()
150
+ })
151
+ })
package/src/slotDom.ts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Containers for placements the BUILD never knew about.
3
+ *
4
+ * Until Phase B of `wiki/plans/active/wiring-live-placement-on-a-remote-host.md`
5
+ * a shell could only mount a MicroDot into a `<div id="…-slot">` somebody had
6
+ * hand-written into `index.html`. That is fine while the topology is compiled
7
+ * in and cannot change; it is fatal the moment the topology is fetched, because
8
+ * a placement added at runtime has nowhere to go and `getElementById` returns
9
+ * `null`.
10
+ *
11
+ * **Markup wins; this is the fallback.** The demo host's twenty-five sections
12
+ * carry a heading and a description per dot that exist nowhere in the topology
13
+ * — the slot manifest declares `id`, `kind`, `row`, `width` and `capacity`, and
14
+ * no copy. Generating over the top of them would silently delete that copy and
15
+ * turn a showcase into a stack of bare boxes, so an element that already exists
16
+ * is always returned as-is. See
17
+ * `wiki/framework/composition/gap-a-generated-section-has-no-copy-to-render.md`.
18
+ *
19
+ * Neither function reads a topology or a route: they take ids and a container,
20
+ * so they are testable without a shell, which is the same reason
21
+ * `mounting.ts` exists.
22
+ */
23
+
24
+ /**
25
+ * The `<section>` a route reveals, created in `generatedRoot` if the page has
26
+ * none.
27
+ *
28
+ * The id matters as much as it does for a slot, though for a different reason:
29
+ * `activate` hides and shows sections by the ids in `route.sectionIds`, so a
30
+ * generated section that does not carry its declared id is a section that never
31
+ * hides — its MicroDot would then render on every route.
32
+ */
33
+ export const ensureSection = (
34
+ sectionId: string,
35
+ generatedRoot: HTMLElement,
36
+ className: string,
37
+ ): HTMLElement => {
38
+ const existing = document.getElementById(sectionId)
39
+ if (existing !== null) return existing
40
+
41
+ const section = document.createElement('section')
42
+ section.id = sectionId
43
+ section.className = className
44
+ section.dataset['generated'] = 'true'
45
+ generatedRoot.appendChild(section)
46
+ return section
47
+ }
48
+
49
+ /**
50
+ * The `<div>` a MicroDot mounts into, created inside `section` if the page has
51
+ * none.
52
+ *
53
+ * **The `id` assignment on the created element is load-bearing and is the
54
+ * single most expensive failure in this repo.** Foldkit dies if its mount
55
+ * container has no `id`, and `Runtime.embed` forks the runtime, so the startup
56
+ * defect never reaches the console: the MicroDot renders nothing, the console
57
+ * is clean, and typecheck, lint, tests and build are all green. Hand-written
58
+ * slots got their ids from a human reading `host-topology.json`; a generated
59
+ * one gets it from here or from nowhere. See
60
+ * `wiki/patterns-and-traps/mount-container-needs-an-id.md`.
61
+ */
62
+ export const ensureSlot = (
63
+ slotId: string,
64
+ section: HTMLElement,
65
+ className: string,
66
+ ): HTMLElement => {
67
+ const existing = document.getElementById(slotId)
68
+ if (existing !== null) return existing
69
+
70
+ const slot = document.createElement('div')
71
+ // Never omit, never rename, never defer to a caller: see above.
72
+ slot.id = slotId
73
+ slot.className = className
74
+ slot.dataset['generated'] = 'true'
75
+ section.appendChild(slot)
76
+ return slot
77
+ }
78
+
79
+ /**
80
+ * Remove generated sections that are now empty.
81
+ *
82
+ * An unmount removes the MicroDot's element but leaves the container that was
83
+ * generated for it, and a page that accumulates empty boxes every time a dot is
84
+ * removed looks broken in a way no assertion catches. Hand-written sections are
85
+ * never touched — they are the page, not scaffolding — which is what
86
+ * `data-generated` distinguishes.
87
+ */
88
+ export const pruneGeneratedContainers = (
89
+ generatedRoot: HTMLElement,
90
+ ): number => {
91
+ const sections = Array.from(
92
+ generatedRoot.querySelectorAll('section[data-generated="true"]'),
93
+ )
94
+ let removed = 0
95
+ sections.forEach(section => {
96
+ const slots = Array.from(
97
+ section.querySelectorAll('div[data-generated="true"]'),
98
+ )
99
+ slots.forEach(slot => {
100
+ if (slot.childElementCount === 0) {
101
+ slot.remove()
102
+ removed += 1
103
+ }
104
+ })
105
+ if (section.childElementCount === 0) {
106
+ section.remove()
107
+ removed += 1
108
+ }
109
+ })
110
+ return removed
111
+ }
@@ -0,0 +1,392 @@
1
+ import { beforeEach, describe, expect, test } from 'vitest'
2
+
3
+ import {
4
+ SLOT_RESIZE_HANDLE_CLASS,
5
+ type SlotResizeStorage,
6
+ applySlotSize,
7
+ attachSlotResize,
8
+ chromeOfSlot,
9
+ clearSlotResize,
10
+ restoreSlotResize,
11
+ slotResizeStorageKey,
12
+ slotSizeVar,
13
+ } from './slotResize.ts'
14
+ import type { SlotSpec } from './slots.ts'
15
+
16
+ const slot = (id: string, resize: SlotSpec['resize']): SlotSpec =>
17
+ resize === undefined
18
+ ? { id, kind: 'rail', row: 1 }
19
+ : { id, kind: 'rail', row: 1, resize }
20
+
21
+ const memoryStorage = (): SlotResizeStorage => {
22
+ const values = new Map<string, string>()
23
+ return {
24
+ getItem: key => values.get(key) ?? null,
25
+ removeItem: key => {
26
+ values.delete(key)
27
+ },
28
+ setItem: (key, value) => {
29
+ values.set(key, value)
30
+ },
31
+ }
32
+ }
33
+
34
+ const wiringShell = (): {
35
+ readonly section: HTMLElement
36
+ readonly drawer: HTMLElement
37
+ readonly bottom: HTMLElement
38
+ } => {
39
+ const section = document.createElement('section')
40
+ section.id = 'section-wiring'
41
+ const canvas = document.createElement('div')
42
+ canvas.id = 'wiring-canvas-slot'
43
+ const drawer = document.createElement('div')
44
+ drawer.className = 'wiring-drawer'
45
+ Object.defineProperty(drawer, 'offsetWidth', {
46
+ value: 380,
47
+ configurable: true,
48
+ })
49
+ Object.defineProperty(drawer, 'offsetHeight', {
50
+ value: 600,
51
+ configurable: true,
52
+ })
53
+ const inspector = document.createElement('div')
54
+ inspector.id = 'wiring-inspector-slot'
55
+ const trace = document.createElement('div')
56
+ trace.id = 'wiring-trace-slot'
57
+ drawer.append(inspector, trace)
58
+ const bottom = document.createElement('div')
59
+ bottom.className = 'wiring-bottom'
60
+ Object.defineProperty(bottom, 'offsetWidth', {
61
+ value: 800,
62
+ configurable: true,
63
+ })
64
+ Object.defineProperty(bottom, 'offsetHeight', {
65
+ value: 200,
66
+ configurable: true,
67
+ })
68
+ const table = document.createElement('div')
69
+ table.id = 'wiring-table-slot'
70
+ bottom.append(table)
71
+ section.append(canvas, drawer, bottom)
72
+ Object.defineProperty(section, 'clientHeight', {
73
+ value: 800,
74
+ configurable: true,
75
+ })
76
+ document.body.append(section)
77
+ return { section, drawer, bottom }
78
+ }
79
+
80
+ beforeEach(() => {
81
+ document.body.replaceChildren()
82
+ window.sessionStorage.clear()
83
+ })
84
+
85
+ describe('chromeOfSlot', () => {
86
+ test('a nested tab pane resizes its grid-area ancestor, not the hidden tab', () => {
87
+ const { section, drawer } = wiringShell()
88
+ const inspector = document.getElementById('wiring-inspector-slot')
89
+ expect(inspector).not.toBeNull()
90
+ if (inspector === null) return
91
+ expect(chromeOfSlot(inspector, section)).toBe(drawer)
92
+ })
93
+ })
94
+
95
+ describe('attachSlotResize', () => {
96
+ test('slots without resize get no handle', () => {
97
+ const { section } = wiringShell()
98
+ attachSlotResize({
99
+ section,
100
+ slots: [slot('wiring-canvas-slot', undefined)],
101
+ })
102
+ expect(section.querySelector(`.${SLOT_RESIZE_HANDLE_CLASS}`)).toBeNull()
103
+ })
104
+
105
+ test('a missing slot id is skipped — generated later, not a throw', () => {
106
+ const { section } = wiringShell()
107
+ expect(() =>
108
+ attachSlotResize({
109
+ section,
110
+ slots: [slot('wiring-missing-slot', 'x')],
111
+ }),
112
+ ).not.toThrow()
113
+ expect(section.querySelector(`.${SLOT_RESIZE_HANDLE_CLASS}`)).toBeNull()
114
+ })
115
+
116
+ test('inspector + trace share the drawer — one x handle, not two', () => {
117
+ const { section, drawer } = wiringShell()
118
+ attachSlotResize({
119
+ section,
120
+ slots: [
121
+ slot('wiring-inspector-slot', 'x'),
122
+ slot('wiring-trace-slot', 'x'),
123
+ ],
124
+ })
125
+ expect(drawer.querySelectorAll(`.${SLOT_RESIZE_HANDLE_CLASS}`).length).toBe(
126
+ 1,
127
+ )
128
+ })
129
+
130
+ test('x and y handles land on their chrome, titled from the prototype', () => {
131
+ const { section, drawer, bottom } = wiringShell()
132
+ attachSlotResize({
133
+ section,
134
+ slots: [
135
+ slot('wiring-inspector-slot', 'x'),
136
+ slot('wiring-table-slot', 'y'),
137
+ ],
138
+ })
139
+ const x = drawer.querySelector(`.${SLOT_RESIZE_HANDLE_CLASS}`)
140
+ const y = bottom.querySelector(`.${SLOT_RESIZE_HANDLE_CLASS}`)
141
+ expect(x).toBeInstanceOf(HTMLButtonElement)
142
+ expect(y).toBeInstanceOf(HTMLButtonElement)
143
+ if (
144
+ !(x instanceof HTMLButtonElement) ||
145
+ !(y instanceof HTMLButtonElement)
146
+ ) {
147
+ return
148
+ }
149
+ expect(x.dataset['axis']).toBe('x')
150
+ expect(y.dataset['axis']).toBe('y')
151
+ expect(x.title).toBe('Drag to resize')
152
+ expect(y.title).toBe('Drag to resize')
153
+ })
154
+
155
+ test('dragging the drawer left grows it and marks data-resized', () => {
156
+ const { section, drawer } = wiringShell()
157
+ attachSlotResize({
158
+ section,
159
+ slots: [slot('wiring-inspector-slot', 'x')],
160
+ })
161
+ const handle = drawer.querySelector(`.${SLOT_RESIZE_HANDLE_CLASS}`)
162
+ expect(handle).toBeInstanceOf(HTMLButtonElement)
163
+ if (!(handle instanceof HTMLButtonElement)) return
164
+
165
+ handle.dispatchEvent(
166
+ new PointerEvent('pointerdown', {
167
+ bubbles: true,
168
+ clientX: 900,
169
+ clientY: 100,
170
+ button: 0,
171
+ pointerId: 1,
172
+ }),
173
+ )
174
+ document.dispatchEvent(
175
+ new PointerEvent('pointermove', {
176
+ bubbles: true,
177
+ clientX: 820,
178
+ clientY: 100,
179
+ pointerId: 1,
180
+ }),
181
+ )
182
+ document.dispatchEvent(
183
+ new PointerEvent('pointerup', {
184
+ bubbles: true,
185
+ clientX: 820,
186
+ clientY: 100,
187
+ pointerId: 1,
188
+ }),
189
+ )
190
+
191
+ expect(
192
+ section.style.getPropertyValue(slotSizeVar('wiring-inspector-slot')),
193
+ ).toBe('460px')
194
+ expect(section.dataset['resized']).toBe('x')
195
+ })
196
+
197
+ test('ArrowLeft on the drawer handle grows it', () => {
198
+ const { section, drawer } = wiringShell()
199
+ attachSlotResize({
200
+ section,
201
+ slots: [slot('wiring-inspector-slot', 'x')],
202
+ })
203
+ const handle = drawer.querySelector(`.${SLOT_RESIZE_HANDLE_CLASS}`)
204
+ expect(handle).toBeInstanceOf(HTMLButtonElement)
205
+ if (!(handle instanceof HTMLButtonElement)) return
206
+ handle.dispatchEvent(
207
+ new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }),
208
+ )
209
+ expect(
210
+ section.style.getPropertyValue(slotSizeVar('wiring-inspector-slot')),
211
+ ).toBe('388px')
212
+ })
213
+
214
+ test('detach removes the handles', () => {
215
+ const { section, drawer } = wiringShell()
216
+ const attached = attachSlotResize({
217
+ section,
218
+ slots: [slot('wiring-inspector-slot', 'x')],
219
+ })
220
+ attached.detach()
221
+ expect(drawer.querySelector(`.${SLOT_RESIZE_HANDLE_CLASS}`)).toBeNull()
222
+ })
223
+ })
224
+
225
+ describe('clearSlotResize', () => {
226
+ test('drops the CSS vars and data-resized so a named preset can apply', () => {
227
+ const { section } = wiringShell()
228
+ applySlotSize(section, 'wiring-inspector-slot', 'x', 500)
229
+ applySlotSize(section, 'wiring-table-slot', 'y', 240)
230
+ expect(section.dataset['resized']).toBe('x y')
231
+ clearSlotResize(section, [
232
+ slot('wiring-inspector-slot', 'x'),
233
+ slot('wiring-table-slot', 'y'),
234
+ ])
235
+ expect(
236
+ section.style.getPropertyValue(slotSizeVar('wiring-inspector-slot')),
237
+ ).toBe('')
238
+ expect(
239
+ section.style.getPropertyValue(slotSizeVar('wiring-table-slot')),
240
+ ).toBe('')
241
+ expect(section.dataset['resized']).toBeUndefined()
242
+ })
243
+ })
244
+
245
+ describe('applySlotSize', () => {
246
+ test('clamps the drawer to the prototype 300–980 band', () => {
247
+ const { section } = wiringShell()
248
+ expect(applySlotSize(section, 'wiring-inspector-slot', 'x', 12)).toBe(300)
249
+ expect(applySlotSize(section, 'wiring-inspector-slot', 'x', 2000)).toBe(980)
250
+ })
251
+ })
252
+
253
+ describe('tab-session persistence', () => {
254
+ test('stores slot id, axis, and the clamped pixel size', () => {
255
+ const storage = memoryStorage()
256
+ const { section } = wiringShell()
257
+ applySlotSize(section, 'wiring-inspector-slot', 'x', 2_000, storage)
258
+ expect(storage.getItem(slotResizeStorageKey(section.id))).toBe(
259
+ '{"version":1,"sizes":[{"slotId":"wiring-inspector-slot","axis":"x","px":980}]}',
260
+ )
261
+ })
262
+
263
+ test('a fresh section restores both axes before handles attach', () => {
264
+ const storage = memoryStorage()
265
+ const first = wiringShell()
266
+ applySlotSize(first.section, 'wiring-inspector-slot', 'x', 480, storage)
267
+ applySlotSize(first.section, 'wiring-table-slot', 'y', 252, storage)
268
+
269
+ document.body.replaceChildren()
270
+ const fresh = wiringShell()
271
+ attachSlotResize({
272
+ section: fresh.section,
273
+ slots: [
274
+ slot('wiring-inspector-slot', 'x'),
275
+ slot('wiring-table-slot', 'y'),
276
+ ],
277
+ storage,
278
+ })
279
+
280
+ expect(
281
+ fresh.section.style.getPropertyValue(
282
+ slotSizeVar('wiring-inspector-slot'),
283
+ ),
284
+ ).toBe('480px')
285
+ expect(
286
+ fresh.section.style.getPropertyValue(slotSizeVar('wiring-table-slot')),
287
+ ).toBe('252px')
288
+ expect(fresh.section.dataset['resized']).toBe('x y')
289
+ })
290
+
291
+ test('a hidden route restores its vertical size without clamping it to 46px', () => {
292
+ const storage = memoryStorage()
293
+ const first = wiringShell()
294
+ applySlotSize(first.section, 'wiring-table-slot', 'y', 252, storage)
295
+
296
+ document.body.replaceChildren()
297
+ const fresh = wiringShell()
298
+ fresh.section.hidden = true
299
+ Object.defineProperty(fresh.section, 'clientHeight', {
300
+ value: 0,
301
+ configurable: true,
302
+ })
303
+ attachSlotResize({
304
+ section: fresh.section,
305
+ slots: [slot('wiring-table-slot', 'y')],
306
+ storage,
307
+ })
308
+
309
+ expect(
310
+ fresh.section.style.getPropertyValue(slotSizeVar('wiring-table-slot')),
311
+ ).toBe('252px')
312
+ expect(storage.getItem(slotResizeStorageKey(fresh.section.id))).toBe(
313
+ '{"version":1,"sizes":[{"slotId":"wiring-table-slot","axis":"y","px":252}]}',
314
+ )
315
+ })
316
+
317
+ test('a named preset clears storage as well as the live DOM override', () => {
318
+ const storage = memoryStorage()
319
+ const first = wiringShell()
320
+ const slots = [
321
+ slot('wiring-inspector-slot', 'x'),
322
+ slot('wiring-table-slot', 'y'),
323
+ ]
324
+ applySlotSize(first.section, 'wiring-inspector-slot', 'x', 480, storage)
325
+ applySlotSize(first.section, 'wiring-table-slot', 'y', 252, storage)
326
+ clearSlotResize(first.section, slots, storage)
327
+ expect(storage.getItem(slotResizeStorageKey(first.section.id))).toBeNull()
328
+
329
+ document.body.replaceChildren()
330
+ const fresh = wiringShell()
331
+ attachSlotResize({ section: fresh.section, slots, storage })
332
+ expect(
333
+ fresh.section.style.getPropertyValue(
334
+ slotSizeVar('wiring-inspector-slot'),
335
+ ),
336
+ ).toBe('')
337
+ expect(fresh.section.dataset['resized']).toBeUndefined()
338
+ })
339
+
340
+ test('missing and malformed storage restore no override', () => {
341
+ const malformed = memoryStorage()
342
+ const first = wiringShell()
343
+ malformed.setItem(slotResizeStorageKey(first.section.id), '{not json')
344
+ expect(() =>
345
+ restoreSlotResize(
346
+ first.section,
347
+ [slot('wiring-inspector-slot', 'x')],
348
+ malformed,
349
+ ),
350
+ ).not.toThrow()
351
+ expect(
352
+ first.section.style.getPropertyValue(
353
+ slotSizeVar('wiring-inspector-slot'),
354
+ ),
355
+ ).toBe('')
356
+
357
+ expect(() =>
358
+ restoreSlotResize(
359
+ first.section,
360
+ [slot('wiring-inspector-slot', 'x')],
361
+ null,
362
+ ),
363
+ ).not.toThrow()
364
+ })
365
+
366
+ test('throwing storage never blocks live resize or preset clear', () => {
367
+ const throwing: SlotResizeStorage = {
368
+ getItem: () => {
369
+ throw new Error('denied')
370
+ },
371
+ removeItem: () => {
372
+ throw new Error('denied')
373
+ },
374
+ setItem: () => {
375
+ throw new Error('denied')
376
+ },
377
+ }
378
+ const { section } = wiringShell()
379
+ const slots = [slot('wiring-inspector-slot', 'x')]
380
+
381
+ expect(() =>
382
+ applySlotSize(section, 'wiring-inspector-slot', 'x', 500, throwing),
383
+ ).not.toThrow()
384
+ expect(
385
+ section.style.getPropertyValue(slotSizeVar('wiring-inspector-slot')),
386
+ ).toBe('500px')
387
+ expect(() => clearSlotResize(section, slots, throwing)).not.toThrow()
388
+ expect(
389
+ section.style.getPropertyValue(slotSizeVar('wiring-inspector-slot')),
390
+ ).toBe('')
391
+ })
392
+ })