@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
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
* is written on a trailing debounce: a write per scene notification re-renders
|
|
5
5
|
* every trial on every frame of a drag.
|
|
6
6
|
*/
|
|
7
|
-
import { act, render } from '@testing-library/react';
|
|
7
|
+
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
8
8
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
9
|
import { useAnnotations } from '../annotations/AnnotationsContext';
|
|
10
10
|
import type { AnnotationsApi, SerializedAnnotations } from '../annotations/types';
|
|
11
11
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
12
12
|
import type { InstrumentList } from '../instrument/types';
|
|
13
13
|
import { Lab } from '../lab/Lab';
|
|
14
|
+
import { LabContext, type LabContextValue } from '../lab/LabContext';
|
|
14
15
|
import { createMemoryAdapter } from '../state/adapters';
|
|
15
|
-
import
|
|
16
|
+
import { labPrefix } from '../state/labRecords';
|
|
16
17
|
|
|
17
18
|
beforeAll(() => {
|
|
18
19
|
HTMLCanvasElement.prototype.getContext = vi.fn(
|
|
@@ -21,12 +22,24 @@ beforeAll(() => {
|
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
let api: AnnotationsApi | null = null;
|
|
25
|
+
let lab: LabContextValue | null = null;
|
|
24
26
|
|
|
25
27
|
function Pane() {
|
|
26
28
|
api = useAnnotations();
|
|
27
29
|
return <div data-testid="pane" />;
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
function CaptureLab() {
|
|
33
|
+
return (
|
|
34
|
+
<LabContext.Consumer>
|
|
35
|
+
{(value) => {
|
|
36
|
+
lab = value;
|
|
37
|
+
return null;
|
|
38
|
+
}}
|
|
39
|
+
</LabContext.Consumer>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
/** Read through a call, so TypeScript does not narrow the module-level `api`
|
|
31
44
|
* to `null` across the remounts these tests are about. */
|
|
32
45
|
function marks(): AnnotationsApi {
|
|
@@ -46,6 +59,8 @@ const inspector = defineInstrument<Record<string, never>, { angle: number }>({
|
|
|
46
59
|
|
|
47
60
|
/** The instrument keeps its own marks, so labkit must not keep them too. */
|
|
48
61
|
let owned: SerializedAnnotations | null = null;
|
|
62
|
+
let loads = 0;
|
|
63
|
+
let gate: Promise<void> = Promise.resolve();
|
|
49
64
|
const selfStoring = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
50
65
|
name: 'SelfStoring',
|
|
51
66
|
defaultConfig: () => ({}),
|
|
@@ -54,7 +69,11 @@ const selfStoring = defineInstrument<Record<string, never>, Record<string, never
|
|
|
54
69
|
annotations: {
|
|
55
70
|
targets: () => [TARGET],
|
|
56
71
|
storage: {
|
|
57
|
-
load: () =>
|
|
72
|
+
load: async () => {
|
|
73
|
+
loads++;
|
|
74
|
+
await gate;
|
|
75
|
+
return owned;
|
|
76
|
+
},
|
|
58
77
|
save: (doc) => {
|
|
59
78
|
owned = doc;
|
|
60
79
|
},
|
|
@@ -62,42 +81,51 @@ const selfStoring = defineInstrument<Record<string, never>, Record<string, never
|
|
|
62
81
|
},
|
|
63
82
|
});
|
|
64
83
|
|
|
65
|
-
function mount(
|
|
66
|
-
|
|
84
|
+
async function mount(backing: Map<string, unknown>, which: InstrumentList[number] = inspector) {
|
|
85
|
+
api = null;
|
|
86
|
+
const view = render(
|
|
67
87
|
<Lab
|
|
68
88
|
instruments={[which]}
|
|
69
89
|
defaultInstrument={which.name}
|
|
70
|
-
storage={
|
|
90
|
+
storage={createMemoryAdapter(backing)}
|
|
71
91
|
storageKey="persist-test"
|
|
72
|
-
|
|
92
|
+
>
|
|
93
|
+
<CaptureLab />
|
|
94
|
+
</Lab>,
|
|
73
95
|
);
|
|
96
|
+
await waitFor(() => expect(api).not.toBeNull());
|
|
97
|
+
return view;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function storedTrial(backing: Map<string, unknown>): Record<string, unknown> | undefined {
|
|
101
|
+
const prefix = `${labPrefix('persist-test')}trial:`;
|
|
102
|
+
return [...backing].find(([key]) => key.startsWith(prefix))?.[1] as
|
|
103
|
+
| Record<string, unknown>
|
|
104
|
+
| undefined;
|
|
74
105
|
}
|
|
75
106
|
|
|
76
107
|
const MARK = { target: 'pane', kind: 'rect' as const, frac: { x: 0.2, y: 0.3, w: 0.1, h: 0.1 } };
|
|
77
108
|
|
|
78
109
|
describe('a trial that persists its marks', () => {
|
|
79
110
|
beforeEach(() => {
|
|
80
|
-
vi.useFakeTimers();
|
|
81
111
|
api = null;
|
|
112
|
+
lab = null;
|
|
82
113
|
owned = null;
|
|
114
|
+
loads = 0;
|
|
115
|
+
gate = Promise.resolve();
|
|
83
116
|
});
|
|
84
117
|
afterEach(() => {
|
|
85
118
|
vi.useRealTimers();
|
|
86
119
|
});
|
|
87
120
|
|
|
88
|
-
it('brings them back on a remount from the same storage', () => {
|
|
89
|
-
const
|
|
90
|
-
const first = mount(
|
|
121
|
+
it('brings them back on a remount from the same storage', async () => {
|
|
122
|
+
const backing = new Map<string, unknown>();
|
|
123
|
+
const first = await mount(backing);
|
|
91
124
|
const id = marks().add(MARK, { angle: 12 });
|
|
92
|
-
|
|
93
|
-
// Past both debounces: labkit's write-back, then the document flush.
|
|
94
|
-
act(() => {
|
|
95
|
-
vi.advanceTimersByTime(1000);
|
|
96
|
-
});
|
|
97
125
|
first.unmount();
|
|
126
|
+
await waitFor(() => expect(storedTrial(backing)?.annotations).toBeDefined());
|
|
98
127
|
|
|
99
|
-
|
|
100
|
-
mount(storage);
|
|
128
|
+
await mount(backing);
|
|
101
129
|
const back = marks().get(id);
|
|
102
130
|
expect(back).toMatchObject({ id, target: 'pane', kind: 'rect', frac: MARK.frac });
|
|
103
131
|
// The staleness snapshot has to survive too, or every restored mark reads
|
|
@@ -107,60 +135,74 @@ describe('a trial that persists its marks', () => {
|
|
|
107
135
|
expect(marks().isStale(back, { angle: 45 })).toBe(true);
|
|
108
136
|
});
|
|
109
137
|
|
|
110
|
-
it('
|
|
111
|
-
const
|
|
112
|
-
|
|
138
|
+
it('keeps a mark out of storage until both debounces pass, while the lab stays open', async () => {
|
|
139
|
+
const backing = new Map<string, unknown>();
|
|
140
|
+
await mount(backing);
|
|
141
|
+
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'Date'] });
|
|
113
142
|
marks().add(MARK);
|
|
114
|
-
act(() =>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
mount(storage);
|
|
121
|
-
// Not a claim about the debounce interval — a claim that one exists, and
|
|
122
|
-
// that a store write is not synchronous with a scene notification.
|
|
123
|
-
//
|
|
124
|
-
// It is also the no-migration claim: the document this remount reads was
|
|
125
|
-
// written with trials and no `annotations` field, exactly as every
|
|
126
|
-
// document from before this arc is, and the trial opens on an empty store
|
|
127
|
-
// rather than failing to load.
|
|
128
|
-
expect(marks().query()).toHaveLength(0);
|
|
143
|
+
await act(() => vi.advanceTimersByTimeAsync(20));
|
|
144
|
+
// Not a claim about either interval — a claim that a store write is not
|
|
145
|
+
// synchronous with a scene notification.
|
|
146
|
+
expect(storedTrial(backing)?.annotations).toBeUndefined();
|
|
147
|
+
await act(() => vi.advanceTimersByTimeAsync(2000));
|
|
148
|
+
expect(storedTrial(backing)?.annotations).toBeDefined();
|
|
129
149
|
});
|
|
130
150
|
|
|
131
|
-
it('flushes on unmount, so the last mark before a close is not lost', () => {
|
|
132
|
-
const
|
|
133
|
-
const first = mount(
|
|
151
|
+
it('flushes on unmount, so the last mark before a close is not lost', async () => {
|
|
152
|
+
const backing = new Map<string, unknown>();
|
|
153
|
+
const first = await mount(backing);
|
|
134
154
|
const id = marks().add(MARK);
|
|
135
|
-
act(() => {
|
|
136
|
-
vi.advanceTimersByTime(500);
|
|
137
|
-
});
|
|
138
155
|
marks().add({ ...MARK, frac: { x: 0.6, y: 0.6, w: 0.1, h: 0.1 } });
|
|
139
156
|
first.unmount();
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
157
|
+
// Nothing was written before the unmount: neither debounce had elapsed.
|
|
158
|
+
await waitFor(() => expect(storedTrial(backing)?.annotations).toBeDefined());
|
|
143
159
|
|
|
144
|
-
|
|
145
|
-
mount(storage);
|
|
160
|
+
await mount(backing);
|
|
146
161
|
expect(marks().query()).toHaveLength(2);
|
|
147
162
|
expect(marks().get(id)).toBeDefined();
|
|
148
163
|
});
|
|
149
164
|
|
|
150
|
-
it('leaves the slot alone when the instrument owns the storage', () => {
|
|
151
|
-
const
|
|
152
|
-
const first = mount(
|
|
165
|
+
it('leaves the slot alone when the instrument owns the storage', async () => {
|
|
166
|
+
const backing = new Map<string, unknown>();
|
|
167
|
+
const first = await mount(backing, selfStoring);
|
|
153
168
|
marks().add(MARK);
|
|
154
|
-
act(() => {
|
|
155
|
-
vi.advanceTimersByTime(1000);
|
|
156
|
-
});
|
|
157
169
|
first.unmount();
|
|
170
|
+
await waitFor(() => expect(owned?.version).toBe(1));
|
|
171
|
+
await waitFor(() => expect(storedTrial(backing)).toBeDefined());
|
|
172
|
+
expect(storedTrial(backing)).not.toHaveProperty('annotations');
|
|
158
173
|
|
|
159
|
-
|
|
160
|
-
expect(storage.read('lk:persist-test:doc')).not.toContain('"annotations"');
|
|
161
|
-
|
|
162
|
-
api = null;
|
|
163
|
-
mount(storage, selfStoring);
|
|
174
|
+
await mount(backing, selfStoring);
|
|
164
175
|
expect(marks().query()).toHaveLength(1);
|
|
165
176
|
});
|
|
177
|
+
|
|
178
|
+
it('loads instrument-owned marks before the lab shows, and a later trial before its body', async () => {
|
|
179
|
+
const backing = new Map<string, unknown>();
|
|
180
|
+
let release = (): void => {};
|
|
181
|
+
gate = new Promise((resolve) => {
|
|
182
|
+
release = resolve;
|
|
183
|
+
});
|
|
184
|
+
render(
|
|
185
|
+
<Lab
|
|
186
|
+
instruments={[selfStoring]}
|
|
187
|
+
defaultInstrument="SelfStoring"
|
|
188
|
+
storage={createMemoryAdapter(backing)}
|
|
189
|
+
storageKey="persist-test"
|
|
190
|
+
>
|
|
191
|
+
<CaptureLab />
|
|
192
|
+
</Lab>,
|
|
193
|
+
);
|
|
194
|
+
await waitFor(() => expect(loads).toBe(1));
|
|
195
|
+
expect(screen.queryByTestId('pane')).toBeNull();
|
|
196
|
+
await act(async () => release());
|
|
197
|
+
expect(await screen.findByTestId('pane')).toBeInTheDocument();
|
|
198
|
+
|
|
199
|
+
gate = new Promise((resolve) => {
|
|
200
|
+
release = resolve;
|
|
201
|
+
});
|
|
202
|
+
act(() => lab?.addTrial('SelfStoring'));
|
|
203
|
+
expect(document.querySelectorAll('.lk-trial--loading')).toHaveLength(1);
|
|
204
|
+
await act(async () => release());
|
|
205
|
+
await waitFor(() => expect(screen.getAllByTestId('pane')).toHaveLength(2));
|
|
206
|
+
expect(loads).toBe(2);
|
|
207
|
+
});
|
|
166
208
|
});
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* follow. The overlay's own geometry is covered in
|
|
5
5
|
* `annotations/Annotations.overlay.test.tsx`.
|
|
6
6
|
*/
|
|
7
|
-
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
7
|
+
import { act, fireEvent, render, screen, within } from '@testing-library/react';
|
|
8
8
|
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
9
9
|
import { useAnnotations, useAnnotationsOptional } from '../annotations/AnnotationsContext';
|
|
10
10
|
import type { AnnotationsApi } from '../annotations/types';
|
|
11
11
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
12
12
|
import { Lab } from '../lab/Lab';
|
|
13
|
+
import { LabContext, type LabContextValue } from '../lab/LabContext';
|
|
13
14
|
|
|
14
15
|
beforeAll(() => {
|
|
15
16
|
HTMLCanvasElement.prototype.getContext = vi.fn(
|
|
@@ -19,9 +20,9 @@ beforeAll(() => {
|
|
|
19
20
|
|
|
20
21
|
let api: AnnotationsApi | null = null;
|
|
21
22
|
|
|
22
|
-
function Pane() {
|
|
23
|
+
function Pane({ tool }: { tool: string | null }) {
|
|
23
24
|
api = useAnnotations();
|
|
24
|
-
return <div data-testid="pane" data-marks={api.query().length} />;
|
|
25
|
+
return <div data-testid="pane" data-marks={api.query().length} data-tool={tool ?? ''} />;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function marks(): AnnotationsApi {
|
|
@@ -33,7 +34,7 @@ const annotating = defineInstrument<Record<string, never>, Record<string, never>
|
|
|
33
34
|
name: 'Annotating',
|
|
34
35
|
defaultConfig: () => ({}),
|
|
35
36
|
initialState: () => ({}),
|
|
36
|
-
render: () => <Pane />,
|
|
37
|
+
render: (ctx) => <Pane tool={ctx.trial.activeToolId} />,
|
|
37
38
|
annotations: {
|
|
38
39
|
targets: () => [{ id: 'pane', ref: { current: null }, content: { w: 200, h: 100 } }],
|
|
39
40
|
},
|
|
@@ -54,7 +55,16 @@ const plain = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
|
54
55
|
describe('an instrument that declares annotations', () => {
|
|
55
56
|
it('gets a drawing palette', () => {
|
|
56
57
|
render(<Lab instruments={[annotating]} defaultInstrument="Annotating" />);
|
|
57
|
-
for (const label of [
|
|
58
|
+
for (const label of [
|
|
59
|
+
'Interact',
|
|
60
|
+
'Select',
|
|
61
|
+
'Freehand',
|
|
62
|
+
'Line',
|
|
63
|
+
'Arrow',
|
|
64
|
+
'Rectangle',
|
|
65
|
+
'Ellipse',
|
|
66
|
+
'Text',
|
|
67
|
+
]) {
|
|
58
68
|
expect(screen.getByRole('button', { name: label })).toBeTruthy();
|
|
59
69
|
}
|
|
60
70
|
});
|
|
@@ -64,14 +74,56 @@ describe('an instrument that declares annotations', () => {
|
|
|
64
74
|
expect(screen.getByTestId('pane').dataset.marks).toBe('0');
|
|
65
75
|
});
|
|
66
76
|
|
|
67
|
-
it('starts in
|
|
77
|
+
it('starts in interact, so a first click reaches the instrument', () => {
|
|
68
78
|
render(<Lab instruments={[annotating]} defaultInstrument="Annotating" />);
|
|
69
|
-
expect(screen.getByRole('button', { name: '
|
|
79
|
+
expect(screen.getByRole('button', { name: 'Interact' }).getAttribute('aria-current')).toBe(
|
|
70
80
|
'true',
|
|
71
81
|
);
|
|
72
82
|
});
|
|
73
83
|
});
|
|
74
84
|
|
|
85
|
+
let labRef: LabContextValue | null = null;
|
|
86
|
+
|
|
87
|
+
function CaptureLab() {
|
|
88
|
+
return (
|
|
89
|
+
<LabContext.Consumer>
|
|
90
|
+
{(value) => {
|
|
91
|
+
if (value) labRef = value;
|
|
92
|
+
return null;
|
|
93
|
+
}}
|
|
94
|
+
</LabContext.Consumer>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe('the annotation tool', () => {
|
|
99
|
+
it("is offered in the lab's rail, not in a trial's", () => {
|
|
100
|
+
render(<Lab instruments={[annotating]} defaultInstrument="Annotating" />);
|
|
101
|
+
expect(screen.getAllByRole('button', { name: 'Rectangle' })).toHaveLength(1);
|
|
102
|
+
const trial = screen.getByRole('region', { name: /trial/i });
|
|
103
|
+
expect(within(trial).queryByRole('button', { name: 'Rectangle' })).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('is one tool across every trial', () => {
|
|
107
|
+
// A tool is what the hand is holding, not a property of a picture: picking
|
|
108
|
+
// Rectangle once has to arm it wherever the next mark is drawn.
|
|
109
|
+
labRef = null;
|
|
110
|
+
render(
|
|
111
|
+
<Lab instruments={[annotating]} defaultInstrument="Annotating">
|
|
112
|
+
<CaptureLab />
|
|
113
|
+
</Lab>,
|
|
114
|
+
);
|
|
115
|
+
act(() => labRef?.addTrial('Annotating'));
|
|
116
|
+
const panes = screen.getAllByTestId('pane');
|
|
117
|
+
expect(panes).toHaveLength(2);
|
|
118
|
+
for (const p of panes) expect(p.dataset.tool).toBe('pointer');
|
|
119
|
+
|
|
120
|
+
act(() => {
|
|
121
|
+
fireEvent.click(screen.getByRole('button', { name: 'Rectangle' }));
|
|
122
|
+
});
|
|
123
|
+
for (const p of screen.getAllByTestId('pane')) expect(p.dataset.tool).toBe('rect');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
75
127
|
describe('the trial undo chrome over marks', () => {
|
|
76
128
|
it('gets undo and redo without the instrument declaring `undo`', () => {
|
|
77
129
|
// Weasel history is the authority for marks, so the capability that
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { auto } from '../config/auto';
|
|
5
|
+
import { f } from '../config/builder';
|
|
4
6
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
7
|
+
import type { Instrument } from '../instrument/types';
|
|
5
8
|
import { Lab } from '../lab/Lab';
|
|
6
9
|
|
|
7
10
|
interface St {
|
|
@@ -74,6 +77,35 @@ describe('an instrument that draws', () => {
|
|
|
74
77
|
});
|
|
75
78
|
});
|
|
76
79
|
|
|
80
|
+
describe('an auto config path', () => {
|
|
81
|
+
it('draws with the resolved value, which is what the panel shows too', async () => {
|
|
82
|
+
const draw = vi.fn();
|
|
83
|
+
const instrument = defineInstrument({
|
|
84
|
+
name: 'Auto',
|
|
85
|
+
config: f.schema({
|
|
86
|
+
gap: f
|
|
87
|
+
.number(12)
|
|
88
|
+
.auto(() => 18)
|
|
89
|
+
.range(0, 48)
|
|
90
|
+
.initial(auto),
|
|
91
|
+
}),
|
|
92
|
+
initialState: () => ({ n: 0 }),
|
|
93
|
+
render: () => null,
|
|
94
|
+
canvas: { layers: [{ id: 'main', draw }] },
|
|
95
|
+
});
|
|
96
|
+
render(<Lab instruments={[instrument as Instrument]} defaultInstrument="Auto" />);
|
|
97
|
+
await new Promise((r) => requestAnimationFrame(() => r(null)));
|
|
98
|
+
|
|
99
|
+
expect(draw).toHaveBeenCalledWith(
|
|
100
|
+
expect.anything(),
|
|
101
|
+
expect.objectContaining({ config: { gap: 18 } }),
|
|
102
|
+
);
|
|
103
|
+
// The panel is handed the raw config, but a ghosted control draws what the
|
|
104
|
+
// resolver decided — a handle at 12 under a readout saying 18 reads as a bug.
|
|
105
|
+
expect(screen.getByRole('slider', { name: 'Gap' })).toHaveValue('18');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
77
109
|
describe('LabProps.instruments', () => {
|
|
78
110
|
it('accepts a typed instrument with no cast', () => {
|
|
79
111
|
// The assignment is the assertion: this file fails to compile if
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { auto } from '../config/auto';
|
|
3
4
|
import { f } from '../config/builder';
|
|
4
5
|
import type { ConfigRule, ControlRenderer } from '../config/types';
|
|
5
6
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
@@ -132,14 +133,18 @@ describe('a nested config value, end to end', () => {
|
|
|
132
133
|
const { instrument } = nestedInstrument();
|
|
133
134
|
render(<Lab instruments={[instrument]} defaultInstrument="Nested" />);
|
|
134
135
|
expect(screen.getByText('20')).toBeInTheDocument();
|
|
135
|
-
fireEvent.change(screen.getByLabelText('Cell size'), {
|
|
136
|
+
fireEvent.change(screen.getByLabelText('Cell size'), {
|
|
137
|
+
target: { value: '40' },
|
|
138
|
+
});
|
|
136
139
|
expect(screen.getByText('40')).toBeInTheDocument();
|
|
137
140
|
});
|
|
138
141
|
|
|
139
142
|
it('hands onConfigChange the whole tree, with the sibling branch intact', () => {
|
|
140
143
|
const { instrument, seen } = nestedInstrument();
|
|
141
144
|
render(<Lab instruments={[instrument]} defaultInstrument="Nested" />);
|
|
142
|
-
fireEvent.change(screen.getByLabelText('Cell size'), {
|
|
145
|
+
fireEvent.change(screen.getByLabelText('Cell size'), {
|
|
146
|
+
target: { value: '40' },
|
|
147
|
+
});
|
|
143
148
|
expect(seen.at(-1)?.config).toEqual({
|
|
144
149
|
showGrid: true,
|
|
145
150
|
grid: { size: 40, color: '#ffffff' },
|
|
@@ -150,3 +155,71 @@ describe('a nested config value, end to end', () => {
|
|
|
150
155
|
});
|
|
151
156
|
});
|
|
152
157
|
});
|
|
158
|
+
|
|
159
|
+
describe('a config written as auto', () => {
|
|
160
|
+
const unpinnable = () => {
|
|
161
|
+
const seen: { config: unknown; prev: unknown }[] = [];
|
|
162
|
+
const instrument = defineInstrument({
|
|
163
|
+
name: 'Unpinnable',
|
|
164
|
+
config: f.schema({
|
|
165
|
+
width: f.number(432),
|
|
166
|
+
gap: f.number(12).auto((c) => (c.width as number) / 24),
|
|
167
|
+
}),
|
|
168
|
+
initialState: () => ({ gap: 12 }),
|
|
169
|
+
onConfigChange: (config, prev, state) => {
|
|
170
|
+
seen.push({ config, prev });
|
|
171
|
+
return { ...state, gap: (config as { gap: number }).gap };
|
|
172
|
+
},
|
|
173
|
+
chrome: [
|
|
174
|
+
{
|
|
175
|
+
id: 'unpin-gap',
|
|
176
|
+
region: 'toolbar' as const,
|
|
177
|
+
render: (ctx) => (
|
|
178
|
+
<button type="button" onClick={() => ctx.setConfig('gap', auto)}>
|
|
179
|
+
unpin gap
|
|
180
|
+
</button>
|
|
181
|
+
),
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
render: (ctx) => <output>{String((ctx.state as { gap: number }).gap)}</output>,
|
|
185
|
+
});
|
|
186
|
+
return { instrument, seen };
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
it('hands onConfigChange the resolved config, never the sentinel', () => {
|
|
190
|
+
const { instrument, seen } = unpinnable();
|
|
191
|
+
render(<Lab instruments={[instrument]} defaultInstrument="Unpinnable" />);
|
|
192
|
+
fireEvent.click(screen.getByText('unpin gap'));
|
|
193
|
+
expect(seen.at(-1)?.config).toEqual({ width: 432, gap: 18 });
|
|
194
|
+
expect(seen.at(-1)?.prev).toEqual({ width: 432, gap: 12 });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('keeps the state that write derived free of the sentinel', () => {
|
|
198
|
+
const { instrument } = unpinnable();
|
|
199
|
+
render(<Lab instruments={[instrument]} defaultInstrument="Unpinnable" />);
|
|
200
|
+
fireEvent.click(screen.getByText('unpin gap'));
|
|
201
|
+
expect(screen.getByText('18')).toBeInTheDocument();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("shows the trial's unpinned path in the settings panel", () => {
|
|
205
|
+
const instrument = defineInstrument({
|
|
206
|
+
name: 'Ghosted',
|
|
207
|
+
config: f.schema({
|
|
208
|
+
width: f.number(432),
|
|
209
|
+
gap: f
|
|
210
|
+
.number(12)
|
|
211
|
+
.initial(auto)
|
|
212
|
+
.auto((c) => (c.width as number) / 24)
|
|
213
|
+
.range(0, 48),
|
|
214
|
+
}),
|
|
215
|
+
initialState: () => ({}),
|
|
216
|
+
render: () => null,
|
|
217
|
+
});
|
|
218
|
+
const { container } = render(<Lab instruments={[instrument]} defaultInstrument="Ghosted" />);
|
|
219
|
+
expect(screen.getByRole('button', { name: /Gap/ })).toHaveAttribute('aria-pressed', 'false');
|
|
220
|
+
expect(screen.getByRole('button', { name: /Width/ })).toHaveAttribute('aria-pressed', 'true');
|
|
221
|
+
// 432 / 24. A ghosted control draws what the resolver decided rather than
|
|
222
|
+
// the pinned 12 underneath it, and un-pinning writes back what it drew.
|
|
223
|
+
expect((container.querySelector('input[type=range]') as HTMLInputElement).value).toBe('18');
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -36,7 +36,7 @@ function makeInstrument(name: string, n: number, delay: number) {
|
|
|
36
36
|
describe('a trial whose instrument declares a job', () => {
|
|
37
37
|
it('shows progress in the trial chrome', async () => {
|
|
38
38
|
const fast = makeInstrument('fast', 3, 1);
|
|
39
|
-
render(<Lab instruments={[fast]} defaultInstrument="fast"
|
|
39
|
+
render(<Lab instruments={[fast]} defaultInstrument="fast" />);
|
|
40
40
|
const bar = await waitFor(() => screen.getByRole('progressbar', { name: /job progress/i }));
|
|
41
41
|
await waitFor(() => {
|
|
42
42
|
// A job that reported a total drives a determinate bar, so the count and
|
|
@@ -49,7 +49,7 @@ describe('a trial whose instrument declares a job', () => {
|
|
|
49
49
|
|
|
50
50
|
it('offers a cancel control while running, and drops it once cancelled', async () => {
|
|
51
51
|
const slow = makeInstrument('slow', 40, 20);
|
|
52
|
-
render(<Lab instruments={[slow]} defaultInstrument="slow"
|
|
52
|
+
render(<Lab instruments={[slow]} defaultInstrument="slow" />);
|
|
53
53
|
const cancel = await waitFor(() => screen.getByRole('button', { name: /cancel/i }));
|
|
54
54
|
await userEvent.click(cancel);
|
|
55
55
|
await waitFor(() => {
|
|
@@ -64,7 +64,7 @@ describe('a trial whose instrument declares a job', () => {
|
|
|
64
64
|
initialState: () => ({ items: [] }),
|
|
65
65
|
render: () => null,
|
|
66
66
|
});
|
|
67
|
-
render(<Lab instruments={[plain]} defaultInstrument="plain"
|
|
67
|
+
render(<Lab instruments={[plain]} defaultInstrument="plain" />);
|
|
68
68
|
await waitFor(() => {
|
|
69
69
|
expect(document.querySelector('.lk-trial')).toBeInTheDocument();
|
|
70
70
|
});
|
package/src/trial/Trial.less
CHANGED
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
// from its own workspace by nothing but a hairline. Raised reads as a panel,
|
|
10
10
|
// and in dark it is translucent, so the backdrop still shows through.
|
|
11
11
|
background: var(--wzl-surface-raised);
|
|
12
|
-
border: var(--wzl-border-w) solid var(--wzl-border-
|
|
12
|
+
border: var(--wzl-border-w) solid var(--wzl-border-strong);
|
|
13
13
|
border-radius: var(--wzl-radius-md);
|
|
14
14
|
box-shadow: 0 1px 3px var(--wzl-shadow);
|
|
15
15
|
overflow: hidden;
|
|
16
16
|
|
|
17
|
+
&[data-focused='true'] {
|
|
18
|
+
border-color: var(--wzl-accent);
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
&__body {
|
|
18
22
|
display: flex;
|
|
19
23
|
flex: 1;
|
|
@@ -96,21 +100,6 @@
|
|
|
96
100
|
flex-shrink: 0;
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
// The strip's own seam. windease positions and sizes it; this is only what
|
|
100
|
-
// it looks like.
|
|
101
|
-
&__panes [data-affordance-kind='resize-x'] {
|
|
102
|
-
background: var(--wzl-border);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
&__panes .windease-affordance-hit:hover [data-affordance-kind='resize-x'],
|
|
106
|
-
&__panes .windease-affordance-hit:focus-visible [data-affordance-kind='resize-x'] {
|
|
107
|
-
background: var(--wzl-accent);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
&__panes .windease-affordance-hit:focus-visible {
|
|
111
|
-
outline: none;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
103
|
&--unknown {
|
|
115
104
|
display: flex;
|
|
116
105
|
align-items: center;
|
|
@@ -126,16 +115,6 @@
|
|
|
126
115
|
padding: 0 var(--wzl-space-sm);
|
|
127
116
|
}
|
|
128
117
|
|
|
129
|
-
.lk-toolbar__load-select {
|
|
130
|
-
font-size: var(--wzl-font-size-sm);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Same `width: 100%` default as the header's Select — pinned so it cannot
|
|
134
|
-
// absorb the toolbar's slack. Placeholder-only, so it need only fit "Load…".
|
|
135
|
-
.lk-root .lk-toolbar__load-select {
|
|
136
|
-
width: 88px;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
118
|
.lk-sidebar__placeholder {
|
|
140
119
|
padding: var(--wzl-space-sm);
|
|
141
120
|
color: var(--wzl-fg-muted);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
2
|
import type { Instrument } from '../instrument/types';
|
|
3
3
|
import { LabContext, type LabContextValue } from '../lab/LabContext';
|
|
4
|
-
import { noneAdapter } from '../state/adapters';
|
|
5
4
|
import { LabStoreContext } from '../state/context';
|
|
6
5
|
import { createLabStore } from '../state/store';
|
|
7
6
|
import type { TrialRecord } from '../state/types';
|
|
@@ -27,7 +26,7 @@ const record: TrialRecord = {
|
|
|
27
26
|
};
|
|
28
27
|
|
|
29
28
|
function Harness() {
|
|
30
|
-
const store = createLabStore(
|
|
29
|
+
const store = createLabStore();
|
|
31
30
|
const lab: LabContextValue = {
|
|
32
31
|
instruments: [stub],
|
|
33
32
|
trials: [record],
|
|
@@ -36,6 +35,9 @@ function Harness() {
|
|
|
36
35
|
closeTrial: noop,
|
|
37
36
|
resetTrial: noop,
|
|
38
37
|
reorderTrials: noop,
|
|
38
|
+
swapTrial: noop,
|
|
39
|
+
focusedTrialId: record.id,
|
|
40
|
+
focusTrial: noop,
|
|
39
41
|
savedSnapshots: [],
|
|
40
42
|
saveSnapshot: noop,
|
|
41
43
|
loadSnapshot: noop,
|
package/src/trial/Trial.test.tsx
CHANGED
|
@@ -35,10 +35,7 @@ type ChromeProps = Parameters<typeof TrialChrome>[0];
|
|
|
35
35
|
* and section folds live on the record, so a test that outlives one mount
|
|
36
36
|
* keeps its own store and hands it to both. */
|
|
37
37
|
function makeChromeStore() {
|
|
38
|
-
const store = createLabStore(
|
|
39
|
-
storageKey: 'test',
|
|
40
|
-
storage: { read: () => null, write: () => {} },
|
|
41
|
-
});
|
|
38
|
+
const store = createLabStore();
|
|
42
39
|
const { undoStack: _undo, ...seed } = stubRecord;
|
|
43
40
|
store.getState().addTrial(seed);
|
|
44
41
|
return store;
|
|
@@ -65,6 +62,9 @@ function ChromeHarness({
|
|
|
65
62
|
closeTrial: vi.fn(),
|
|
66
63
|
resetTrial: vi.fn(),
|
|
67
64
|
reorderTrials: vi.fn(),
|
|
65
|
+
swapTrial: vi.fn(),
|
|
66
|
+
focusedTrialId: stubRecord.id,
|
|
67
|
+
focusTrial: vi.fn(),
|
|
68
68
|
savedSnapshots: [],
|
|
69
69
|
saveSnapshot: vi.fn(),
|
|
70
70
|
loadSnapshot: vi.fn(),
|
|
@@ -205,6 +205,33 @@ describe('<TrialChrome>', () => {
|
|
|
205
205
|
expect(screen.getByRole('region', { name: 'Trial Sprocket 7' })).toBeInTheDocument();
|
|
206
206
|
});
|
|
207
207
|
|
|
208
|
+
it("reads the instrument's title over its name, and returns to it on setTitle(null)", () => {
|
|
209
|
+
const titled: Instrument = { ...stubInstrument, title: 'Stub / Default' };
|
|
210
|
+
render(
|
|
211
|
+
<ChromeHarness
|
|
212
|
+
instrument={titled}
|
|
213
|
+
chrome={[
|
|
214
|
+
{
|
|
215
|
+
id: 'rename',
|
|
216
|
+
region: 'titlebar',
|
|
217
|
+
item: { icon: Glyph, label: 'Rename', onActivate: (ctx) => ctx.setTitle('Sprocket 7') },
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'untitle',
|
|
221
|
+
region: 'titlebar',
|
|
222
|
+
item: { icon: Glyph, label: 'Untitle', onActivate: (ctx) => ctx.setTitle(null) },
|
|
223
|
+
},
|
|
224
|
+
]}
|
|
225
|
+
/>,
|
|
226
|
+
);
|
|
227
|
+
expect(screen.getByRole('region', { name: 'Trial Stub / Default' })).toBeInTheDocument();
|
|
228
|
+
expect(screen.getByText('Stub / Default')).toBeInTheDocument();
|
|
229
|
+
fireEvent.click(screen.getByRole('button', { name: 'Rename' }));
|
|
230
|
+
expect(screen.getByRole('region', { name: 'Trial Sprocket 7' })).toBeInTheDocument();
|
|
231
|
+
fireEvent.click(screen.getByRole('button', { name: 'Untitle' }));
|
|
232
|
+
expect(screen.getByRole('region', { name: 'Trial Stub / Default' })).toBeInTheDocument();
|
|
233
|
+
});
|
|
234
|
+
|
|
208
235
|
it('Cmd+S triggers saveSnapshot', () => {
|
|
209
236
|
const saveSnapshot = vi.fn();
|
|
210
237
|
render(<ChromeHarness labOverrides={{ saveSnapshot }} />);
|