@weasel-js/labkit 1.4.4 → 1.5.0
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.
- package/README.md +13 -2
- package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CqkxkFJW.d.ts} +1 -1
- package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +116 -0
- package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-8R6UmlvN.d.ts} +253 -31
- package/dist/_dts/index-B8uu2ba7.d.ts +237 -0
- package/dist/_dts/{index-Blv9uQzu.d.ts → index-retnAut7.d.ts} +18 -10
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-Ca2LCnPq.d.ts} +65 -2
- package/dist/_dts/usePanZoom-nS798rOF.d.ts +160 -0
- package/dist/_dts/useTrialState-jcx_TdU6.d.ts +212 -0
- package/dist/canvas/index.d.ts +17 -128
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +4 -92
- package/dist/chrome/index.js +9 -5
- package/dist/chunk-67SJLMC7.js +704 -0
- package/dist/chunk-67SJLMC7.js.map +1 -0
- package/dist/chunk-6ZDGOZQV.js +310 -0
- package/dist/chunk-6ZDGOZQV.js.map +1 -0
- package/dist/{chunk-MOM3GOVY.js → chunk-EKIICY6X.js} +111 -25
- package/dist/chunk-EKIICY6X.js.map +1 -0
- package/dist/{chunk-CTRKTLYZ.js → chunk-H6ZAOWNE.js} +11 -11
- package/dist/chunk-H6ZAOWNE.js.map +1 -0
- package/dist/{chunk-SMHP6XZ4.js → chunk-KJALCDWE.js} +9 -7
- package/dist/chunk-KJALCDWE.js.map +1 -0
- package/dist/{chunk-BKFVHKJH.js → chunk-MUKOW3TC.js} +3 -3
- package/dist/{chunk-BKFVHKJH.js.map → chunk-MUKOW3TC.js.map} +1 -1
- package/dist/chunk-NDRYLVOW.js +15 -0
- package/dist/chunk-NDRYLVOW.js.map +1 -0
- package/dist/{chunk-64ZCN3DA.js → chunk-NS64DXMN.js} +226 -159
- package/dist/chunk-NS64DXMN.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-PGETSEDK.js} +135 -13
- package/dist/chunk-PGETSEDK.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-QLSV2N3G.js} +32 -37
- package/dist/chunk-QLSV2N3G.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-S2SHZRD7.js +7681 -0
- package/dist/chunk-S2SHZRD7.js.map +1 -0
- package/dist/{chunk-U3IYHIAE.js → chunk-TQBBYTAF.js} +19 -82
- package/dist/chunk-TQBBYTAF.js.map +1 -0
- package/dist/chunk-UU3NJO6N.js +72 -0
- package/dist/chunk-UU3NJO6N.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ZPH5WTWF.js} +21 -15
- package/dist/chunk-ZPH5WTWF.js.map +1 -0
- package/dist/config/index.d.ts +232 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +2 -3
- package/dist/controls/index.js +3 -2
- package/dist/dragdrop/index.d.ts +2 -4
- package/dist/index.d.ts +110 -381
- package/dist/index.js +691 -817
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +3 -5
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +6 -8
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +164 -122
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +53 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +12 -7
- package/dist/state/index.js +16 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +84 -50
- package/dist/surface/index.d.ts +44 -7
- package/dist/surface/index.js +2 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +2 -4
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +53 -5
- package/src/annotations/Annotations.overlay.test.tsx +85 -6
- package/src/annotations/drawOne.test.ts +1 -1
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/paint.test.ts +2 -2
- package/src/annotations/paint.ts +1 -1
- package/src/annotations/preload.ts +6 -0
- package/src/annotations/store.test.ts +18 -0
- package/src/annotations/store.ts +5 -4
- package/src/annotations/svgNodes.test.ts +2 -2
- package/src/annotations/toolMap.ts +3 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- package/src/canvas/CanvasStack.test.tsx +25 -0
- package/src/canvas/CanvasStack.tsx +31 -4
- package/src/canvas/Stage.less +18 -0
- package/src/canvas/Stage.test.tsx +186 -0
- package/src/canvas/Stage.tsx +134 -0
- package/src/canvas/camera.test.ts +17 -2
- package/src/canvas/camera.ts +9 -9
- package/src/canvas/index.ts +4 -0
- package/src/canvas/usePanZoom.ts +10 -5
- package/src/chrome/ChromeRegions.stories.tsx +0 -3
- package/src/chrome/LabChrome.tsx +114 -0
- package/src/chrome/builtins.tsx +20 -31
- package/src/chrome/index.ts +15 -0
- package/src/chrome/labRegions.test.tsx +208 -0
- package/src/chrome/labTypes.ts +50 -0
- package/src/chrome/merge.ts +7 -9
- package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
- package/src/chrome/regions/PaletteRegion.tsx +21 -9
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +30 -16
- package/src/chrome/types.ts +47 -20
- package/src/config/builder.test.ts +4 -0
- package/src/config/builder.ts +16 -10
- package/src/config/declarationEmit.fixture.ts +11 -0
- package/src/config/declarationEmit.test.ts +51 -0
- package/src/config/entry.test.ts +46 -0
- package/src/config/index.ts +13 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/types.ts +3 -1
- package/src/controls/ControlPanel.test.tsx +16 -0
- package/src/controls/ControlPanel.tsx +5 -2
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +38 -5
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +11 -0
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +0 -2
- package/src/lab/Lab.surface.test.tsx +66 -3
- package/src/lab/Lab.test.tsx +91 -17
- package/src/lab/Lab.tsx +337 -124
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +0 -3
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -13
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabShell.less +49 -12
- package/src/lab/LabSwitcher.less +8 -1
- package/src/lab/Workspace.surface.test.tsx +2 -1
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/loupe/AGENTS.md +1 -1
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +1 -1
- package/src/loupe/useLoupe.ts +1 -1
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +13 -1
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +108 -0
- package/src/primitives/Split.tsx +159 -0
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -0
- package/src/state/Persistence.stories.tsx +75 -0
- package/src/state/Persistence.tsx +33 -0
- package/src/state/SingletonExperiment.test.tsx +47 -53
- package/src/state/SingletonExperiment.tsx +24 -13
- package/src/state/adapterContract.ts +116 -0
- package/src/state/adapters.test.ts +113 -61
- package/src/state/adapters.ts +328 -69
- package/src/state/document.test.ts +71 -96
- package/src/state/document.ts +66 -52
- package/src/state/helpers.test.ts +1 -1
- package/src/state/index.ts +8 -0
- package/src/state/labRecords.test.ts +105 -0
- package/src/state/labRecords.ts +160 -0
- package/src/state/openLabStore.test.ts +658 -0
- package/src/state/openLabStore.ts +403 -0
- package/src/state/records.test.ts +234 -0
- package/src/state/records.ts +219 -0
- package/src/state/store.test.ts +104 -635
- package/src/state/store.ts +116 -157
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +39 -15
- package/src/state/useOpenOnce.ts +68 -0
- package/src/state/usePersistedState.test.tsx +125 -0
- package/src/state/usePersistedState.ts +66 -0
- package/src/state/useTrialState.test.tsx +2 -3
- package/src/state/view.test.ts +49 -15
- package/src/state/view.ts +18 -1
- package/src/styles.less +2 -0
- package/src/surface/AGENTS.md +39 -7
- package/src/surface/SurfaceContext.ts +26 -3
- package/src/surface/index.ts +2 -0
- package/src/surface/useSurfaceTile.test.tsx +2 -1
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +175 -0
- package/src/surface/useTiledSurface.ts +54 -5
- package/src/theme/Interstellar.stories.tsx +7 -7
- package/src/theme/interstellar.test.ts +1 -1
- package/src/theme/interstellar.tokens.json +2 -2
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +47 -4
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +1 -26
- package/src/trial/Trial.stories.tsx +1 -2
- package/src/trial/Trial.test.tsx +28 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +118 -38
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +7 -4
- package/src/trial/trialOps.ts +5 -3
- package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
- package/dist/chunk-2P6PP5N4.js +0 -553
- package/dist/chunk-2P6PP5N4.js.map +0 -1
- package/dist/chunk-64ZCN3DA.js.map +0 -1
- package/dist/chunk-CTRKTLYZ.js.map +0 -1
- package/dist/chunk-FQMJUVHJ.js.map +0 -1
- package/dist/chunk-I6JCVE24.js.map +0 -1
- package/dist/chunk-MOM3GOVY.js.map +0 -1
- package/dist/chunk-SMHP6XZ4.js.map +0 -1
- package/dist/chunk-TJ7QY3OC.js.map +0 -1
- package/dist/chunk-U3IYHIAE.js.map +0 -1
- package/dist/chunk-W3ECWC2K.js +0 -7335
- package/dist/chunk-W3ECWC2K.js.map +0 -1
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { Instrument } from '../instrument/types';
|
|
3
|
+
import { createMemoryAdapter } from './adapters';
|
|
4
|
+
import { CURRENT_DOCUMENT_VERSION, labDocumentKey, quarantineKey } from './document';
|
|
5
|
+
import { labStorageKey } from './helpers';
|
|
6
|
+
import { labPrefix, recordsOfDocument } from './labRecords';
|
|
7
|
+
import { type OpenLabStoreOptions, openLabStore } from './openLabStore';
|
|
8
|
+
import type { LabDocument, StorageAdapter, TrialRecord } from './types';
|
|
9
|
+
|
|
10
|
+
const VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
|
|
11
|
+
const P = labPrefix('test');
|
|
12
|
+
|
|
13
|
+
function trial(id: string, extra: Record<string, unknown> = {}) {
|
|
14
|
+
return { id, instrumentName: 'T', config: {}, state: {}, view: VIEW, ...extra };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function open(backing: Map<string, unknown>, overrides: Partial<OpenLabStoreOptions> = {}) {
|
|
18
|
+
return openLabStore({ storageKey: 'test', storage: createMemoryAdapter(backing), ...overrides });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Lab records as a current labkit writes them. */
|
|
22
|
+
function storeLab(backing: Map<string, unknown>, doc: Partial<LabDocument>, storageKey = 'test') {
|
|
23
|
+
const full: LabDocument = {
|
|
24
|
+
version: CURRENT_DOCUMENT_VERSION,
|
|
25
|
+
trials: [],
|
|
26
|
+
saves: [],
|
|
27
|
+
layout: {},
|
|
28
|
+
undockedPanels: {},
|
|
29
|
+
mode: 'auto',
|
|
30
|
+
...doc,
|
|
31
|
+
};
|
|
32
|
+
for (const [name, value] of recordsOfDocument(full)) {
|
|
33
|
+
backing.set(labPrefix(storageKey) + name, structuredClone(value));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A version-3 document, as the single-document labkit stored one. */
|
|
38
|
+
function storeV3(backing: Map<string, unknown>, doc: Record<string, unknown>) {
|
|
39
|
+
backing.set(labDocumentKey('test'), JSON.stringify({ version: 3, ...doc }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Let the memory adapter's queued notifications reach the other tab. */
|
|
43
|
+
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
44
|
+
|
|
45
|
+
const SNAPSHOT = {
|
|
46
|
+
id: 's1',
|
|
47
|
+
name: 'saved',
|
|
48
|
+
trialId: 'w1',
|
|
49
|
+
instrumentName: 'T',
|
|
50
|
+
config: { x: 1 },
|
|
51
|
+
state: { n: 5 },
|
|
52
|
+
savedAt: 1000,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
vi.restoreAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('openLabStore — round trips', () => {
|
|
60
|
+
it('reopens a trial it stored, with an empty undo history', async () => {
|
|
61
|
+
const backing = new Map<string, unknown>();
|
|
62
|
+
const first = await open(backing);
|
|
63
|
+
first.store.getState().addTrial(trial('w1', { state: { n: 7 } }));
|
|
64
|
+
first.store.getState().updateTrialUndoStack('w1', { past: [{ n: 6 }], future: [] });
|
|
65
|
+
await first.close();
|
|
66
|
+
|
|
67
|
+
const again = await open(backing);
|
|
68
|
+
expect(again.store.getState().trials).toHaveLength(1);
|
|
69
|
+
expect(again.store.getState().trials[0]).toMatchObject({ id: 'w1', state: { n: 7 } });
|
|
70
|
+
expect(again.store.getState().trials[0]?.undoStack).toEqual({ past: [], future: [] });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// An instrument holding a Map, a Set or anything else storage cannot keep
|
|
74
|
+
// needs these at both ends.
|
|
75
|
+
it("runs an instrument's serializers when writing and when reading", async () => {
|
|
76
|
+
const backing = new Map<string, unknown>();
|
|
77
|
+
const serializers = {
|
|
78
|
+
T: {
|
|
79
|
+
serialize: (state: unknown) => [...(state as Map<string, number>)],
|
|
80
|
+
deserialize: (data: unknown) => new Map(data as [string, number][]),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const first = await open(backing, { serializers });
|
|
84
|
+
first.store.getState().addTrial(trial('w1', { state: new Map([['a', 1]]) }));
|
|
85
|
+
await first.close();
|
|
86
|
+
expect((backing.get(`${P}trial:w1`) as { state: unknown }).state).toEqual([['a', 1]]);
|
|
87
|
+
|
|
88
|
+
const again = await open(backing, { serializers });
|
|
89
|
+
expect(again.store.getState().trials[0]?.state).toEqual(new Map([['a', 1]]));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('hands `deserialize` the config the state was saved against', async () => {
|
|
93
|
+
const backing = new Map<string, unknown>();
|
|
94
|
+
const first = await open(backing);
|
|
95
|
+
first.store.getState().addTrial(trial('w1', { config: { scale: 4 }, state: { n: 1 } }));
|
|
96
|
+
await first.close();
|
|
97
|
+
|
|
98
|
+
const deserialize = vi.fn((data: unknown) => data);
|
|
99
|
+
await open(backing, { serializers: { T: { deserialize } } });
|
|
100
|
+
expect(deserialize).toHaveBeenCalledWith({ n: 1 }, { scale: 4 });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('keeps a title and folded sections', async () => {
|
|
104
|
+
const backing = new Map<string, unknown>();
|
|
105
|
+
const first = await open(backing);
|
|
106
|
+
first.store.getState().addTrial(trial('w1'));
|
|
107
|
+
first.store.getState().setTrialTitle('w1', 'Sprocket 7');
|
|
108
|
+
first.store.getState().setTrialSectionCollapsed('w1', 'settings/Shape', true);
|
|
109
|
+
await first.close();
|
|
110
|
+
|
|
111
|
+
const t = (await open(backing)).store.getState().trials[0];
|
|
112
|
+
expect(t?.title).toBe('Sprocket 7');
|
|
113
|
+
expect(t?.collapsedSections).toEqual({ 'settings/Shape': true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('keeps saves, layout, undocked panels and mode', async () => {
|
|
117
|
+
const backing = new Map<string, unknown>();
|
|
118
|
+
const first = await open(backing);
|
|
119
|
+
first.store.getState().addTrial(trial('w1'));
|
|
120
|
+
first.store.getState().saveSnapshot('w1', 'first');
|
|
121
|
+
first.store.getState().setLayout({ w1: { h: 9 } });
|
|
122
|
+
first.store.getState().undockPanel('w1', 'settings', 'floating');
|
|
123
|
+
first.store.getState().setMode('light');
|
|
124
|
+
await first.close();
|
|
125
|
+
|
|
126
|
+
const s = (await open(backing)).store.getState();
|
|
127
|
+
expect(s.savedSnapshots.map((sn) => sn.name)).toEqual(['first']);
|
|
128
|
+
expect(s.layout).toEqual({ w1: { h: 9 } });
|
|
129
|
+
expect(Object.values(s.undockedPanels)).toEqual([
|
|
130
|
+
{ trialId: 'w1', sectionId: 'settings', as: 'floating' },
|
|
131
|
+
]);
|
|
132
|
+
expect(s.mode).toBe('light');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('persists a bare store.setState too', async () => {
|
|
136
|
+
const backing = new Map<string, unknown>();
|
|
137
|
+
const first = await open(backing);
|
|
138
|
+
first.store.setState({ layout: { direct: true } });
|
|
139
|
+
await first.close();
|
|
140
|
+
expect((await open(backing)).store.getState().layout).toEqual({ direct: true });
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('openLabStore — config defaults', () => {
|
|
145
|
+
const stored = (config: unknown) => ({
|
|
146
|
+
trials: [trial('w1', { config })],
|
|
147
|
+
saves: [{ ...SNAPSHOT, config }],
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('fills a branch a config stored before the schema nested it never had', async () => {
|
|
151
|
+
const backing = new Map<string, unknown>();
|
|
152
|
+
storeLab(backing, stored({ showGrid: false, gridSize: 40 }));
|
|
153
|
+
const { store } = await open(backing, {
|
|
154
|
+
configDefaults: { T: () => ({ showGrid: true, grid: { size: 20, color: '#fff' } }) },
|
|
155
|
+
});
|
|
156
|
+
expect(store.getState().trials[0]?.config).toEqual({
|
|
157
|
+
showGrid: false,
|
|
158
|
+
gridSize: 40,
|
|
159
|
+
grid: { size: 20, color: '#fff' },
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('fills the config on a saved snapshot the same way', async () => {
|
|
164
|
+
const backing = new Map<string, unknown>();
|
|
165
|
+
storeLab(backing, stored({ grid: { size: 40 } }));
|
|
166
|
+
const { store } = await open(backing, {
|
|
167
|
+
configDefaults: { T: () => ({ grid: { size: 20, color: '#fff' } }) },
|
|
168
|
+
});
|
|
169
|
+
expect(store.getState().savedSnapshots[0]?.config).toEqual({
|
|
170
|
+
grid: { size: 40, color: '#fff' },
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('hands a deserializer the filled config, not the stored one', async () => {
|
|
175
|
+
const backing = new Map<string, unknown>();
|
|
176
|
+
storeLab(backing, stored({ grid: { size: 40 } }));
|
|
177
|
+
const seen: unknown[] = [];
|
|
178
|
+
await open(backing, {
|
|
179
|
+
configDefaults: { T: () => ({ grid: { size: 20, color: '#fff' } }) },
|
|
180
|
+
serializers: {
|
|
181
|
+
T: {
|
|
182
|
+
deserialize: (state, config) => {
|
|
183
|
+
seen.push(config);
|
|
184
|
+
return state;
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
expect(seen).toEqual([{ grid: { size: 40, color: '#fff' } }]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('leaves a config alone when no defaults are registered for its instrument', async () => {
|
|
193
|
+
const backing = new Map<string, unknown>();
|
|
194
|
+
storeLab(backing, stored({ only: 1 }));
|
|
195
|
+
expect((await open(backing)).store.getState().trials[0]?.config).toEqual({ only: 1 });
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('openLabStore — config migrations', () => {
|
|
200
|
+
const stored = (config: unknown) => ({
|
|
201
|
+
trials: [trial('w1', { config })],
|
|
202
|
+
saves: [{ ...SNAPSHOT, config }],
|
|
203
|
+
});
|
|
204
|
+
const defaults = { T: () => ({ grid: { size: 20, color: '#fff' } }) };
|
|
205
|
+
const moveGridSize = (config: unknown) => {
|
|
206
|
+
const { gridSize, ...rest } = config as Record<string, unknown>;
|
|
207
|
+
if (gridSize === undefined) return config;
|
|
208
|
+
return { ...rest, grid: { ...(rest.grid as object | undefined), size: gridSize } };
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
it('moves a renamed value before the defaults fill the gaps', async () => {
|
|
212
|
+
const backing = new Map<string, unknown>();
|
|
213
|
+
storeLab(backing, stored({ gridSize: 40 }));
|
|
214
|
+
const { store } = await open(backing, {
|
|
215
|
+
configDefaults: defaults,
|
|
216
|
+
configMigrations: { T: moveGridSize },
|
|
217
|
+
});
|
|
218
|
+
expect(store.getState().trials[0]?.config).toEqual({ grid: { size: 40, color: '#fff' } });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('moves the config on a saved snapshot the same way', async () => {
|
|
222
|
+
const backing = new Map<string, unknown>();
|
|
223
|
+
storeLab(backing, stored({ gridSize: 40 }));
|
|
224
|
+
const { store } = await open(backing, {
|
|
225
|
+
configDefaults: defaults,
|
|
226
|
+
configMigrations: { T: moveGridSize },
|
|
227
|
+
});
|
|
228
|
+
expect(store.getState().savedSnapshots[0]?.config).toEqual({
|
|
229
|
+
grid: { size: 40, color: '#fff' },
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('hands a deserializer the moved config', async () => {
|
|
234
|
+
const backing = new Map<string, unknown>();
|
|
235
|
+
storeLab(backing, stored({ gridSize: 40 }));
|
|
236
|
+
const seen: unknown[] = [];
|
|
237
|
+
await open(backing, {
|
|
238
|
+
configDefaults: defaults,
|
|
239
|
+
configMigrations: { T: moveGridSize },
|
|
240
|
+
serializers: {
|
|
241
|
+
T: {
|
|
242
|
+
deserialize: (state, config) => {
|
|
243
|
+
seen.push(config);
|
|
244
|
+
return state;
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
expect(seen).toEqual([{ grid: { size: 40, color: '#fff' } }]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('moves a trial another writer stores', async () => {
|
|
253
|
+
const backing = new Map<string, unknown>();
|
|
254
|
+
const options = { configDefaults: defaults, configMigrations: { T: moveGridSize } };
|
|
255
|
+
const mine = await open(backing, options);
|
|
256
|
+
const theirs = await open(backing, options);
|
|
257
|
+
theirs.store.getState().addTrial(trial('w2', { config: { gridSize: 7 } }));
|
|
258
|
+
await theirs.close();
|
|
259
|
+
await tick();
|
|
260
|
+
expect(mine.store.getState().trials.find((t) => t.id === 'w2')?.config).toEqual({
|
|
261
|
+
grid: { size: 7, color: '#fff' },
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('openLabStore — instruments set after opening', () => {
|
|
267
|
+
const plain: Instrument = {
|
|
268
|
+
name: 'T',
|
|
269
|
+
defaultConfig: () => ({}),
|
|
270
|
+
initialState: () => ({}),
|
|
271
|
+
render: () => null,
|
|
272
|
+
};
|
|
273
|
+
const mapped: Instrument = {
|
|
274
|
+
...plain,
|
|
275
|
+
name: 'Mapped',
|
|
276
|
+
serialize: (s) => [...(s as Set<string>)],
|
|
277
|
+
deserialize: (data) => new Set(data as string[]),
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
it("writes and reads a trial through an added instrument's serializers", async () => {
|
|
281
|
+
const backing = new Map<string, unknown>();
|
|
282
|
+
const opened = await open(backing, { instruments: [plain] });
|
|
283
|
+
opened.store.getState().setInstruments([plain, mapped]);
|
|
284
|
+
|
|
285
|
+
opened.store
|
|
286
|
+
.getState()
|
|
287
|
+
.addTrial(trial('m', { instrumentName: 'Mapped', state: new Set(['a']) }));
|
|
288
|
+
await opened.records.flush();
|
|
289
|
+
expect((backing.get(`${P}trial:m`) as { state: unknown }).state).toEqual(['a']);
|
|
290
|
+
|
|
291
|
+
await createMemoryAdapter(backing).set(`${P}trial:r`, {
|
|
292
|
+
...trial('r', { instrumentName: 'Mapped', state: ['b'] }),
|
|
293
|
+
order: 1,
|
|
294
|
+
});
|
|
295
|
+
await tick();
|
|
296
|
+
expect(opened.store.getState().trials.find((t) => t.id === 'r')?.state).toEqual(new Set(['b']));
|
|
297
|
+
await opened.close();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('openLabStore — what a change writes', () => {
|
|
302
|
+
function counting(backing: Map<string, unknown>) {
|
|
303
|
+
const storage = createMemoryAdapter(backing);
|
|
304
|
+
const set = vi.spyOn(storage, 'set');
|
|
305
|
+
const del = vi.spyOn(storage, 'delete');
|
|
306
|
+
return { storage, set, del };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
it('writes only the trial that changed', async () => {
|
|
310
|
+
const backing = new Map<string, unknown>();
|
|
311
|
+
storeLab(backing, { trials: [trial('a'), trial('b')] });
|
|
312
|
+
const { storage, set } = counting(backing);
|
|
313
|
+
const opened = await openLabStore({ storageKey: 'test', storage });
|
|
314
|
+
opened.store.getState().updateTrialState('b', { n: 1 });
|
|
315
|
+
await opened.close();
|
|
316
|
+
expect(set.mock.calls.map(([key]) => key)).toEqual([`${P}trial:b`]);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('writes nothing for a change to undo history alone', async () => {
|
|
320
|
+
const backing = new Map<string, unknown>();
|
|
321
|
+
storeLab(backing, { trials: [trial('a')] });
|
|
322
|
+
const { storage, set } = counting(backing);
|
|
323
|
+
const opened = await openLabStore({ storageKey: 'test', storage });
|
|
324
|
+
opened.store.getState().updateTrialUndoStack('a', { past: [{}], future: [] });
|
|
325
|
+
await opened.close();
|
|
326
|
+
expect(set).not.toHaveBeenCalled();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("deletes a removed trial's record and every value scoped to it", async () => {
|
|
330
|
+
const backing = new Map<string, unknown>();
|
|
331
|
+
storeLab(backing, { trials: [trial('a'), trial('b')] });
|
|
332
|
+
backing.set(`${P}value:trial:a:tab`, 'shape');
|
|
333
|
+
backing.set(`${P}value:trial:b:tab`, 'color');
|
|
334
|
+
const opened = await open(backing);
|
|
335
|
+
opened.store.getState().removeTrial('a');
|
|
336
|
+
await opened.close();
|
|
337
|
+
expect(backing.has(`${P}trial:a`)).toBe(false);
|
|
338
|
+
expect(backing.has(`${P}value:trial:a:tab`)).toBe(false);
|
|
339
|
+
expect(backing.get(`${P}value:trial:b:tab`)).toBe('color');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('writes and deletes one record per snapshot', async () => {
|
|
343
|
+
const backing = new Map<string, unknown>();
|
|
344
|
+
storeLab(backing, { trials: [trial('w1')], saves: [SNAPSHOT] });
|
|
345
|
+
const opened = await open(backing);
|
|
346
|
+
opened.store.getState().deleteSnapshot('s1');
|
|
347
|
+
await opened.close();
|
|
348
|
+
expect(backing.has(`${P}save:s1`)).toBe(false);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('writes a meta record with the first change to a new lab', async () => {
|
|
352
|
+
const backing = new Map<string, unknown>();
|
|
353
|
+
const opened = await open(backing);
|
|
354
|
+
opened.store.getState().setLayout({ a: 1 });
|
|
355
|
+
await opened.close();
|
|
356
|
+
expect(backing.get(`${P}meta`)).toEqual({ version: CURRENT_DOCUMENT_VERSION, mode: 'auto' });
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('writes nothing for a lab opened and closed untouched', async () => {
|
|
360
|
+
const backing = new Map<string, unknown>();
|
|
361
|
+
await (await open(backing)).close();
|
|
362
|
+
expect(backing.size).toBe(0);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('openLabStore — trial order', () => {
|
|
367
|
+
it('keeps a reorder across a reload', async () => {
|
|
368
|
+
const backing = new Map<string, unknown>();
|
|
369
|
+
storeLab(backing, { trials: [trial('a'), trial('b'), trial('c')] });
|
|
370
|
+
const opened = await open(backing);
|
|
371
|
+
const [a, b, c] = opened.store.getState().trials as [TrialRecord, TrialRecord, TrialRecord];
|
|
372
|
+
opened.store.setState({ trials: [c, a, b] });
|
|
373
|
+
await opened.close();
|
|
374
|
+
const ids = (await open(backing)).store.getState().trials.map((t) => t.id);
|
|
375
|
+
expect(ids).toEqual(['c', 'a', 'b']);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('keeps a trial inserted mid-list where it was put', async () => {
|
|
379
|
+
const backing = new Map<string, unknown>();
|
|
380
|
+
storeLab(backing, { trials: [trial('a'), trial('c')] });
|
|
381
|
+
const opened = await open(backing);
|
|
382
|
+
const [a, c] = opened.store.getState().trials as [TrialRecord, TrialRecord];
|
|
383
|
+
const b: TrialRecord = { ...trial('b'), undoStack: { past: [], future: [] } };
|
|
384
|
+
opened.store.setState({ trials: [a, b, c] });
|
|
385
|
+
await opened.close();
|
|
386
|
+
const ids = (await open(backing)).store.getState().trials.map((t) => t.id);
|
|
387
|
+
expect(ids).toEqual(['a', 'b', 'c']);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('openLabStore — two tabs', () => {
|
|
392
|
+
it('keeps a trial each tab added at the same time, in the same order everywhere', async () => {
|
|
393
|
+
const backing = new Map<string, unknown>();
|
|
394
|
+
const tabA = await open(backing);
|
|
395
|
+
const tabB = await open(backing);
|
|
396
|
+
tabB.store.getState().addTrial(trial('b1'));
|
|
397
|
+
tabA.store.getState().addTrial(trial('a1'));
|
|
398
|
+
await Promise.all([tabA.records.flush(), tabB.records.flush()]);
|
|
399
|
+
await tick();
|
|
400
|
+
|
|
401
|
+
const order = (s: typeof tabA) => s.store.getState().trials.map((t) => t.id);
|
|
402
|
+
expect(order(tabA)).toEqual(['a1', 'b1']);
|
|
403
|
+
expect(order(tabB)).toEqual(['a1', 'b1']);
|
|
404
|
+
expect(order(await open(backing))).toEqual(['a1', 'b1']);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("applies another tab's edit to a trial, and clears only that trial's undo history", async () => {
|
|
408
|
+
const backing = new Map<string, unknown>();
|
|
409
|
+
storeLab(backing, { trials: [trial('t1', { state: { n: 0 } }), trial('t2')] });
|
|
410
|
+
const tabA = await open(backing);
|
|
411
|
+
const tabB = await open(backing);
|
|
412
|
+
tabA.store.getState().updateTrialUndoStack('t1', { past: [{ n: -1 }], future: [] });
|
|
413
|
+
tabA.store.getState().updateTrialUndoStack('t2', { past: [{ n: -2 }], future: [] });
|
|
414
|
+
|
|
415
|
+
tabB.store.getState().updateTrialState('t1', { n: 5 });
|
|
416
|
+
await tabB.records.flush();
|
|
417
|
+
await tick();
|
|
418
|
+
|
|
419
|
+
const [t1, t2] = tabA.store.getState().trials;
|
|
420
|
+
expect(t1?.state).toEqual({ n: 5 });
|
|
421
|
+
expect(t1?.undoStack).toEqual({ past: [], future: [] });
|
|
422
|
+
expect(t2?.undoStack).toEqual({ past: [{ n: -2 }], future: [] });
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("applies another tab's delete, snapshot, layout and mode without writing them back", async () => {
|
|
426
|
+
const backing = new Map<string, unknown>();
|
|
427
|
+
storeLab(backing, { trials: [trial('t1'), trial('t2')] });
|
|
428
|
+
const storageA = createMemoryAdapter(backing);
|
|
429
|
+
const setA = vi.spyOn(storageA, 'set');
|
|
430
|
+
const tabA = await openLabStore({ storageKey: 'test', storage: storageA });
|
|
431
|
+
const tabB = await open(backing);
|
|
432
|
+
|
|
433
|
+
tabB.store.getState().removeTrial('t2');
|
|
434
|
+
tabB.store.getState().saveSnapshot('t1', 'from b');
|
|
435
|
+
tabB.store.getState().setLayout({ t1: { h: 2 } });
|
|
436
|
+
tabB.store.getState().setMode('dark');
|
|
437
|
+
await tabB.records.flush();
|
|
438
|
+
await tick();
|
|
439
|
+
|
|
440
|
+
const s = tabA.store.getState();
|
|
441
|
+
expect(s.trials.map((t) => t.id)).toEqual(['t1']);
|
|
442
|
+
expect(s.savedSnapshots.map((sn) => sn.name)).toEqual(['from b']);
|
|
443
|
+
expect(s.layout).toEqual({ t1: { h: 2 } });
|
|
444
|
+
expect(s.mode).toBe('dark');
|
|
445
|
+
await tabA.records.flush();
|
|
446
|
+
expect(setA).not.toHaveBeenCalled();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('stops writing once another tab stores the lab with a newer labkit', async () => {
|
|
450
|
+
const backing = new Map<string, unknown>();
|
|
451
|
+
const tabA = await open(backing);
|
|
452
|
+
const newer = createMemoryAdapter(backing);
|
|
453
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
454
|
+
await newer.set(`${P}meta`, { version: CURRENT_DOCUMENT_VERSION + 1, mode: 'auto' });
|
|
455
|
+
await tick();
|
|
456
|
+
tabA.store.getState().setLayout({ mine: true });
|
|
457
|
+
await tabA.close();
|
|
458
|
+
expect(backing.has(`${P}layout`)).toBe(false);
|
|
459
|
+
expect(warn).toHaveBeenCalled();
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
describe('openLabStore — records it cannot use', () => {
|
|
464
|
+
it('opens records from a newer labkit empty, and never writes over them', async () => {
|
|
465
|
+
const backing = new Map<string, unknown>();
|
|
466
|
+
storeLab(backing, { trials: [trial('w1')] });
|
|
467
|
+
backing.set(`${P}meta`, { version: 999, mode: 'auto' });
|
|
468
|
+
const before = new Map(backing);
|
|
469
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
470
|
+
|
|
471
|
+
const opened = await open(backing);
|
|
472
|
+
expect(opened.store.getState().trials).toEqual([]);
|
|
473
|
+
opened.store.getState().setMode('light');
|
|
474
|
+
await opened.close();
|
|
475
|
+
expect(backing).toEqual(before);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('ignores a record whose name no lab writes', async () => {
|
|
479
|
+
const backing = new Map<string, unknown>();
|
|
480
|
+
storeLab(backing, { trials: [trial('w1')] });
|
|
481
|
+
backing.set(`${P}trial:w1:extra`, { id: 'nope' });
|
|
482
|
+
backing.set(`${P}bogus`, 1);
|
|
483
|
+
expect((await open(backing)).store.getState().trials.map((t) => t.id)).toEqual(['w1']);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("never reads another lab's records, even one whose name extends its own", async () => {
|
|
487
|
+
const backing = new Map<string, unknown>();
|
|
488
|
+
storeLab(backing, { trials: [trial('sibling')] }, 'a:trial:x');
|
|
489
|
+
storeLab(backing, { trials: [trial('mine')] }, 'a');
|
|
490
|
+
const opened = await openLabStore({ storageKey: 'a', storage: createMemoryAdapter(backing) });
|
|
491
|
+
expect(opened.store.getState().trials.map((t) => t.id)).toEqual(['mine']);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe('openLabStore — folding a version-3 document', () => {
|
|
496
|
+
it('opens it, writes it as records, and deletes it only after they read back', async () => {
|
|
497
|
+
const backing = new Map<string, unknown>();
|
|
498
|
+
storeV3(backing, {
|
|
499
|
+
trials: [trial('w1', { view: { zoom: 2, pan: { x: 0, y: 0 } } })],
|
|
500
|
+
saves: [SNAPSHOT],
|
|
501
|
+
layout: { w1: { h: 3 } },
|
|
502
|
+
undockedPanels: {},
|
|
503
|
+
mode: 'dark',
|
|
504
|
+
});
|
|
505
|
+
const opened = await open(backing);
|
|
506
|
+
const s = opened.store.getState();
|
|
507
|
+
expect(s.trials.map((t) => t.id)).toEqual(['w1']);
|
|
508
|
+
expect(s.savedSnapshots).toHaveLength(1);
|
|
509
|
+
expect(s.layout).toEqual({ w1: { h: 3 } });
|
|
510
|
+
expect(s.mode).toBe('dark');
|
|
511
|
+
expect(backing.has(labDocumentKey('test'))).toBe(false);
|
|
512
|
+
expect(backing.get(`${P}meta`)).toEqual({ version: CURRENT_DOCUMENT_VERSION, mode: 'dark' });
|
|
513
|
+
expect(backing.has(`${P}trial:w1`)).toBe(true);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('opens a version-1 document, whose tiles were still called workspaces', async () => {
|
|
517
|
+
const backing = new Map<string, unknown>();
|
|
518
|
+
backing.set(
|
|
519
|
+
labDocumentKey('test'),
|
|
520
|
+
JSON.stringify({
|
|
521
|
+
version: 1,
|
|
522
|
+
workspaces: [trial('w1', { config: { gain: 3 }, state: { n: 7 } })],
|
|
523
|
+
saves: [{ id: 's1', name: 'a save', trialId: 'w1' }],
|
|
524
|
+
layout: { w1: { h: 3 } },
|
|
525
|
+
mode: 'dark',
|
|
526
|
+
}),
|
|
527
|
+
);
|
|
528
|
+
const s = (await open(backing)).store.getState();
|
|
529
|
+
expect(s.trials[0]).toMatchObject({ id: 'w1', config: { gain: 3 }, state: { n: 7 } });
|
|
530
|
+
expect(s.savedSnapshots).toHaveLength(1);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('keeps the old document when the records do not read back', async () => {
|
|
534
|
+
const backing = new Map<string, unknown>();
|
|
535
|
+
storeV3(backing, { trials: [trial('w1')], saves: [], layout: {}, mode: 'auto' });
|
|
536
|
+
const memory = createMemoryAdapter(backing);
|
|
537
|
+
const dropsMeta: StorageAdapter = {
|
|
538
|
+
...memory,
|
|
539
|
+
set: async (key, value) => {
|
|
540
|
+
if (key === `${P}meta`) return;
|
|
541
|
+
await memory.set(key, value);
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
545
|
+
const opened = await openLabStore({ storageKey: 'test', storage: dropsMeta });
|
|
546
|
+
expect(opened.store.getState().trials.map((t) => t.id)).toEqual(['w1']);
|
|
547
|
+
expect(backing.has(labDocumentKey('test'))).toBe(true);
|
|
548
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('keeping the old storage'));
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('fills whatever an incomplete document is missing', async () => {
|
|
552
|
+
const backing = new Map<string, unknown>();
|
|
553
|
+
storeV3(backing, {});
|
|
554
|
+
const s = (await open(backing, { initialMode: 'light' })).store.getState();
|
|
555
|
+
expect(s.trials).toEqual([]);
|
|
556
|
+
expect(s.savedSnapshots).toEqual([]);
|
|
557
|
+
expect(s.layout).toEqual({});
|
|
558
|
+
expect(s.mode).toBe('light');
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('opens a trial stored before titles and folds existed', async () => {
|
|
562
|
+
const backing = new Map<string, unknown>();
|
|
563
|
+
storeV3(backing, { trials: [trial('w1')], saves: [], layout: {}, mode: 'auto' });
|
|
564
|
+
const t = (await open(backing)).store.getState().trials[0];
|
|
565
|
+
expect(t?.title).toBeUndefined();
|
|
566
|
+
expect(t?.collapsedSections).toBeUndefined();
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('opens a document from a newer labkit empty, and leaves it alone', async () => {
|
|
570
|
+
const backing = new Map<string, unknown>();
|
|
571
|
+
const future = JSON.stringify({ version: 999, trials: [trial('w1')] });
|
|
572
|
+
backing.set(labDocumentKey('test'), future);
|
|
573
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
574
|
+
const opened = await open(backing);
|
|
575
|
+
expect(opened.store.getState().trials).toEqual([]);
|
|
576
|
+
opened.store.getState().setMode('light');
|
|
577
|
+
await opened.close();
|
|
578
|
+
expect([...backing]).toEqual([[labDocumentKey('test'), future]]);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('quarantines an unparseable document, deletes it, and persists again', async () => {
|
|
582
|
+
const backing = new Map<string, unknown>();
|
|
583
|
+
backing.set(labDocumentKey('test'), '{{{not json');
|
|
584
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
585
|
+
const opened = await open(backing);
|
|
586
|
+
expect(opened.store.getState().trials).toEqual([]);
|
|
587
|
+
opened.store.getState().setMode('light');
|
|
588
|
+
await opened.close();
|
|
589
|
+
expect(backing.get(quarantineKey('test'))).toBe('{{{not json');
|
|
590
|
+
expect(backing.has(labDocumentKey('test'))).toBe(false);
|
|
591
|
+
expect(backing.get(`${P}meta`)).toEqual({ version: CURRENT_DOCUMENT_VERSION, mode: 'light' });
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('leaves the original alone and stops persisting when the quarantine copy will not land', async () => {
|
|
595
|
+
const backing = new Map<string, unknown>();
|
|
596
|
+
backing.set(labDocumentKey('test'), '{{{not json');
|
|
597
|
+
const memory = createMemoryAdapter(backing);
|
|
598
|
+
const dropsQuarantine: StorageAdapter = {
|
|
599
|
+
...memory,
|
|
600
|
+
set: async (key, value) => {
|
|
601
|
+
if (key === quarantineKey('test')) return;
|
|
602
|
+
await memory.set(key, value);
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
606
|
+
const opened = await openLabStore({ storageKey: 'test', storage: dropsQuarantine });
|
|
607
|
+
opened.store.getState().setMode('light');
|
|
608
|
+
await opened.close();
|
|
609
|
+
expect([...backing]).toEqual([[labDocumentKey('test'), '{{{not json']]);
|
|
610
|
+
expect(warn).toHaveBeenCalledWith(
|
|
611
|
+
expect.stringContaining('could not be quarantined'),
|
|
612
|
+
undefined,
|
|
613
|
+
);
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
describe('openLabStore — folding the four pre-document keys', () => {
|
|
618
|
+
it('folds them, turning the old interstellar theme into dark, and deletes them', async () => {
|
|
619
|
+
const backing = new Map<string, unknown>();
|
|
620
|
+
backing.set(labStorageKey('test', 'workspaces'), JSON.stringify([trial('w1')]));
|
|
621
|
+
backing.set(labStorageKey('test', 'theme'), 'interstellar');
|
|
622
|
+
const s = (await open(backing)).store.getState();
|
|
623
|
+
expect(s.mode).toBe('dark');
|
|
624
|
+
expect(s.trials.map((t) => t.id)).toEqual(['w1']);
|
|
625
|
+
expect(backing.has(labStorageKey('test', 'workspaces'))).toBe(false);
|
|
626
|
+
expect(backing.has(labStorageKey('test', 'theme'))).toBe(false);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('honors initialMode when the old lab never set a theme', async () => {
|
|
630
|
+
const backing = new Map<string, unknown>();
|
|
631
|
+
backing.set(labStorageKey('test', 'workspaces'), JSON.stringify([]));
|
|
632
|
+
expect((await open(backing, { initialMode: 'dark' })).store.getState().mode).toBe('dark');
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("folds lab a without touching lab a:saves, whose old document sat where a's saves bucket did", async () => {
|
|
636
|
+
const backing = new Map<string, unknown>();
|
|
637
|
+
const sibling = await openLabStore({
|
|
638
|
+
storageKey: 'a:saves',
|
|
639
|
+
storage: createMemoryAdapter(backing),
|
|
640
|
+
});
|
|
641
|
+
sibling.store.getState().addTrial(trial('sw'));
|
|
642
|
+
await sibling.close();
|
|
643
|
+
backing.set(labStorageKey('a', 'workspaces'), JSON.stringify([]));
|
|
644
|
+
backing.set(labStorageKey('a', 'theme'), 'dark');
|
|
645
|
+
|
|
646
|
+
const lab = await openLabStore({ storageKey: 'a', storage: createMemoryAdapter(backing) });
|
|
647
|
+
expect(lab.store.getState().savedSnapshots).toEqual([]);
|
|
648
|
+
expect(lab.store.getState().trials).toEqual([]);
|
|
649
|
+
await lab.close();
|
|
650
|
+
|
|
651
|
+
expect(backing.has(labStorageKey('a', 'workspaces'))).toBe(false);
|
|
652
|
+
const again = await openLabStore({
|
|
653
|
+
storageKey: 'a:saves',
|
|
654
|
+
storage: createMemoryAdapter(backing),
|
|
655
|
+
});
|
|
656
|
+
expect(again.store.getState().trials.map((t) => t.id)).toEqual(['sw']);
|
|
657
|
+
});
|
|
658
|
+
});
|