@weasel-js/labkit 1.7.1 → 1.7.2
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/dist/chrome/index.js +4 -4
- package/dist/{chunk-GAHELLJT.js → chunk-RODIY7HI.js} +7 -7
- package/dist/{chunk-GAHELLJT.js.map → chunk-RODIY7HI.js.map} +1 -1
- package/dist/{chunk-4KSC7NXL.js → chunk-U6UQVGFP.js} +1984 -1892
- package/dist/chunk-U6UQVGFP.js.map +1 -0
- package/dist/chunk-WFALF2X2.js +302 -0
- package/dist/chunk-WFALF2X2.js.map +1 -0
- package/dist/{chunk-JFOJESJF.js → chunk-WWJDPNZZ.js} +22 -22
- package/dist/chunk-WWJDPNZZ.js.map +1 -0
- package/dist/{chunk-CEGFXTBD.js → chunk-XRX5S5NT.js} +4 -4
- package/dist/{chunk-CEGFXTBD.js.map → chunk-XRX5S5NT.js.map} +1 -1
- package/dist/{chunk-FB4MIZOO.js → chunk-XZDCEMEL.js} +6 -6
- package/dist/chunk-XZDCEMEL.js.map +1 -0
- package/dist/controls/index.d.ts +55 -3
- package/dist/controls/index.js +3 -2
- package/dist/fonts/oswald-tabular-digits.woff2 +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +135 -134
- package/dist/index.js.map +1 -1
- package/dist/layers/index.js +1 -1
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.js +3 -3
- package/dist/styles.css +228 -11
- package/package.json +11 -11
- package/src/annotations/capture.browser.test.ts +9 -3
- package/src/canvas/Stage.test.tsx +34 -25
- package/src/chrome/labRegions.test.tsx +37 -26
- package/src/chrome/regions/PaletteRegion.test.tsx +2 -2
- package/src/controls/ControlMatrix.less +245 -0
- package/src/controls/ControlMatrix.stories.less +13 -0
- package/src/controls/ControlMatrix.stories.tsx +135 -0
- package/src/controls/ControlMatrix.test.tsx +206 -0
- package/src/controls/ControlMatrix.tsx +446 -0
- package/src/controls/ControlPanel.tsx +4 -2
- package/src/controls/index.ts +6 -0
- package/src/dragdrop/dragDrop.test.tsx +34 -33
- package/src/index.ts +6 -0
- package/src/lab/Lab.chrome.test.tsx +15 -10
- package/src/lab/Lab.nebula.test.tsx +5 -5
- package/src/lab/Lab.surface.test.tsx +30 -21
- package/src/lab/Lab.test.tsx +75 -74
- package/src/lab/Lab.undock.test.tsx +15 -10
- package/src/lab/LabHeader.test.tsx +15 -14
- package/src/lab/LabPalette.test.tsx +12 -7
- package/src/lab/LabShell.less +1 -2
- package/src/lab/LabZoom.test.tsx +55 -44
- package/src/lab/Workspace.surface.test.tsx +16 -13
- package/src/lab/Workspace.test.tsx +32 -27
- package/src/overview/OverviewMarks.test.tsx +9 -8
- package/src/overview/TrialOverview.test.tsx +18 -17
- package/src/primitives/FpsMeter.less +1 -2
- package/src/primitives/JobProgress.less +1 -2
- package/src/primitives/ScaleIndicator.less +1 -1
- package/src/primitives/StatusBar.less +1 -1
- package/src/specimen/Specimen.less +1 -0
- package/src/styles.less +3 -0
- package/src/trial/Trial.annotations.persist.test.tsx +20 -9
- package/src/trial/Trial.annotations.test.tsx +46 -41
- package/src/trial/Trial.canvas.test.tsx +21 -17
- package/src/trial/Trial.config.test.tsx +34 -27
- package/src/trial/Trial.loupe.test.tsx +27 -18
- package/src/trial/Trial.targets.test.tsx +9 -8
- package/src/trial/Trial.test.tsx +51 -46
- package/src/trial/Trial.trialId.test.tsx +7 -6
- package/dist/chunk-4KSC7NXL.js.map +0 -1
- package/dist/chunk-FB4MIZOO.js.map +0 -1
- package/dist/chunk-JFOJESJF.js.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
.lk-fps-meter {
|
|
2
|
+
.numeric();
|
|
2
3
|
display: inline-flex;
|
|
3
4
|
align-items: center;
|
|
4
5
|
white-space: nowrap;
|
|
5
6
|
gap: var(--wzl-space-xs);
|
|
6
|
-
font-family: var(--wzl-font-mono);
|
|
7
7
|
font-size: var(--wzl-font-size-sm);
|
|
8
8
|
color: var(--wzl-fg-muted);
|
|
9
9
|
}
|
|
@@ -14,6 +14,5 @@
|
|
|
14
14
|
a 240Hz display reads three digits, and a readout that grows into them
|
|
15
15
|
pushes whatever shares its row. */
|
|
16
16
|
min-width: 3ch;
|
|
17
|
-
font-variant-numeric: tabular-nums;
|
|
18
17
|
text-align: right;
|
|
19
18
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
.lk-status-bar {
|
|
2
|
+
.numeric();
|
|
2
3
|
display: flex;
|
|
3
4
|
align-items: center;
|
|
4
5
|
/* One line, always. A bar that grows a second line takes it from the canvas
|
|
@@ -11,7 +12,6 @@
|
|
|
11
12
|
border-top: 1px solid var(--wzl-border);
|
|
12
13
|
font-size: var(--wzl-font-size-sm);
|
|
13
14
|
color: var(--wzl-fg-muted);
|
|
14
|
-
font-family: var(--wzl-font-mono);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
.lk-status-bar-section {
|
package/src/styles.less
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Left as a CSS @import for the consumer's bundler to resolve; build-css.mjs
|
|
3
3
|
// swaps it for the faces themselves, pointed at dist/fonts/.
|
|
4
4
|
@import (css) '@weasel-js/theme/faces.css';
|
|
5
|
+
// The numeric face's one definition, taken as a mixin: `.numeric();`.
|
|
6
|
+
@import (reference, less) '@weasel-js/theme/numeric.module.css';
|
|
5
7
|
@import './theme/base.less';
|
|
6
8
|
@import './lab/Workspace.less';
|
|
7
9
|
@import './lab/LabShell.less';
|
|
@@ -22,6 +24,7 @@
|
|
|
22
24
|
@import './chrome/regions/PaletteRegion.less';
|
|
23
25
|
@import './chrome/regions/SidebarRegion.less';
|
|
24
26
|
@import './controls/ControlPanel.less';
|
|
27
|
+
@import './controls/ControlMatrix.less';
|
|
25
28
|
@import './canvas/CanvasStack.less';
|
|
26
29
|
@import './canvas/Stage.less';
|
|
27
30
|
@import './canvas/LinkedCursor.less';
|
|
@@ -4,7 +4,8 @@
|
|
|
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,
|
|
7
|
+
import { act, screen, waitFor } from '@testing-library/react';
|
|
8
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
8
9
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
10
|
import { useAnnotations } from '../annotations/AnnotationsContext';
|
|
10
11
|
import type { AnnotationsApi, SerializedAnnotations } from '../annotations/types';
|
|
@@ -83,7 +84,7 @@ const selfStoring = defineInstrument<Record<string, never>, Record<string, never
|
|
|
83
84
|
|
|
84
85
|
async function mount(backing: Map<string, unknown>, which: InstrumentList[number] = inspector) {
|
|
85
86
|
api = null;
|
|
86
|
-
const view =
|
|
87
|
+
const view = await renderSettled(
|
|
87
88
|
<Lab
|
|
88
89
|
instruments={[which]}
|
|
89
90
|
defaultInstrument={which.name}
|
|
@@ -121,7 +122,10 @@ describe('a trial that persists its marks', () => {
|
|
|
121
122
|
it('brings them back on a remount from the same storage', async () => {
|
|
122
123
|
const backing = new Map<string, unknown>();
|
|
123
124
|
const first = await mount(backing);
|
|
124
|
-
|
|
125
|
+
let id = '';
|
|
126
|
+
act(() => {
|
|
127
|
+
id = marks().add(MARK, { angle: 12 });
|
|
128
|
+
});
|
|
125
129
|
first.unmount();
|
|
126
130
|
await waitFor(() => expect(storedTrial(backing)?.annotations).toBeDefined());
|
|
127
131
|
|
|
@@ -139,7 +143,9 @@ describe('a trial that persists its marks', () => {
|
|
|
139
143
|
const backing = new Map<string, unknown>();
|
|
140
144
|
await mount(backing);
|
|
141
145
|
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'Date'] });
|
|
142
|
-
|
|
146
|
+
act(() => {
|
|
147
|
+
marks().add(MARK);
|
|
148
|
+
});
|
|
143
149
|
await act(() => vi.advanceTimersByTimeAsync(20));
|
|
144
150
|
// Not a claim about either interval — a claim that a store write is not
|
|
145
151
|
// synchronous with a scene notification.
|
|
@@ -151,8 +157,11 @@ describe('a trial that persists its marks', () => {
|
|
|
151
157
|
it('flushes on unmount, so the last mark before a close is not lost', async () => {
|
|
152
158
|
const backing = new Map<string, unknown>();
|
|
153
159
|
const first = await mount(backing);
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
let id = '';
|
|
161
|
+
act(() => {
|
|
162
|
+
id = marks().add(MARK);
|
|
163
|
+
marks().add({ ...MARK, frac: { x: 0.6, y: 0.6, w: 0.1, h: 0.1 } });
|
|
164
|
+
});
|
|
156
165
|
first.unmount();
|
|
157
166
|
// Nothing was written before the unmount: neither debounce had elapsed.
|
|
158
167
|
await waitFor(() => expect(storedTrial(backing)?.annotations).toBeDefined());
|
|
@@ -165,7 +174,9 @@ describe('a trial that persists its marks', () => {
|
|
|
165
174
|
it('leaves the slot alone when the instrument owns the storage', async () => {
|
|
166
175
|
const backing = new Map<string, unknown>();
|
|
167
176
|
const first = await mount(backing, selfStoring);
|
|
168
|
-
|
|
177
|
+
act(() => {
|
|
178
|
+
marks().add(MARK);
|
|
179
|
+
});
|
|
169
180
|
first.unmount();
|
|
170
181
|
await waitFor(() => expect(owned?.version).toBe(1));
|
|
171
182
|
await waitFor(() => expect(storedTrial(backing)).toBeDefined());
|
|
@@ -181,7 +192,7 @@ describe('a trial that persists its marks', () => {
|
|
|
181
192
|
gate = new Promise((resolve) => {
|
|
182
193
|
release = resolve;
|
|
183
194
|
});
|
|
184
|
-
|
|
195
|
+
await renderSettled(
|
|
185
196
|
<Lab
|
|
186
197
|
instruments={[selfStoring]}
|
|
187
198
|
defaultInstrument="SelfStoring"
|
|
@@ -199,7 +210,7 @@ describe('a trial that persists its marks', () => {
|
|
|
199
210
|
gate = new Promise((resolve) => {
|
|
200
211
|
release = resolve;
|
|
201
212
|
});
|
|
202
|
-
act(() => lab?.addTrial('SelfStoring'));
|
|
213
|
+
await act(async () => lab?.addTrial('SelfStoring'));
|
|
203
214
|
expect(document.querySelectorAll('.lk-trial--loading')).toHaveLength(1);
|
|
204
215
|
await act(async () => release());
|
|
205
216
|
await waitFor(() => expect(screen.getAllByTestId('pane')).toHaveLength(2));
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* follow. The overlay's own geometry is covered in
|
|
5
5
|
* `annotations/Annotations.overlay.test.tsx`.
|
|
6
6
|
*/
|
|
7
|
-
import { act, fireEvent,
|
|
7
|
+
import { act, fireEvent, screen, within } from '@testing-library/react';
|
|
8
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
8
9
|
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
9
10
|
import { useAnnotations, useAnnotationsOptional } from '../annotations/AnnotationsContext';
|
|
10
11
|
import type { AnnotationsApi } from '../annotations/types';
|
|
@@ -53,8 +54,8 @@ const plain = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
describe('an instrument that declares annotations', () => {
|
|
56
|
-
it('gets a drawing palette', () => {
|
|
57
|
-
|
|
57
|
+
it('gets a drawing palette', async () => {
|
|
58
|
+
await renderSettled(<Lab instruments={[annotating]} defaultInstrument="Annotating" />);
|
|
58
59
|
for (const label of [
|
|
59
60
|
'Interact',
|
|
60
61
|
'Select',
|
|
@@ -69,7 +70,7 @@ describe('an instrument that declares annotations', () => {
|
|
|
69
70
|
}
|
|
70
71
|
});
|
|
71
72
|
|
|
72
|
-
it('gets only the tools it names', () => {
|
|
73
|
+
it('gets only the tools it names', async () => {
|
|
73
74
|
const narrow = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
74
75
|
...annotating,
|
|
75
76
|
name: 'Narrow',
|
|
@@ -78,20 +79,20 @@ describe('an instrument that declares annotations', () => {
|
|
|
78
79
|
tools: ['pointer', 'select'],
|
|
79
80
|
},
|
|
80
81
|
});
|
|
81
|
-
|
|
82
|
+
await renderSettled(<Lab instruments={[narrow]} defaultInstrument="Narrow" />);
|
|
82
83
|
expect(screen.getByRole('button', { name: 'Interact' })).toBeTruthy();
|
|
83
84
|
expect(screen.getByRole('button', { name: 'Select' })).toBeTruthy();
|
|
84
85
|
expect(screen.queryByRole('button', { name: 'Freehand' })).toBeNull();
|
|
85
86
|
expect(screen.queryByRole('button', { name: 'Rectangle' })).toBeNull();
|
|
86
87
|
});
|
|
87
88
|
|
|
88
|
-
it('puts the store in reach of its own render', () => {
|
|
89
|
-
|
|
89
|
+
it('puts the store in reach of its own render', async () => {
|
|
90
|
+
await renderSettled(<Lab instruments={[annotating]} defaultInstrument="Annotating" />);
|
|
90
91
|
expect(screen.getByTestId('pane').dataset.marks).toBe('0');
|
|
91
92
|
});
|
|
92
93
|
|
|
93
|
-
it('starts in interact, so a first click reaches the instrument', () => {
|
|
94
|
-
|
|
94
|
+
it('starts in interact, so a first click reaches the instrument', async () => {
|
|
95
|
+
await renderSettled(<Lab instruments={[annotating]} defaultInstrument="Annotating" />);
|
|
95
96
|
expect(screen.getByRole('button', { name: 'Interact' }).getAttribute('aria-current')).toBe(
|
|
96
97
|
'true',
|
|
97
98
|
);
|
|
@@ -112,28 +113,28 @@ function CaptureLab() {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
describe('the annotation tool', () => {
|
|
115
|
-
it("is offered in the lab's rail, not in a trial's", () => {
|
|
116
|
-
|
|
116
|
+
it("is offered in the lab's rail, not in a trial's", async () => {
|
|
117
|
+
await renderSettled(<Lab instruments={[annotating]} defaultInstrument="Annotating" />);
|
|
117
118
|
expect(screen.getAllByRole('button', { name: 'Rectangle' })).toHaveLength(1);
|
|
118
119
|
const trial = screen.getByRole('region', { name: /trial/i });
|
|
119
120
|
expect(within(trial).queryByRole('button', { name: 'Rectangle' })).toBeNull();
|
|
120
121
|
});
|
|
121
122
|
|
|
122
|
-
it('is one tool across every trial', () => {
|
|
123
|
+
it('is one tool across every trial', async () => {
|
|
123
124
|
// A tool is what the hand is holding, not a property of a picture: picking
|
|
124
125
|
// Rectangle once has to arm it wherever the next mark is drawn.
|
|
125
126
|
labRef = null;
|
|
126
|
-
|
|
127
|
+
await renderSettled(
|
|
127
128
|
<Lab instruments={[annotating]} defaultInstrument="Annotating">
|
|
128
129
|
<CaptureLab />
|
|
129
130
|
</Lab>,
|
|
130
131
|
);
|
|
131
|
-
act(() => labRef?.addTrial('Annotating'));
|
|
132
|
+
await act(async () => labRef?.addTrial('Annotating'));
|
|
132
133
|
const panes = screen.getAllByTestId('pane');
|
|
133
134
|
expect(panes).toHaveLength(2);
|
|
134
135
|
for (const p of panes) expect(p.dataset.tool).toBe('pointer');
|
|
135
136
|
|
|
136
|
-
act(() => {
|
|
137
|
+
await act(async () => {
|
|
137
138
|
fireEvent.click(screen.getByRole('button', { name: 'Rectangle' }));
|
|
138
139
|
});
|
|
139
140
|
for (const p of screen.getAllByTestId('pane')) expect(p.dataset.tool).toBe('rect');
|
|
@@ -141,39 +142,39 @@ describe('the annotation tool', () => {
|
|
|
141
142
|
});
|
|
142
143
|
|
|
143
144
|
describe('the trial undo chrome over marks', () => {
|
|
144
|
-
it('gets undo and redo without the instrument declaring `undo`', () => {
|
|
145
|
+
it('gets undo and redo without the instrument declaring `undo`', async () => {
|
|
145
146
|
// Weasel history is the authority for marks, so the capability that
|
|
146
147
|
// creates them is what earns the buttons.
|
|
147
|
-
|
|
148
|
+
await renderSettled(<Lab instruments={[annotating]} defaultInstrument="Annotating" />);
|
|
148
149
|
expect(screen.getByRole('button', { name: 'Undo' })).toBeTruthy();
|
|
149
150
|
expect(screen.getByRole('button', { name: 'Redo' })).toBeTruthy();
|
|
150
151
|
});
|
|
151
152
|
|
|
152
|
-
it('takes back a mark, and puts it back', () => {
|
|
153
|
-
|
|
153
|
+
it('takes back a mark, and puts it back', async () => {
|
|
154
|
+
await renderSettled(<Lab instruments={[annotating]} defaultInstrument="Annotating" />);
|
|
154
155
|
const undo = screen.getByRole('button', { name: 'Undo' });
|
|
155
156
|
expect(undo.getAttribute('aria-disabled') ?? undo.getAttribute('disabled')).not.toBeNull();
|
|
156
157
|
|
|
157
|
-
act(() => {
|
|
158
|
+
await act(async () => {
|
|
158
159
|
marks().add({ target: 'pane', kind: 'rect', frac: { x: 0.1, y: 0.1, w: 0.2, h: 0.2 } });
|
|
159
160
|
});
|
|
160
161
|
expect(screen.getByTestId('pane').dataset.marks).toBe('1');
|
|
161
162
|
|
|
162
|
-
act(() => {
|
|
163
|
+
await act(async () => {
|
|
163
164
|
fireEvent.click(screen.getByRole('button', { name: 'Undo' }));
|
|
164
165
|
});
|
|
165
166
|
expect(screen.getByTestId('pane').dataset.marks).toBe('0');
|
|
166
167
|
|
|
167
|
-
act(() => {
|
|
168
|
+
await act(async () => {
|
|
168
169
|
fireEvent.click(screen.getByRole('button', { name: 'Redo' }));
|
|
169
170
|
});
|
|
170
171
|
expect(screen.getByTestId('pane').dataset.marks).toBe('1');
|
|
171
172
|
});
|
|
172
173
|
|
|
173
|
-
it('lists the marks in a sidebar panel', () => {
|
|
174
|
-
|
|
174
|
+
it('lists the marks in a sidebar panel', async () => {
|
|
175
|
+
await renderSettled(<Lab instruments={[annotating]} defaultInstrument="Annotating" />);
|
|
175
176
|
expect(screen.getByText('Marks')).toBeTruthy();
|
|
176
|
-
act(() => {
|
|
177
|
+
await act(async () => {
|
|
177
178
|
marks().add({ target: 'pane', kind: 'line', frac: { x: 0, y: 0, w: 0.5, h: 0 } });
|
|
178
179
|
});
|
|
179
180
|
expect(screen.getAllByRole('listitem')).toHaveLength(1);
|
|
@@ -190,7 +191,7 @@ function GrowingPane({ n, grow }: { n: number; grow: () => void }) {
|
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
describe('targets that follow the trial state', () => {
|
|
193
|
-
it('reach a reader of the store once the change commits', () => {
|
|
194
|
+
it('reach a reader of the store once the change commits', async () => {
|
|
194
195
|
// The store reads the committed state, so a reader rendering in the same
|
|
195
196
|
// pass as the change sees the one before; the trial tells it to read again.
|
|
196
197
|
const growing = defineInstrument<{ n: number }, Record<string, never>>({
|
|
@@ -209,9 +210,9 @@ describe('targets that follow the trial state', () => {
|
|
|
209
210
|
})),
|
|
210
211
|
},
|
|
211
212
|
});
|
|
212
|
-
|
|
213
|
+
await renderSettled(<Lab instruments={[growing]} defaultInstrument="Growing" />);
|
|
213
214
|
expect(screen.getByTestId('growing').dataset.targets).toBe('1');
|
|
214
|
-
act(() => {
|
|
215
|
+
await act(async () => {
|
|
215
216
|
fireEvent.click(screen.getByTestId('growing'));
|
|
216
217
|
});
|
|
217
218
|
expect(screen.getByTestId('growing').textContent).toBe('2');
|
|
@@ -232,30 +233,34 @@ describe("the capability's selection mode", () => {
|
|
|
232
233
|
...(selection ? { selection } : {}),
|
|
233
234
|
},
|
|
234
235
|
});
|
|
235
|
-
const selectBoth = (): readonly string[] => {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
236
|
+
const selectBoth = async (): Promise<readonly string[]> => {
|
|
237
|
+
let both: [string, string] = ['', ''];
|
|
238
|
+
await act(async () => {
|
|
239
|
+
const onA = marks().add({ target: 'a', kind: 'rect', frac: { x: 0, y: 0, w: 0.1, h: 0.1 } });
|
|
240
|
+
const onB = marks().add({ target: 'b', kind: 'rect', frac: { x: 0, y: 0, w: 0.1, h: 0.1 } });
|
|
241
|
+
marks().setSelection([onA, onB]);
|
|
242
|
+
both = [onA, onB];
|
|
243
|
+
});
|
|
244
|
+
return both;
|
|
240
245
|
};
|
|
241
246
|
|
|
242
|
-
it('keeps the selection to one target by default', () => {
|
|
243
|
-
|
|
244
|
-
const [onA] = selectBoth();
|
|
247
|
+
it('keeps the selection to one target by default', async () => {
|
|
248
|
+
await renderSettled(<Lab instruments={[twoTargets()]} defaultInstrument="Two" />);
|
|
249
|
+
const [onA] = await selectBoth();
|
|
245
250
|
expect(marks().selection()).toEqual([onA]);
|
|
246
251
|
});
|
|
247
252
|
|
|
248
|
-
it("reaches the trial's store when the instrument asks for per-target", () => {
|
|
249
|
-
|
|
253
|
+
it("reaches the trial's store when the instrument asks for per-target", async () => {
|
|
254
|
+
await renderSettled(<Lab instruments={[twoTargets('per-target')]} defaultInstrument="Two" />);
|
|
250
255
|
expect(marks().selection()).toEqual([]);
|
|
251
|
-
const both = selectBoth();
|
|
256
|
+
const both = await selectBoth();
|
|
252
257
|
expect(marks().selection()).toEqual(both);
|
|
253
258
|
});
|
|
254
259
|
});
|
|
255
260
|
|
|
256
261
|
describe('an instrument that declares none', () => {
|
|
257
|
-
it('gets no store and no palette', () => {
|
|
258
|
-
|
|
262
|
+
it('gets no store and no palette', async () => {
|
|
263
|
+
await renderSettled(<Lab instruments={[plain]} defaultInstrument="Plain" />);
|
|
259
264
|
expect(screen.getByTestId('plain').dataset.hasApi).toBe('false');
|
|
260
265
|
expect(screen.queryByRole('button', { name: 'Rectangle' })).toBeNull();
|
|
261
266
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { act, screen } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
3
4
|
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import { auto } from '../config/auto';
|
|
5
6
|
import { f } from '../config/builder';
|
|
@@ -7,6 +8,9 @@ import { defineInstrument } from '../instrument/defineInstrument';
|
|
|
7
8
|
import type { Instrument } from '../instrument/types';
|
|
8
9
|
import { Lab } from '../lab/Lab';
|
|
9
10
|
|
|
11
|
+
/** Waits one animation frame inside act, so what the frame loop sets commits there. */
|
|
12
|
+
const nextFrame = () => act(() => new Promise((r) => requestAnimationFrame(() => r(null))));
|
|
13
|
+
|
|
10
14
|
interface St {
|
|
11
15
|
n: number;
|
|
12
16
|
}
|
|
@@ -44,8 +48,8 @@ function makeInstrument(
|
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
describe('an instrument that draws', () => {
|
|
47
|
-
it('still renders its own DOM, as an overlay over the canvas', () => {
|
|
48
|
-
const { container } =
|
|
51
|
+
it('still renders its own DOM, as an overlay over the canvas', async () => {
|
|
52
|
+
const { container } = await renderSettled(
|
|
49
53
|
<Lab instruments={[makeInstrument(() => undefined)]} defaultInstrument="Probe" />,
|
|
50
54
|
);
|
|
51
55
|
expect(screen.getByTestId('readout')).toHaveTextContent('n = 7');
|
|
@@ -57,8 +61,8 @@ describe('an instrument that draws', () => {
|
|
|
57
61
|
|
|
58
62
|
it('draws with the camera applied, so world geometry lands where the view says', async () => {
|
|
59
63
|
const draw = vi.fn();
|
|
60
|
-
|
|
61
|
-
await
|
|
64
|
+
await renderSettled(<Lab instruments={[makeInstrument(draw)]} defaultInstrument="Probe" />);
|
|
65
|
+
await nextFrame();
|
|
62
66
|
|
|
63
67
|
expect(draw).toHaveBeenCalled();
|
|
64
68
|
expect(ctxSpies.translate).toHaveBeenCalledWith(30, 40);
|
|
@@ -67,8 +71,8 @@ describe('an instrument that draws', () => {
|
|
|
67
71
|
|
|
68
72
|
it('passes zoom in the args so a layer can keep line widths unscaled', async () => {
|
|
69
73
|
const draw = vi.fn();
|
|
70
|
-
|
|
71
|
-
await
|
|
74
|
+
await renderSettled(<Lab instruments={[makeInstrument(draw)]} defaultInstrument="Probe" />);
|
|
75
|
+
await nextFrame();
|
|
72
76
|
|
|
73
77
|
expect(draw).toHaveBeenCalledWith(
|
|
74
78
|
expect.anything(),
|
|
@@ -93,10 +97,10 @@ describe('an auto config path', () => {
|
|
|
93
97
|
render: () => null,
|
|
94
98
|
canvas: { layers: [{ id: 'main', draw }] },
|
|
95
99
|
});
|
|
96
|
-
const { container } =
|
|
100
|
+
const { container } = await renderSettled(
|
|
97
101
|
<Lab instruments={[instrument as Instrument]} defaultInstrument="Auto" />,
|
|
98
102
|
);
|
|
99
|
-
await
|
|
103
|
+
await nextFrame();
|
|
100
104
|
|
|
101
105
|
expect(draw).toHaveBeenCalledWith(
|
|
102
106
|
expect.anything(),
|
|
@@ -111,11 +115,11 @@ describe('an auto config path', () => {
|
|
|
111
115
|
});
|
|
112
116
|
|
|
113
117
|
describe('LabProps.instruments', () => {
|
|
114
|
-
it('accepts a typed instrument with no cast', () => {
|
|
118
|
+
it('accepts a typed instrument with no cast', async () => {
|
|
115
119
|
// The assignment is the assertion: this file fails to compile if
|
|
116
120
|
// `defineInstrument<St, ...>`'s result stops being assignable.
|
|
117
121
|
const typed = makeInstrument(() => undefined);
|
|
118
|
-
|
|
122
|
+
await renderSettled(<Lab instruments={[typed]} defaultInstrument="Probe" />);
|
|
119
123
|
expect(screen.getByTestId('readout')).toBeInTheDocument();
|
|
120
124
|
});
|
|
121
125
|
|
|
@@ -132,8 +136,8 @@ describe('LabProps.instruments', () => {
|
|
|
132
136
|
const instrument = makeInstrument(vi.fn(), {
|
|
133
137
|
worldSpec: { origin: { x: 0.5, y: 0.5 }, yAxis: 'up' },
|
|
134
138
|
});
|
|
135
|
-
|
|
136
|
-
await
|
|
139
|
+
await renderSettled(<Lab instruments={[instrument]} defaultInstrument="Probe" />);
|
|
140
|
+
await nextFrame();
|
|
137
141
|
|
|
138
142
|
// Origin (400,300) plus the instrument's pan (30,40); y runs up.
|
|
139
143
|
expect(ctxSpies.translate).toHaveBeenCalledWith(430, 340);
|
|
@@ -156,9 +160,9 @@ describe('LabProps.instruments', () => {
|
|
|
156
160
|
pan: { x: width / 4, y: height / 4 },
|
|
157
161
|
}),
|
|
158
162
|
});
|
|
159
|
-
|
|
160
|
-
await
|
|
161
|
-
await
|
|
163
|
+
await renderSettled(<Lab instruments={[instrument]} defaultInstrument="Probe" />);
|
|
164
|
+
await nextFrame();
|
|
165
|
+
await nextFrame();
|
|
162
166
|
|
|
163
167
|
expect(ctxSpies.translate).toHaveBeenCalledWith(200, 150);
|
|
164
168
|
vi.restoreAllMocks();
|
|
@@ -182,7 +186,7 @@ describe('LabProps.instruments', () => {
|
|
|
182
186
|
},
|
|
183
187
|
layers: { ids: ['grid', 'glyph'] },
|
|
184
188
|
});
|
|
185
|
-
|
|
189
|
+
await renderSettled(<Lab instruments={[instrument]} defaultInstrument="Probe" />);
|
|
186
190
|
expect([...seen]).toEqual(['grid', 'glyph']);
|
|
187
191
|
|
|
188
192
|
await userEvent.click(screen.getByRole('checkbox', { name: 'Show grid' }));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { fireEvent,
|
|
1
|
+
import { fireEvent, screen } from '@testing-library/react';
|
|
2
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
2
3
|
import { describe, expect, it } from 'vitest';
|
|
3
4
|
import { auto } from '../config/auto';
|
|
4
5
|
import { f } from '../config/builder';
|
|
@@ -27,19 +28,19 @@ const legacyInstrument = defineInstrument({
|
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
describe('a trial renders its instrument config', () => {
|
|
30
|
-
it('renders a builder schema into the settings sidebar', () => {
|
|
31
|
-
|
|
31
|
+
it('renders a builder schema into the settings sidebar', async () => {
|
|
32
|
+
await renderSettled(<Lab instruments={[schemaInstrument]} defaultInstrument="Schema" />);
|
|
32
33
|
expect(screen.getByLabelText('Show grid')).toBeInTheDocument();
|
|
33
34
|
expect(screen.getByText('Grid spacing')).toBeInTheDocument();
|
|
34
35
|
});
|
|
35
36
|
|
|
36
|
-
it('renders a legacy ConfigField list through the same path', () => {
|
|
37
|
-
|
|
37
|
+
it('renders a legacy ConfigField list through the same path', async () => {
|
|
38
|
+
await renderSettled(<Lab instruments={[legacyInstrument]} defaultInstrument="Legacy" />);
|
|
38
39
|
expect(screen.getByLabelText('Show grid')).toBeInTheDocument();
|
|
39
40
|
});
|
|
40
41
|
|
|
41
|
-
it('writes a config change back to the trial', () => {
|
|
42
|
-
|
|
42
|
+
it('writes a config change back to the trial', async () => {
|
|
43
|
+
await renderSettled(<Lab instruments={[schemaInstrument]} defaultInstrument="Schema" />);
|
|
43
44
|
const checkbox = screen.getByLabelText('Show grid') as HTMLInputElement;
|
|
44
45
|
expect(checkbox.checked).toBe(true);
|
|
45
46
|
fireEvent.click(checkbox);
|
|
@@ -48,7 +49,7 @@ describe('a trial renders its instrument config', () => {
|
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
describe('lab-wide config seams', () => {
|
|
51
|
-
it('applies a lab-wide rule before labkit inference', () => {
|
|
52
|
+
it('applies a lab-wide rule before labkit inference', async () => {
|
|
52
53
|
const inst = defineInstrument({
|
|
53
54
|
name: 'Ruled',
|
|
54
55
|
config: f.schema({ tintColor: f.value('#ffffff') }),
|
|
@@ -56,22 +57,24 @@ describe('lab-wide config seams', () => {
|
|
|
56
57
|
render: () => null,
|
|
57
58
|
});
|
|
58
59
|
const colorByName: ConfigRule = (ctx) => (ctx.key.endsWith('Color') ? { kind: 'color' } : null);
|
|
59
|
-
|
|
60
|
+
await renderSettled(
|
|
61
|
+
<Lab instruments={[inst]} defaultInstrument="Ruled" configRules={[colorByName]} />,
|
|
62
|
+
);
|
|
60
63
|
expect(screen.getByLabelText('Tint color')).toHaveAttribute('type', 'color');
|
|
61
64
|
});
|
|
62
65
|
|
|
63
|
-
it('without the rule the same leaf falls back to a text input', () => {
|
|
66
|
+
it('without the rule the same leaf falls back to a text input', async () => {
|
|
64
67
|
const inst = defineInstrument({
|
|
65
68
|
name: 'Unruled',
|
|
66
69
|
config: f.schema({ tintColor: f.value('#ffffff') }),
|
|
67
70
|
initialState: () => ({}),
|
|
68
71
|
render: () => null,
|
|
69
72
|
});
|
|
70
|
-
|
|
73
|
+
await renderSettled(<Lab instruments={[inst]} defaultInstrument="Unruled" />);
|
|
71
74
|
expect(screen.getByLabelText('Tint color')).toHaveAttribute('type', 'text');
|
|
72
75
|
});
|
|
73
76
|
|
|
74
|
-
it('supplies a lab-wide control for a kind labkit does not ship', () => {
|
|
77
|
+
it('supplies a lab-wide control for a kind labkit does not ship', async () => {
|
|
75
78
|
const inst = defineInstrument({
|
|
76
79
|
name: 'Custom',
|
|
77
80
|
config: f.schema({ offset: f.custom('vector2', { x: 3 }) }),
|
|
@@ -81,29 +84,31 @@ describe('lab-wide config seams', () => {
|
|
|
81
84
|
const vector2: ControlRenderer = (ctx) => (
|
|
82
85
|
<span>vec:{String((ctx.value as { x: number }).x)}</span>
|
|
83
86
|
);
|
|
84
|
-
|
|
87
|
+
await renderSettled(
|
|
88
|
+
<Lab instruments={[inst]} defaultInstrument="Custom" controls={{ vector2 }} />,
|
|
89
|
+
);
|
|
85
90
|
expect(screen.getByText('vec:3')).toBeInTheDocument();
|
|
86
91
|
});
|
|
87
92
|
|
|
88
|
-
it('shows the placeholder when no control is supplied for that kind', () => {
|
|
93
|
+
it('shows the placeholder when no control is supplied for that kind', async () => {
|
|
89
94
|
const inst = defineInstrument({
|
|
90
95
|
name: 'Unwired',
|
|
91
96
|
config: f.schema({ offset: f.custom('vector2', { x: 3 }) }),
|
|
92
97
|
initialState: () => ({}),
|
|
93
98
|
render: () => null,
|
|
94
99
|
});
|
|
95
|
-
|
|
100
|
+
await renderSettled(<Lab instruments={[inst]} defaultInstrument="Unwired" />);
|
|
96
101
|
expect(screen.getByText(/vector2/)).toBeInTheDocument();
|
|
97
102
|
});
|
|
98
103
|
|
|
99
|
-
it('omits the settings section for an instrument with no config', () => {
|
|
104
|
+
it('omits the settings section for an instrument with no config', async () => {
|
|
100
105
|
const inst = defineInstrument({
|
|
101
106
|
name: 'Bare',
|
|
102
107
|
defaultConfig: () => ({}),
|
|
103
108
|
initialState: () => ({}),
|
|
104
109
|
render: () => null,
|
|
105
110
|
});
|
|
106
|
-
|
|
111
|
+
await renderSettled(<Lab instruments={[inst]} defaultInstrument="Bare" />);
|
|
107
112
|
expect(screen.queryByText('Settings')).not.toBeInTheDocument();
|
|
108
113
|
});
|
|
109
114
|
});
|
|
@@ -129,9 +134,9 @@ describe('a nested config value, end to end', () => {
|
|
|
129
134
|
return { instrument, seen };
|
|
130
135
|
};
|
|
131
136
|
|
|
132
|
-
it('renders a nested leaf and writes the change back into the tree', () => {
|
|
137
|
+
it('renders a nested leaf and writes the change back into the tree', async () => {
|
|
133
138
|
const { instrument } = nestedInstrument();
|
|
134
|
-
|
|
139
|
+
await renderSettled(<Lab instruments={[instrument]} defaultInstrument="Nested" />);
|
|
135
140
|
expect(screen.getByText('20')).toBeInTheDocument();
|
|
136
141
|
fireEvent.change(screen.getByLabelText('Cell size'), {
|
|
137
142
|
target: { value: '40' },
|
|
@@ -139,9 +144,9 @@ describe('a nested config value, end to end', () => {
|
|
|
139
144
|
expect(screen.getByText('40')).toBeInTheDocument();
|
|
140
145
|
});
|
|
141
146
|
|
|
142
|
-
it('hands onConfigChange the whole tree, with the sibling branch intact', () => {
|
|
147
|
+
it('hands onConfigChange the whole tree, with the sibling branch intact', async () => {
|
|
143
148
|
const { instrument, seen } = nestedInstrument();
|
|
144
|
-
|
|
149
|
+
await renderSettled(<Lab instruments={[instrument]} defaultInstrument="Nested" />);
|
|
145
150
|
fireEvent.change(screen.getByLabelText('Cell size'), {
|
|
146
151
|
target: { value: '40' },
|
|
147
152
|
});
|
|
@@ -186,22 +191,22 @@ describe('a config written as auto', () => {
|
|
|
186
191
|
return { instrument, seen };
|
|
187
192
|
};
|
|
188
193
|
|
|
189
|
-
it('hands onConfigChange the resolved config, never the sentinel', () => {
|
|
194
|
+
it('hands onConfigChange the resolved config, never the sentinel', async () => {
|
|
190
195
|
const { instrument, seen } = unpinnable();
|
|
191
|
-
|
|
196
|
+
await renderSettled(<Lab instruments={[instrument]} defaultInstrument="Unpinnable" />);
|
|
192
197
|
fireEvent.click(screen.getByText('unpin gap'));
|
|
193
198
|
expect(seen.at(-1)?.config).toEqual({ width: 432, gap: 18 });
|
|
194
199
|
expect(seen.at(-1)?.prev).toEqual({ width: 432, gap: 12 });
|
|
195
200
|
});
|
|
196
201
|
|
|
197
|
-
it('keeps the state that write derived free of the sentinel', () => {
|
|
202
|
+
it('keeps the state that write derived free of the sentinel', async () => {
|
|
198
203
|
const { instrument } = unpinnable();
|
|
199
|
-
|
|
204
|
+
await renderSettled(<Lab instruments={[instrument]} defaultInstrument="Unpinnable" />);
|
|
200
205
|
fireEvent.click(screen.getByText('unpin gap'));
|
|
201
206
|
expect(screen.getByText('18')).toBeInTheDocument();
|
|
202
207
|
});
|
|
203
208
|
|
|
204
|
-
it("shows the trial's unpinned path in the settings panel", () => {
|
|
209
|
+
it("shows the trial's unpinned path in the settings panel", async () => {
|
|
205
210
|
const instrument = defineInstrument({
|
|
206
211
|
name: 'Ghosted',
|
|
207
212
|
config: f.schema({
|
|
@@ -215,7 +220,9 @@ describe('a config written as auto', () => {
|
|
|
215
220
|
initialState: () => ({}),
|
|
216
221
|
render: () => null,
|
|
217
222
|
});
|
|
218
|
-
const { container } =
|
|
223
|
+
const { container } = await renderSettled(
|
|
224
|
+
<Lab instruments={[instrument]} defaultInstrument="Ghosted" />,
|
|
225
|
+
);
|
|
219
226
|
expect(screen.getByRole('button', { name: /Gap/ })).toHaveAttribute('aria-pressed', 'false');
|
|
220
227
|
expect(screen.getByRole('button', { name: /Width/ })).toHaveAttribute('aria-pressed', 'true');
|
|
221
228
|
// 432 / 24. An auto row holds what the resolver decided rather than
|