@weasel-js/labkit 1.4.4 → 1.5.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.
- package/README.md +54 -6
- package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CJNMS6mz.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-Gwr8fLpK.d.ts +453 -0
- package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-B4okC2lX.d.ts} +291 -31
- package/dist/_dts/{index-Blv9uQzu.d.ts → index-BNdz-tNd.d.ts} +105 -17
- package/dist/_dts/index-CSJoj4LC.d.ts +248 -0
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-DzoNY1pO.d.ts} +91 -4
- package/dist/_dts/usePanZoom-Pzf9f2jh.d.ts +160 -0
- package/dist/_dts/useTrialState-uLlmaccn.d.ts +212 -0
- package/dist/canvas/index.d.ts +18 -129
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +5 -93
- package/dist/chrome/index.js +10 -6
- package/dist/chunk-7PFH3CNM.js +150 -0
- package/dist/chunk-7PFH3CNM.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-CEIUBPPW.js} +143 -17
- package/dist/chunk-CEIUBPPW.js.map +1 -0
- package/dist/chunk-EH7UEQV7.js +590 -0
- package/dist/chunk-EH7UEQV7.js.map +1 -0
- package/dist/{chunk-MOM3GOVY.js → chunk-EHGVROVV.js} +125 -27
- package/dist/chunk-EHGVROVV.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ERFMC6WU.js} +160 -78
- package/dist/chunk-ERFMC6WU.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-64ZCN3DA.js → chunk-IHUVQ3TX.js} +250 -160
- package/dist/chunk-IHUVQ3TX.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-IJJUK4FU.js} +53 -46
- package/dist/chunk-IJJUK4FU.js.map +1 -0
- package/dist/chunk-IXTQI7JX.js +737 -0
- package/dist/chunk-IXTQI7JX.js.map +1 -0
- package/dist/{chunk-BKFVHKJH.js → chunk-MDVTK5U3.js} +4 -4
- package/dist/{chunk-BKFVHKJH.js.map → chunk-MDVTK5U3.js.map} +1 -1
- package/dist/chunk-PHK4W562.js +72 -0
- package/dist/chunk-PHK4W562.js.map +1 -0
- package/dist/chunk-PJ7WEQBJ.js +18 -0
- package/dist/chunk-PJ7WEQBJ.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-T3EC2DXD.js +12628 -0
- package/dist/chunk-T3EC2DXD.js.map +1 -0
- package/dist/{chunk-SMHP6XZ4.js → chunk-UN4U4P63.js} +10 -8
- package/dist/chunk-UN4U4P63.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/chunk-Z76KEZAX.js +15 -0
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/chunk-ZDHY47EV.js +363 -0
- package/dist/chunk-ZDHY47EV.js.map +1 -0
- package/dist/config/index.d.ts +276 -0
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +3 -4
- package/dist/controls/index.js +4 -3
- package/dist/dragdrop/index.d.ts +3 -5
- package/dist/index.d.ts +266 -413
- package/dist/index.js +1775 -2251
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -6
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +69 -20
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +175 -309
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +55 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +13 -8
- package/dist/state/index.js +18 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +223 -76
- 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 +3 -5
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +56 -5
- package/src/annotations/Annotations.less +4 -0
- package/src/annotations/Annotations.overlay.test.tsx +105 -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/svgNodes.ts +1 -0
- package/src/annotations/toolMap.test.ts +9 -5
- package/src/annotations/toolMap.ts +6 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- package/src/canvas/CanvasStack.stories.tsx +1 -1
- 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.test.ts +28 -0
- package/src/canvas/usePanZoom.ts +21 -9
- package/src/chrome/ChromeRegions.stories.tsx +1 -4
- package/src/chrome/LabChrome.tsx +130 -0
- package/src/chrome/builtins.test.ts +1 -0
- package/src/chrome/builtins.tsx +21 -31
- package/src/chrome/index.ts +16 -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 +16 -9
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +34 -16
- package/src/chrome/types.ts +50 -20
- package/src/config/auto.test.ts +12 -0
- package/src/config/auto.ts +15 -0
- package/src/config/autoConfig.test.ts +107 -0
- package/src/config/autoConfig.ts +153 -0
- package/src/config/builder.test.ts +42 -0
- package/src/config/builder.ts +68 -17
- 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 +16 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/path.ts +7 -2
- package/src/config/resolve.test.ts +25 -0
- package/src/config/resolve.ts +17 -1
- package/src/config/types.ts +34 -2
- package/src/config/useResolvedConfig.ts +26 -0
- package/src/controls/AutoControls.stories.tsx +88 -0
- package/src/controls/ControlPanel.stories.tsx +42 -1
- package/src/controls/ControlPanel.test.tsx +530 -4
- package/src/controls/ControlPanel.tsx +562 -82
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +66 -5
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +40 -1
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.chrome.test.tsx +16 -0
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +1 -3
- package/src/lab/Lab.surface.test.tsx +71 -3
- package/src/lab/Lab.test.tsx +168 -16
- package/src/lab/Lab.tsx +434 -129
- package/src/lab/LabContext.ts +8 -0
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +1 -4
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -14
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabRoot.test.tsx +56 -0
- package/src/lab/LabRoot.tsx +47 -0
- package/src/lab/LabShell.less +73 -15
- package/src/lab/LabShell.stories.tsx +50 -1
- package/src/lab/LabShell.tsx +18 -27
- package/src/lab/LabSwitcher.less +18 -11
- package/src/lab/LabSwitcher.test.tsx +14 -9
- package/src/lab/LabSwitcher.tsx +8 -4
- package/src/lab/Workspace.less +1 -1
- package/src/lab/Workspace.stories.tsx +1 -1
- package/src/lab/Workspace.surface.test.tsx +1 -0
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/lab/index.ts +2 -0
- package/src/layers/LayerList.stories.tsx +1 -1
- package/src/loupe/AGENTS.md +17 -5
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/LoupeGestures.tsx +67 -0
- package/src/loupe/TrialLoupe.tsx +43 -28
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/index.ts +4 -0
- package/src/loupe/loupeActions.ts +98 -0
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +60 -3
- package/src/loupe/useLoupe.ts +29 -49
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +14 -2
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/Legend.stories.tsx +1 -1
- package/src/primitives/ScaleIndicator.stories.tsx +1 -1
- package/src/primitives/Sidebar.less +7 -0
- package/src/primitives/Sidebar.stories.tsx +1 -1
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +133 -0
- package/src/primitives/Split.tsx +175 -0
- package/src/primitives/StatusBar.stories.tsx +1 -1
- package/src/primitives/Toolbar.stories.tsx +1 -1
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -0
- package/src/specimen/Specimen.less +95 -0
- package/src/specimen/Specimen.stories.tsx +13 -0
- package/src/specimen/Specimen.test.tsx +13 -0
- package/src/specimen/Specimen.tsx +888 -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 +169 -635
- package/src/state/store.ts +146 -163
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +65 -17
- 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 +87 -3
- package/src/state/useTrialState.ts +9 -1
- package/src/state/view.test.ts +49 -15
- package/src/state/view.ts +18 -1
- package/src/styles.less +3 -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 +1 -0
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +231 -1
- package/src/surface/useTiledSurface.ts +76 -15
- package/src/theme/Interstellar.stories.tsx +9 -9
- package/src/theme/interstellar.test.ts +17 -6
- package/src/theme/interstellar.theme.json +180 -0
- package/src/theme/interstellar.ts +8 -5
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +59 -7
- package/src/trial/Trial.canvas.test.tsx +32 -0
- package/src/trial/Trial.config.test.tsx +75 -2
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +5 -26
- package/src/trial/Trial.stories.tsx +5 -3
- package/src/trial/Trial.test.tsx +31 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +139 -52
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +24 -7
- package/src/trial/trialOps.test.ts +101 -1
- package/src/trial/trialOps.ts +67 -7
- 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-E44SU6XS.js +0 -50
- package/dist/chunk-E44SU6XS.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 +0 -343
- package/dist/chunk-U3IYHIAE.js.map +0 -1
- package/dist/chunk-W3ECWC2K.js +0 -7335
- package/dist/chunk-W3ECWC2K.js.map +0 -1
- package/src/theme/interstellar.tokens.json +0 -61
package/src/state/store.test.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import { describe, expect, it
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { labStorageKey } from './helpers';
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { auto } from '../config/auto';
|
|
3
|
+
import type { Instrument } from '../instrument/types';
|
|
5
4
|
import { createLabStore } from './store';
|
|
5
|
+
import type { CreateLabStoreOptions } from './types';
|
|
6
6
|
|
|
7
|
-
function makeStore(overrides?:
|
|
8
|
-
return createLabStore({
|
|
9
|
-
storageKey: 'test',
|
|
10
|
-
storage: createMemoryAdapter(),
|
|
11
|
-
...overrides,
|
|
12
|
-
});
|
|
7
|
+
function makeStore(overrides?: CreateLabStoreOptions) {
|
|
8
|
+
return createLabStore({ ...overrides });
|
|
13
9
|
}
|
|
14
10
|
|
|
11
|
+
const VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
|
|
12
|
+
|
|
15
13
|
describe('createLabStore — initial state', () => {
|
|
16
14
|
it('starts with empty trials and saves', () => {
|
|
17
15
|
const s = makeStore();
|
|
@@ -125,104 +123,12 @@ describe('updateTrialConfig', () => {
|
|
|
125
123
|
});
|
|
126
124
|
});
|
|
127
125
|
|
|
128
|
-
describe('createLabStore - config defaults', () => {
|
|
129
|
-
const stored = (config: unknown) =>
|
|
130
|
-
JSON.stringify({
|
|
131
|
-
version: CURRENT_DOCUMENT_VERSION,
|
|
132
|
-
trials: [
|
|
133
|
-
{
|
|
134
|
-
id: 'w1',
|
|
135
|
-
instrumentName: 'T',
|
|
136
|
-
config,
|
|
137
|
-
state: {},
|
|
138
|
-
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
139
|
-
},
|
|
140
|
-
],
|
|
141
|
-
saves: [
|
|
142
|
-
{
|
|
143
|
-
id: 's1',
|
|
144
|
-
name: 'saved',
|
|
145
|
-
trialId: 'w1',
|
|
146
|
-
instrumentName: 'T',
|
|
147
|
-
config,
|
|
148
|
-
state: {},
|
|
149
|
-
savedAt: 1,
|
|
150
|
-
},
|
|
151
|
-
],
|
|
152
|
-
layout: {},
|
|
153
|
-
undockedPanels: {},
|
|
154
|
-
mode: 'auto',
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('fills a branch a config stored before the schema nested it never had', () => {
|
|
158
|
-
const storage = createMemoryAdapter();
|
|
159
|
-
storage.write(labDocumentKey('test'), stored({ showGrid: false, gridSize: 40 }));
|
|
160
|
-
const s = createLabStore({
|
|
161
|
-
storageKey: 'test',
|
|
162
|
-
storage,
|
|
163
|
-
configDefaults: { T: () => ({ showGrid: true, grid: { size: 20, color: '#fff' } }) },
|
|
164
|
-
});
|
|
165
|
-
expect(s.getState().trials[0]?.config).toEqual({
|
|
166
|
-
showGrid: false,
|
|
167
|
-
gridSize: 40,
|
|
168
|
-
grid: { size: 20, color: '#fff' },
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('fills the config on a saved snapshot the same way', () => {
|
|
173
|
-
const storage = createMemoryAdapter();
|
|
174
|
-
storage.write(labDocumentKey('test'), stored({ grid: { size: 40 } }));
|
|
175
|
-
const s = createLabStore({
|
|
176
|
-
storageKey: 'test',
|
|
177
|
-
storage,
|
|
178
|
-
configDefaults: { T: () => ({ grid: { size: 20, color: '#fff' } }) },
|
|
179
|
-
});
|
|
180
|
-
expect(s.getState().savedSnapshots[0]?.config).toEqual({
|
|
181
|
-
grid: { size: 40, color: '#fff' },
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('hands a deserializer the filled config, not the stored one', () => {
|
|
186
|
-
const storage = createMemoryAdapter();
|
|
187
|
-
storage.write(labDocumentKey('test'), stored({ grid: { size: 40 } }));
|
|
188
|
-
const seen: unknown[] = [];
|
|
189
|
-
createLabStore({
|
|
190
|
-
storageKey: 'test',
|
|
191
|
-
storage,
|
|
192
|
-
configDefaults: { T: () => ({ grid: { size: 20, color: '#fff' } }) },
|
|
193
|
-
serializers: {
|
|
194
|
-
T: {
|
|
195
|
-
deserialize: (state, config) => {
|
|
196
|
-
seen.push(config);
|
|
197
|
-
return state;
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
expect(seen).toEqual([{ grid: { size: 40, color: '#fff' } }]);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('leaves a config alone when no defaults are registered for its instrument', () => {
|
|
206
|
-
const storage = createMemoryAdapter();
|
|
207
|
-
storage.write(labDocumentKey('test'), stored({ only: 1 }));
|
|
208
|
-
const s = createLabStore({ storageKey: 'test', storage });
|
|
209
|
-
expect(s.getState().trials[0]?.config).toEqual({ only: 1 });
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
|
|
213
126
|
describe('setMode', () => {
|
|
214
127
|
it('updates mode', () => {
|
|
215
128
|
const s = makeStore();
|
|
216
129
|
s.getState().setMode('dark');
|
|
217
130
|
expect(s.getState().mode).toBe('dark');
|
|
218
131
|
});
|
|
219
|
-
|
|
220
|
-
it('hydrates a stored interstellar preference as dark', () => {
|
|
221
|
-
const storage = createMemoryAdapter();
|
|
222
|
-
storage.write(labStorageKey('test', 'theme'), 'interstellar');
|
|
223
|
-
const s = createLabStore({ storageKey: 'test', storage });
|
|
224
|
-
expect(s.getState().mode).toBe('dark');
|
|
225
|
-
});
|
|
226
132
|
});
|
|
227
133
|
|
|
228
134
|
describe('save/load/delete snapshots', () => {
|
|
@@ -258,133 +164,90 @@ describe('save/load/delete snapshots', () => {
|
|
|
258
164
|
expect((s.getState().trials[0]?.state as { n: number }).n).toBe(5);
|
|
259
165
|
});
|
|
260
166
|
|
|
261
|
-
it('loadSnapshot
|
|
167
|
+
it('loadSnapshot restores the auto paths the trial had when saved', () => {
|
|
262
168
|
const s = makeStore();
|
|
263
169
|
s.getState().addTrial({
|
|
264
170
|
id: 'w1',
|
|
265
|
-
instrumentName: '
|
|
266
|
-
config: {},
|
|
267
|
-
state: {
|
|
171
|
+
instrumentName: 'T',
|
|
172
|
+
config: { x: 1, y: 2 },
|
|
173
|
+
state: {},
|
|
268
174
|
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
175
|
+
auto: ['x'],
|
|
269
176
|
});
|
|
270
|
-
s.getState().saveSnapshot('w1', '
|
|
271
|
-
s.getState().
|
|
177
|
+
s.getState().saveSnapshot('w1', 'snap1');
|
|
178
|
+
expect(s.getState().savedSnapshots[0]?.auto).toEqual(['x']);
|
|
272
179
|
const snapId = s.getState().savedSnapshots[0]?.id ?? '';
|
|
273
|
-
|
|
274
|
-
s.getState().
|
|
275
|
-
|
|
276
|
-
|
|
180
|
+
|
|
181
|
+
const w = s.getState().trials[0];
|
|
182
|
+
s.getState().addTrial({ ...(w as NonNullable<typeof w>), id: 'w2', auto: ['y'] });
|
|
183
|
+
s.getState().loadSnapshot(snapId, 'w2');
|
|
184
|
+
expect(s.getState().trials[1]?.auto).toEqual(['x']);
|
|
277
185
|
});
|
|
278
186
|
|
|
279
|
-
it('
|
|
187
|
+
it('a snapshot saved with nothing auto pins every field on load', () => {
|
|
280
188
|
const s = makeStore();
|
|
281
189
|
s.getState().addTrial({
|
|
282
190
|
id: 'w1',
|
|
283
191
|
instrumentName: 'T',
|
|
284
|
-
config: {},
|
|
192
|
+
config: { x: 1 },
|
|
285
193
|
state: {},
|
|
286
194
|
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
287
195
|
});
|
|
288
|
-
s.getState().saveSnapshot('w1', '
|
|
289
|
-
|
|
290
|
-
s.getState().
|
|
291
|
-
|
|
196
|
+
s.getState().saveSnapshot('w1', 'snap1');
|
|
197
|
+
expect(s.getState().savedSnapshots[0]?.auto).toEqual([]);
|
|
198
|
+
const snapId = s.getState().savedSnapshots[0]?.id ?? '';
|
|
199
|
+
s.getState().updateTrialConfig('w1', 'x', auto);
|
|
200
|
+
expect(s.getState().trials[0]?.auto).toEqual(['x']);
|
|
201
|
+
s.getState().loadSnapshot(snapId, 'w1');
|
|
202
|
+
expect(s.getState().trials[0]?.auto).toBeUndefined();
|
|
292
203
|
});
|
|
293
204
|
|
|
294
|
-
it(
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
205
|
+
it("a snapshot saved before snapshots carried auto paths leaves the trial's own", () => {
|
|
206
|
+
const snap = {
|
|
207
|
+
id: 'old',
|
|
208
|
+
name: 'old',
|
|
209
|
+
trialId: 'w1',
|
|
298
210
|
instrumentName: 'T',
|
|
299
|
-
config: {},
|
|
211
|
+
config: { x: 7 },
|
|
300
212
|
state: {},
|
|
301
|
-
|
|
213
|
+
savedAt: 1,
|
|
214
|
+
};
|
|
215
|
+
const s = makeStore({
|
|
216
|
+
initial: { version: 1, trials: [], saves: [snap], mode: 'auto' } as never,
|
|
302
217
|
});
|
|
303
|
-
s.getState().
|
|
304
|
-
s.getState().saveSnapshot('w1', 'second');
|
|
305
|
-
const list = s.getState().listSnapshots('w1');
|
|
306
|
-
expect(list[0]?.name).toBe('second');
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
describe('persistence — hydration', () => {
|
|
311
|
-
it('hydrates trials from storage on construction', () => {
|
|
312
|
-
vi.useFakeTimers();
|
|
313
|
-
const mem = createMemoryAdapter();
|
|
314
|
-
const seedStore = createLabStore({ storageKey: 'test', storage: mem });
|
|
315
|
-
seedStore.getState().addTrial({
|
|
218
|
+
s.getState().addTrial({
|
|
316
219
|
id: 'w1',
|
|
317
220
|
instrumentName: 'T',
|
|
318
|
-
config: {},
|
|
319
|
-
state: {
|
|
221
|
+
config: { x: 1 },
|
|
222
|
+
state: {},
|
|
320
223
|
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
224
|
+
auto: ['x'],
|
|
321
225
|
});
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const hydrated = createLabStore({ storageKey: 'test', storage: mem });
|
|
326
|
-
expect(hydrated.getState().trials).toHaveLength(1);
|
|
327
|
-
expect((hydrated.getState().trials[0]?.state as { n: number }).n).toBe(7);
|
|
226
|
+
s.getState().loadSnapshot('old', 'w1');
|
|
227
|
+
expect(s.getState().trials[0]?.config).toEqual({ x: 7 });
|
|
228
|
+
expect(s.getState().trials[0]?.auto).toEqual(['x']);
|
|
328
229
|
});
|
|
329
230
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
it("runs an instrument's serializers at flush and at hydrate", () => {
|
|
334
|
-
vi.useFakeTimers();
|
|
335
|
-
const mem = createMemoryAdapter();
|
|
336
|
-
const serializers = {
|
|
337
|
-
T: {
|
|
338
|
-
serialize: (state: unknown) => [...(state as Map<string, number>)],
|
|
339
|
-
deserialize: (data: unknown) => new Map(data as [string, number][]),
|
|
340
|
-
},
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
const seed = createLabStore({ storageKey: 'test', storage: mem, serializers });
|
|
344
|
-
seed.getState().addTrial({
|
|
231
|
+
it('loadSnapshot blocks cross-instrument load', () => {
|
|
232
|
+
const s = makeStore();
|
|
233
|
+
s.getState().addTrial({
|
|
345
234
|
id: 'w1',
|
|
346
|
-
instrumentName: '
|
|
235
|
+
instrumentName: 'A',
|
|
347
236
|
config: {},
|
|
348
|
-
state: new Map([['a', 1]]),
|
|
349
|
-
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
350
|
-
});
|
|
351
|
-
vi.advanceTimersByTime(500);
|
|
352
|
-
vi.useRealTimers();
|
|
353
|
-
|
|
354
|
-
const written = mem.read(labDocumentKey('test')) ?? '';
|
|
355
|
-
expect(written).toContain('[["a",1]]');
|
|
356
|
-
|
|
357
|
-
const hydrated = createLabStore({ storageKey: 'test', storage: mem, serializers });
|
|
358
|
-
expect(hydrated.getState().trials[0]?.state).toEqual(new Map([['a', 1]]));
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('hands `deserialize` the config the state was saved against', () => {
|
|
362
|
-
vi.useFakeTimers();
|
|
363
|
-
const mem = createMemoryAdapter();
|
|
364
|
-
const deserialize = vi.fn((data: unknown) => data);
|
|
365
|
-
const seed = createLabStore({ storageKey: 'test', storage: mem });
|
|
366
|
-
seed.getState().addTrial({
|
|
367
|
-
id: 'w1',
|
|
368
|
-
instrumentName: 'T',
|
|
369
|
-
config: { scale: 4 },
|
|
370
237
|
state: { n: 1 },
|
|
371
238
|
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
372
239
|
});
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
240
|
+
s.getState().saveSnapshot('w1', 'snap-a');
|
|
241
|
+
s.getState().setTrialInstrument('w1', 'B');
|
|
242
|
+
const snapId = s.getState().savedSnapshots[0]?.id ?? '';
|
|
243
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
244
|
+
s.getState().loadSnapshot(snapId, 'w1');
|
|
245
|
+
expect(warn).toHaveBeenCalled();
|
|
246
|
+
warn.mockRestore();
|
|
378
247
|
});
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
describe('persistence — debounced writes', () => {
|
|
382
|
-
it('multiple rapid mutations produce one write call', () => {
|
|
383
|
-
vi.useFakeTimers();
|
|
384
|
-
const mem = createMemoryAdapter();
|
|
385
|
-
const writeSpy = vi.spyOn(mem, 'write');
|
|
386
|
-
const s = createLabStore({ storageKey: 'test', storage: mem });
|
|
387
248
|
|
|
249
|
+
it('deleteSnapshot removes the snapshot', () => {
|
|
250
|
+
const s = makeStore();
|
|
388
251
|
s.getState().addTrial({
|
|
389
252
|
id: 'w1',
|
|
390
253
|
instrumentName: 'T',
|
|
@@ -392,415 +255,25 @@ describe('persistence — debounced writes', () => {
|
|
|
392
255
|
state: {},
|
|
393
256
|
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
394
257
|
});
|
|
395
|
-
s.getState().
|
|
396
|
-
s.getState().
|
|
397
|
-
s.getState().
|
|
398
|
-
|
|
399
|
-
const writesBefore = writeSpy.mock.calls.length;
|
|
400
|
-
vi.advanceTimersByTime(400);
|
|
401
|
-
const writesAfter = writeSpy.mock.calls.length;
|
|
402
|
-
|
|
403
|
-
// One flush, one document.
|
|
404
|
-
expect(writesAfter - writesBefore).toBe(1);
|
|
405
|
-
vi.useRealTimers();
|
|
406
|
-
});
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
describe('createLabStore — hydrating', () => {
|
|
410
|
-
it('reads a current-version document', () => {
|
|
411
|
-
const storage = createMemoryAdapter();
|
|
412
|
-
storage.write(
|
|
413
|
-
labDocumentKey('test'),
|
|
414
|
-
JSON.stringify({
|
|
415
|
-
version: CURRENT_DOCUMENT_VERSION,
|
|
416
|
-
trials: [
|
|
417
|
-
{
|
|
418
|
-
id: 'w1',
|
|
419
|
-
instrumentName: 'Test',
|
|
420
|
-
config: {},
|
|
421
|
-
state: {},
|
|
422
|
-
view: { zoom: 2, pan: { x: 0, y: 0 } },
|
|
423
|
-
},
|
|
424
|
-
],
|
|
425
|
-
saves: [],
|
|
426
|
-
layout: { w1: { h: 3 } },
|
|
427
|
-
mode: 'dark',
|
|
428
|
-
}),
|
|
429
|
-
);
|
|
430
|
-
|
|
431
|
-
const s = makeStore({ storage }).getState();
|
|
432
|
-
expect(s.trials).toHaveLength(1);
|
|
433
|
-
expect(s.trials[0].undoStack).toEqual({ past: [], future: [] });
|
|
434
|
-
expect(s.layout).toEqual({ w1: { h: 3 } });
|
|
435
|
-
expect(s.mode).toBe('dark');
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it('opens a version-1 document, whose tiles were still called workspaces', () => {
|
|
439
|
-
const storage = createMemoryAdapter();
|
|
440
|
-
storage.write(
|
|
441
|
-
labDocumentKey('test'),
|
|
442
|
-
JSON.stringify({
|
|
443
|
-
version: 1,
|
|
444
|
-
workspaces: [
|
|
445
|
-
{
|
|
446
|
-
id: 'w1',
|
|
447
|
-
instrumentName: 'Test',
|
|
448
|
-
config: { gain: 3 },
|
|
449
|
-
state: { n: 7 },
|
|
450
|
-
view: { zoom: 2, pan: { x: 0, y: 0 } },
|
|
451
|
-
},
|
|
452
|
-
],
|
|
453
|
-
saves: [{ id: 's1', name: 'a save', trialId: 'w1' }],
|
|
454
|
-
layout: { w1: { h: 3 } },
|
|
455
|
-
mode: 'dark',
|
|
456
|
-
}),
|
|
457
|
-
);
|
|
458
|
-
|
|
459
|
-
const s = makeStore({ storage }).getState();
|
|
460
|
-
expect(s.trials).toHaveLength(1);
|
|
461
|
-
expect(s.trials[0]).toMatchObject({ id: 'w1', config: { gain: 3 }, state: { n: 7 } });
|
|
462
|
-
expect(s.savedSnapshots).toHaveLength(1);
|
|
463
|
-
expect(s.layout).toEqual({ w1: { h: 3 } });
|
|
464
|
-
expect(s.mode).toBe('dark');
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
it('folds the four legacy keys into state', () => {
|
|
468
|
-
const storage = createMemoryAdapter();
|
|
469
|
-
storage.write(labStorageKey('test', 'workspaces'), JSON.stringify([]));
|
|
470
|
-
storage.write(labStorageKey('test', 'theme'), 'interstellar');
|
|
471
|
-
|
|
472
|
-
const s = makeStore({ storage }).getState();
|
|
473
|
-
expect(s.mode).toBe('dark');
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
it('honors initialMode when a folded legacy lab never set a theme', () => {
|
|
477
|
-
const storage = createMemoryAdapter();
|
|
478
|
-
storage.write(labStorageKey('test', 'workspaces'), JSON.stringify([]));
|
|
479
|
-
|
|
480
|
-
const s = makeStore({ storage, initialMode: 'dark' }).getState();
|
|
481
|
-
expect(s.mode).toBe('dark');
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
it('starts empty and preserves a document from a future version', () => {
|
|
485
|
-
const storage = createMemoryAdapter();
|
|
486
|
-
const future = JSON.stringify({ version: 999, trials: [{ id: 'w1' }] });
|
|
487
|
-
storage.write(labDocumentKey('test'), future);
|
|
488
|
-
|
|
489
|
-
const store = makeStore({ storage });
|
|
490
|
-
expect(store.getState().trials).toEqual([]);
|
|
491
|
-
expect(storage.read(labDocumentKey('test'))).toBe(future);
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
it('quarantines a document that fails to parse', () => {
|
|
495
|
-
const storage = createMemoryAdapter();
|
|
496
|
-
storage.write(labDocumentKey('test'), '{{{not json');
|
|
497
|
-
|
|
498
|
-
const s = makeStore({ storage }).getState();
|
|
499
|
-
expect(s.trials).toEqual([]);
|
|
500
|
-
expect(storage.read(quarantineKey('test'))).toBe('{{{not json');
|
|
501
|
-
});
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
describe('createLabStore — flushing', () => {
|
|
505
|
-
it('writes one document and no legacy keys', () => {
|
|
506
|
-
vi.useFakeTimers();
|
|
507
|
-
const storage = createMemoryAdapter();
|
|
508
|
-
const s = makeStore({ storage });
|
|
509
|
-
s.getState().setMode('light');
|
|
510
|
-
vi.advanceTimersByTime(400);
|
|
511
|
-
vi.useRealTimers();
|
|
512
|
-
|
|
513
|
-
const doc = JSON.parse(storage.read(labDocumentKey('test')) as string);
|
|
514
|
-
expect(doc.version).toBe(CURRENT_DOCUMENT_VERSION);
|
|
515
|
-
expect(doc.mode).toBe('light');
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
it('deletes the legacy keys once the folded document is written', () => {
|
|
519
|
-
vi.useFakeTimers();
|
|
520
|
-
const storage = createMemoryAdapter();
|
|
521
|
-
storage.write(labStorageKey('test', 'workspaces'), JSON.stringify([]));
|
|
522
|
-
storage.write(labStorageKey('test', 'theme'), 'interstellar');
|
|
523
|
-
|
|
524
|
-
const s = makeStore({ storage });
|
|
525
|
-
s.getState().setMode('dark');
|
|
526
|
-
vi.advanceTimersByTime(400);
|
|
527
|
-
vi.useRealTimers();
|
|
528
|
-
|
|
529
|
-
expect(storage.read(labDocumentKey('test'))).not.toBeNull();
|
|
530
|
-
expect(storage.read(labStorageKey('test', 'workspaces'))).toBeNull();
|
|
531
|
-
expect(storage.read(labStorageKey('test', 'theme'))).toBeNull();
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
it('does not write when the stored document is from the future', () => {
|
|
535
|
-
vi.useFakeTimers();
|
|
536
|
-
const storage = createMemoryAdapter();
|
|
537
|
-
const future = JSON.stringify({ version: 999 });
|
|
538
|
-
storage.write(labDocumentKey('test'), future);
|
|
539
|
-
|
|
540
|
-
const s = makeStore({ storage });
|
|
541
|
-
s.getState().setMode('light');
|
|
542
|
-
vi.advanceTimersByTime(400);
|
|
543
|
-
vi.useRealTimers();
|
|
544
|
-
|
|
545
|
-
expect(storage.read(labDocumentKey('test'))).toBe(future);
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
it('keeps the legacy keys when the document write silently fails', () => {
|
|
549
|
-
vi.useFakeTimers();
|
|
550
|
-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
551
|
-
const storage = createMemoryAdapter();
|
|
552
|
-
const write = storage.write.bind(storage);
|
|
553
|
-
// A quota failure as localStorageAdapter reports one: swallowed, void.
|
|
554
|
-
storage.write = (key, value) => {
|
|
555
|
-
if (key === labDocumentKey('test')) return;
|
|
556
|
-
write(key, value);
|
|
557
|
-
};
|
|
558
|
-
storage.write(labStorageKey('test', 'workspaces'), JSON.stringify([]));
|
|
559
|
-
storage.write(labStorageKey('test', 'theme'), 'interstellar');
|
|
560
|
-
|
|
561
|
-
const s = makeStore({ storage });
|
|
562
|
-
s.getState().setMode('dark');
|
|
563
|
-
vi.advanceTimersByTime(400);
|
|
564
|
-
vi.useRealTimers();
|
|
565
|
-
|
|
566
|
-
expect(storage.read(labDocumentKey('test'))).toBeNull();
|
|
567
|
-
expect(storage.read(labStorageKey('test', 'workspaces'))).toBe('[]');
|
|
568
|
-
expect(storage.read(labStorageKey('test', 'theme'))).toBe('interstellar');
|
|
569
|
-
expect(warn).toHaveBeenCalled();
|
|
570
|
-
warn.mockRestore();
|
|
258
|
+
s.getState().saveSnapshot('w1', 'snap');
|
|
259
|
+
const id = s.getState().savedSnapshots[0]?.id ?? '';
|
|
260
|
+
s.getState().deleteSnapshot(id);
|
|
261
|
+
expect(s.getState().savedSnapshots).toHaveLength(0);
|
|
571
262
|
});
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
const VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
|
|
575
|
-
|
|
576
|
-
const SNAPSHOT = {
|
|
577
|
-
id: 's1',
|
|
578
|
-
name: 'saved',
|
|
579
|
-
trialId: 'w1',
|
|
580
|
-
instrumentName: 'T',
|
|
581
|
-
config: { x: 1 },
|
|
582
|
-
state: { n: 5 },
|
|
583
|
-
savedAt: 1000,
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
function writeDocument(
|
|
587
|
-
storage: ReturnType<typeof createMemoryAdapter>,
|
|
588
|
-
doc: Record<string, unknown>,
|
|
589
|
-
storageKey = 'test',
|
|
590
|
-
): void {
|
|
591
|
-
storage.write(
|
|
592
|
-
labDocumentKey(storageKey),
|
|
593
|
-
JSON.stringify({ version: CURRENT_DOCUMENT_VERSION, ...doc }),
|
|
594
|
-
);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
describe('createLabStore — labs whose keys once aliased', () => {
|
|
598
|
-
it("keeps a document out of a sibling lab's legacy bucket", () => {
|
|
599
|
-
vi.useFakeTimers();
|
|
600
|
-
const storage = createMemoryAdapter();
|
|
601
263
|
|
|
602
|
-
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
id: '
|
|
264
|
+
it('listSnapshots returns newest first', () => {
|
|
265
|
+
const s = makeStore();
|
|
266
|
+
s.getState().addTrial({
|
|
267
|
+
id: 'w1',
|
|
606
268
|
instrumentName: 'T',
|
|
607
269
|
config: {},
|
|
608
|
-
state: {
|
|
609
|
-
view:
|
|
270
|
+
state: {},
|
|
271
|
+
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
610
272
|
});
|
|
611
|
-
vi.advanceTimersByTime(400);
|
|
612
|
-
const siblingDocument = storage.read(labDocumentKey('a:saves'));
|
|
613
|
-
expect(siblingDocument).not.toBeNull();
|
|
614
|
-
|
|
615
|
-
storage.write(labStorageKey('a', 'workspaces'), JSON.stringify([]));
|
|
616
|
-
storage.write(labStorageKey('a', 'theme'), 'dark');
|
|
617
|
-
|
|
618
|
-
const lab = createLabStore({ storageKey: 'a', storage });
|
|
619
|
-
// Lab "a" does not read the sibling's document as its own saves bucket.
|
|
620
|
-
expect(lab.getState().savedSnapshots).toEqual([]);
|
|
621
|
-
expect(lab.getState().trials).toEqual([]);
|
|
622
|
-
|
|
623
|
-
lab.getState().setMode('light');
|
|
624
|
-
vi.advanceTimersByTime(400);
|
|
625
|
-
vi.useRealTimers();
|
|
626
|
-
|
|
627
|
-
// Folding lab "a" deletes its own legacy keys, not the sibling's document.
|
|
628
|
-
expect(storage.read(labStorageKey('a', 'workspaces'))).toBeNull();
|
|
629
|
-
expect(storage.read(labDocumentKey('a:saves'))).toBe(siblingDocument);
|
|
630
|
-
const rehydrated = createLabStore({ storageKey: 'a:saves', storage });
|
|
631
|
-
expect(rehydrated.getState().trials).toHaveLength(1);
|
|
632
|
-
});
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
describe('createLabStore — hydrating an incomplete current-version document', () => {
|
|
636
|
-
it('hydrates a document with no saves as an empty snapshot list', () => {
|
|
637
|
-
const storage = createMemoryAdapter();
|
|
638
|
-
writeDocument(storage, { trials: [], layout: {}, mode: 'dark' });
|
|
639
|
-
|
|
640
|
-
const s = makeStore({ storage }).getState();
|
|
641
|
-
expect(s.savedSnapshots).toEqual([]);
|
|
642
|
-
expect(s.listSnapshots()).toEqual([]);
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
it('hydrates a document with no layout as an empty layout', () => {
|
|
646
|
-
const storage = createMemoryAdapter();
|
|
647
|
-
writeDocument(storage, { trials: [], saves: [], mode: 'dark' });
|
|
648
|
-
|
|
649
|
-
expect(makeStore({ storage }).getState().layout).toEqual({});
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
it('falls back to initialMode when the document has no mode', () => {
|
|
653
|
-
const storage = createMemoryAdapter();
|
|
654
|
-
writeDocument(storage, { trials: [], saves: [], layout: {} });
|
|
655
|
-
|
|
656
|
-
expect(makeStore({ storage, initialMode: 'light' }).getState().mode).toBe('light');
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
it('hydrates a document holding nothing but its version', () => {
|
|
660
|
-
const storage = createMemoryAdapter();
|
|
661
|
-
writeDocument(storage, {});
|
|
662
|
-
|
|
663
|
-
const s = makeStore({ storage }).getState();
|
|
664
|
-
expect(s.trials).toEqual([]);
|
|
665
|
-
expect(s.savedSnapshots).toEqual([]);
|
|
666
|
-
expect(s.layout).toEqual({});
|
|
667
|
-
expect(s.mode).toBe('auto');
|
|
668
|
-
expect(s.listSnapshots()).toEqual([]);
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
it('can save a snapshot into a document that had no saves', () => {
|
|
672
|
-
const storage = createMemoryAdapter();
|
|
673
|
-
writeDocument(storage, {});
|
|
674
|
-
|
|
675
|
-
const s = makeStore({ storage });
|
|
676
|
-
s.getState().addTrial({ id: 'w1', instrumentName: 'T', config: {}, state: {}, view: VIEW });
|
|
677
|
-
s.getState().saveSnapshot('w1', 'first');
|
|
678
|
-
expect(s.getState().listSnapshots('w1')).toHaveLength(1);
|
|
679
|
-
});
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
describe('persistence — saves and layout round-trip', () => {
|
|
683
|
-
it('hydrates a populated saves array into the store', () => {
|
|
684
|
-
const storage = createMemoryAdapter();
|
|
685
|
-
writeDocument(storage, { trials: [], saves: [SNAPSHOT], layout: {}, mode: 'auto' });
|
|
686
|
-
|
|
687
|
-
const s = makeStore({ storage }).getState();
|
|
688
|
-
expect(s.savedSnapshots).toEqual([SNAPSHOT]);
|
|
689
|
-
expect(s.listSnapshots('w1')).toEqual([SNAPSHOT]);
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
it('writes the hydrated saves back out on the next flush', () => {
|
|
693
|
-
vi.useFakeTimers();
|
|
694
|
-
const storage = createMemoryAdapter();
|
|
695
|
-
writeDocument(storage, { trials: [], saves: [SNAPSHOT], layout: {}, mode: 'auto' });
|
|
696
|
-
|
|
697
|
-
const s = makeStore({ storage });
|
|
698
|
-
s.getState().setMode('light');
|
|
699
|
-
vi.advanceTimersByTime(400);
|
|
700
|
-
vi.useRealTimers();
|
|
701
|
-
|
|
702
|
-
const doc = JSON.parse(storage.read(labDocumentKey('test')) as string);
|
|
703
|
-
expect(doc.saves).toEqual([SNAPSHOT]);
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
it('writes a snapshot saved this session out on flush', () => {
|
|
707
|
-
vi.useFakeTimers();
|
|
708
|
-
const storage = createMemoryAdapter();
|
|
709
|
-
const s = makeStore({ storage });
|
|
710
|
-
s.getState().addTrial({ id: 'w1', instrumentName: 'T', config: {}, state: {}, view: VIEW });
|
|
711
273
|
s.getState().saveSnapshot('w1', 'first');
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const doc = JSON.parse(storage.read(labDocumentKey('test')) as string);
|
|
716
|
-
expect(doc.saves).toHaveLength(1);
|
|
717
|
-
expect(doc.saves[0].name).toBe('first');
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
it('writes the layout out on flush', () => {
|
|
721
|
-
vi.useFakeTimers();
|
|
722
|
-
const storage = createMemoryAdapter();
|
|
723
|
-
const s = makeStore({ storage });
|
|
724
|
-
s.getState().setLayout({ w1: { h: 9 } });
|
|
725
|
-
vi.advanceTimersByTime(400);
|
|
726
|
-
vi.useRealTimers();
|
|
727
|
-
|
|
728
|
-
const doc = JSON.parse(storage.read(labDocumentKey('test')) as string);
|
|
729
|
-
expect(doc.layout).toEqual({ w1: { h: 9 } });
|
|
730
|
-
});
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
describe('createLabStore — quarantining an unusable document', () => {
|
|
734
|
-
it('persists again once the unusable document is safely quarantined', () => {
|
|
735
|
-
vi.useFakeTimers();
|
|
736
|
-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
737
|
-
const storage = createMemoryAdapter();
|
|
738
|
-
storage.write(labDocumentKey('test'), '{{{not json');
|
|
739
|
-
|
|
740
|
-
const s = makeStore({ storage });
|
|
741
|
-
s.getState().setMode('light');
|
|
742
|
-
vi.advanceTimersByTime(400);
|
|
743
|
-
vi.useRealTimers();
|
|
744
|
-
|
|
745
|
-
expect(storage.read(quarantineKey('test'))).toBe('{{{not json');
|
|
746
|
-
const doc = JSON.parse(storage.read(labDocumentKey('test')) as string);
|
|
747
|
-
expect(doc.mode).toBe('light');
|
|
748
|
-
warn.mockRestore();
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
it('leaves the original alone when the quarantine copy cannot be written', () => {
|
|
752
|
-
vi.useFakeTimers();
|
|
753
|
-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
754
|
-
const storage = createMemoryAdapter();
|
|
755
|
-
const write = storage.write.bind(storage);
|
|
756
|
-
// A quota failure as localStorageAdapter reports one: swallowed, void.
|
|
757
|
-
storage.write = (key, value) => {
|
|
758
|
-
if (key === quarantineKey('test')) return;
|
|
759
|
-
write(key, value);
|
|
760
|
-
};
|
|
761
|
-
storage.write(labDocumentKey('test'), '{{{not json');
|
|
762
|
-
|
|
763
|
-
const s = makeStore({ storage });
|
|
764
|
-
s.getState().setMode('light');
|
|
765
|
-
vi.advanceTimersByTime(400);
|
|
766
|
-
vi.useRealTimers();
|
|
767
|
-
|
|
768
|
-
expect(storage.read(labDocumentKey('test'))).toBe('{{{not json');
|
|
769
|
-
expect(warn).toHaveBeenCalled();
|
|
770
|
-
warn.mockRestore();
|
|
771
|
-
});
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
describe('createLabStore — folding without a mutation', () => {
|
|
775
|
-
it('completes the fold on first load alone', () => {
|
|
776
|
-
vi.useFakeTimers();
|
|
777
|
-
const storage = createMemoryAdapter();
|
|
778
|
-
storage.write(labStorageKey('test', 'workspaces'), JSON.stringify([]));
|
|
779
|
-
storage.write(labStorageKey('test', 'theme'), 'interstellar');
|
|
780
|
-
|
|
781
|
-
makeStore({ storage });
|
|
782
|
-
vi.advanceTimersByTime(400);
|
|
783
|
-
vi.useRealTimers();
|
|
784
|
-
|
|
785
|
-
const doc = JSON.parse(storage.read(labDocumentKey('test')) as string);
|
|
786
|
-
expect(doc.mode).toBe('dark');
|
|
787
|
-
expect(storage.read(labStorageKey('test', 'workspaces'))).toBeNull();
|
|
788
|
-
expect(storage.read(labStorageKey('test', 'theme'))).toBeNull();
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
it('does not schedule a write for a document from the future', () => {
|
|
792
|
-
vi.useFakeTimers();
|
|
793
|
-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
794
|
-
const storage = createMemoryAdapter();
|
|
795
|
-
const future = JSON.stringify({ version: 999 });
|
|
796
|
-
storage.write(labDocumentKey('test'), future);
|
|
797
|
-
|
|
798
|
-
makeStore({ storage });
|
|
799
|
-
vi.advanceTimersByTime(400);
|
|
800
|
-
vi.useRealTimers();
|
|
801
|
-
|
|
802
|
-
expect(storage.read(labDocumentKey('test'))).toBe(future);
|
|
803
|
-
warn.mockRestore();
|
|
274
|
+
s.getState().saveSnapshot('w1', 'second');
|
|
275
|
+
const list = s.getState().listSnapshots('w1');
|
|
276
|
+
expect(list[0]?.name).toBe('second');
|
|
804
277
|
});
|
|
805
278
|
});
|
|
806
279
|
|
|
@@ -846,18 +319,6 @@ describe('setTrialTitle', () => {
|
|
|
846
319
|
s.getState().setTrialTitle('w1', 'Sprocket 7');
|
|
847
320
|
expect(s.getState().trials[0]).toBe(before);
|
|
848
321
|
});
|
|
849
|
-
|
|
850
|
-
it('round-trips through storage', () => {
|
|
851
|
-
vi.useFakeTimers();
|
|
852
|
-
const storage = createMemoryAdapter();
|
|
853
|
-
const first = makeStore({ storage });
|
|
854
|
-
first.getState().addTrial({ id: 'w1', instrumentName: 'T', config: {}, state: {}, view: VIEW });
|
|
855
|
-
first.getState().setTrialTitle('w1', 'Sprocket 7');
|
|
856
|
-
vi.advanceTimersByTime(400);
|
|
857
|
-
vi.useRealTimers();
|
|
858
|
-
|
|
859
|
-
expect(makeStore({ storage }).getState().trials[0]?.title).toBe('Sprocket 7');
|
|
860
|
-
});
|
|
861
322
|
});
|
|
862
323
|
|
|
863
324
|
describe('setTrialSectionCollapsed', () => {
|
|
@@ -877,31 +338,104 @@ describe('setTrialSectionCollapsed', () => {
|
|
|
877
338
|
s.getState().setTrialSectionCollapsed('w1', 'settings', true);
|
|
878
339
|
expect(s.getState().trials[0]).toBe(before);
|
|
879
340
|
});
|
|
341
|
+
});
|
|
880
342
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
343
|
+
describe('setInstruments', () => {
|
|
344
|
+
const small: Instrument = {
|
|
345
|
+
name: 'Grid',
|
|
346
|
+
defaultConfig: () => ({ size: 10 }),
|
|
347
|
+
initialState: () => ({}),
|
|
348
|
+
render: () => null,
|
|
349
|
+
};
|
|
350
|
+
const other: Instrument = { ...small, name: 'Other', defaultConfig: () => ({ n: 1 }) };
|
|
351
|
+
|
|
352
|
+
function trial(id: string, instrumentName: string, config: unknown) {
|
|
353
|
+
return { id, instrumentName, config, state: {}, view: null };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
it('fills a replaced instrument’s open trials from its new defaults', () => {
|
|
357
|
+
const store = createLabStore({ instruments: [small, other] });
|
|
358
|
+
store.getState().addTrial(trial('a', 'Grid', { size: 4 }));
|
|
359
|
+
store.getState().addTrial(trial('b', 'Other', { n: 2 }));
|
|
360
|
+
const untouched = store.getState().trials[1];
|
|
361
|
+
|
|
362
|
+
const grown: Instrument = { ...small, defaultConfig: () => ({ size: 10, color: 'red' }) };
|
|
363
|
+
store.getState().setInstruments([grown, other]);
|
|
364
|
+
|
|
365
|
+
expect(store.getState().trials[0]?.config).toEqual({ size: 4, color: 'red' });
|
|
366
|
+
expect(store.getState().trials[1]).toBe(untouched);
|
|
367
|
+
expect(store.getState().instruments).toEqual([grown, other]);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('serializes an instrument added after the store was built', () => {
|
|
371
|
+
const store = createLabStore({ instruments: [small] });
|
|
372
|
+
const mapped: Instrument = {
|
|
373
|
+
...small,
|
|
374
|
+
name: 'Mapped',
|
|
375
|
+
serialize: (s) => ({ seen: [...(s as { seen: Set<string> }).seen] }),
|
|
376
|
+
};
|
|
377
|
+
store.getState().setInstruments([small, mapped]);
|
|
378
|
+
store.getState().addTrial({
|
|
379
|
+
id: 'm',
|
|
380
|
+
instrumentName: 'Mapped',
|
|
381
|
+
config: { size: 10 },
|
|
382
|
+
state: { seen: new Set(['a']) },
|
|
383
|
+
view: null,
|
|
902
384
|
});
|
|
903
|
-
|
|
904
|
-
expect(
|
|
905
|
-
|
|
385
|
+
store.getState().saveSnapshot('m', 'one');
|
|
386
|
+
expect(store.getState().savedSnapshots[0]?.state).toEqual({ seen: ['a'] });
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('is a no-op for the list it already holds', () => {
|
|
390
|
+
const list = [small];
|
|
391
|
+
const store = createLabStore({ instruments: list });
|
|
392
|
+
const before = store.getState();
|
|
393
|
+
store.getState().setInstruments(list);
|
|
394
|
+
expect(store.getState()).toBe(before);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const mapped: Instrument = {
|
|
398
|
+
...small,
|
|
399
|
+
name: 'Mapped',
|
|
400
|
+
serialize: (s) => ({ seen: [...(s as { seen: Set<string> }).seen] }),
|
|
401
|
+
};
|
|
402
|
+
const mappedTrial = {
|
|
403
|
+
id: 'm',
|
|
404
|
+
instrumentName: 'Mapped',
|
|
405
|
+
config: { size: 10 },
|
|
406
|
+
state: { seen: new Set(['a']) },
|
|
407
|
+
view: null,
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
it('keeps the hooks of an instrument that leaves the list while its trials remain', () => {
|
|
411
|
+
const store = createLabStore({ instruments: [mapped] });
|
|
412
|
+
store.getState().addTrial(mappedTrial);
|
|
413
|
+
store.getState().setInstruments([]);
|
|
414
|
+
store.getState().saveSnapshot('m', 'one');
|
|
415
|
+
expect(store.getState().savedSnapshots[0]?.state).toEqual({ seen: ['a'] });
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('drops the serializer of an instrument replaced by one that declares none', () => {
|
|
419
|
+
const store = createLabStore({ instruments: [mapped] });
|
|
420
|
+
store.getState().addTrial(mappedTrial);
|
|
421
|
+
store.getState().setInstruments([{ ...small, name: 'Mapped' }]);
|
|
422
|
+
store.getState().saveSnapshot('m', 'one');
|
|
423
|
+
expect(store.getState().savedSnapshots[0]?.state).toEqual({ seen: new Set(['a']) });
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('changes neither hooks nor state when a new default config throws', () => {
|
|
427
|
+
const store = createLabStore({ instruments: [small, other] });
|
|
428
|
+
store.getState().addTrial(trial('a', 'Grid', { size: 4 }));
|
|
429
|
+
const before = store.getState();
|
|
430
|
+
const hooks = store.getState().instrumentHooks();
|
|
431
|
+
const broken: Instrument = {
|
|
432
|
+
...small,
|
|
433
|
+
defaultConfig: () => {
|
|
434
|
+
throw new Error('boom');
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
expect(() => store.getState().setInstruments([broken, mapped])).toThrow('boom');
|
|
438
|
+
expect(store.getState()).toBe(before);
|
|
439
|
+
expect(store.getState().instrumentHooks()).toBe(hooks);
|
|
906
440
|
});
|
|
907
441
|
});
|