@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.
Files changed (67) hide show
  1. package/dist/chrome/index.js +4 -4
  2. package/dist/{chunk-GAHELLJT.js → chunk-RODIY7HI.js} +7 -7
  3. package/dist/{chunk-GAHELLJT.js.map → chunk-RODIY7HI.js.map} +1 -1
  4. package/dist/{chunk-4KSC7NXL.js → chunk-U6UQVGFP.js} +1984 -1892
  5. package/dist/chunk-U6UQVGFP.js.map +1 -0
  6. package/dist/chunk-WFALF2X2.js +302 -0
  7. package/dist/chunk-WFALF2X2.js.map +1 -0
  8. package/dist/{chunk-JFOJESJF.js → chunk-WWJDPNZZ.js} +22 -22
  9. package/dist/chunk-WWJDPNZZ.js.map +1 -0
  10. package/dist/{chunk-CEGFXTBD.js → chunk-XRX5S5NT.js} +4 -4
  11. package/dist/{chunk-CEGFXTBD.js.map → chunk-XRX5S5NT.js.map} +1 -1
  12. package/dist/{chunk-FB4MIZOO.js → chunk-XZDCEMEL.js} +6 -6
  13. package/dist/chunk-XZDCEMEL.js.map +1 -0
  14. package/dist/controls/index.d.ts +55 -3
  15. package/dist/controls/index.js +3 -2
  16. package/dist/fonts/oswald-tabular-digits.woff2 +0 -0
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +135 -134
  19. package/dist/index.js.map +1 -1
  20. package/dist/layers/index.js +1 -1
  21. package/dist/passthrough/weasel-ui.js +1 -1
  22. package/dist/primitives/index.js +3 -3
  23. package/dist/styles.css +228 -11
  24. package/package.json +11 -11
  25. package/src/annotations/capture.browser.test.ts +9 -3
  26. package/src/canvas/Stage.test.tsx +34 -25
  27. package/src/chrome/labRegions.test.tsx +37 -26
  28. package/src/chrome/regions/PaletteRegion.test.tsx +2 -2
  29. package/src/controls/ControlMatrix.less +245 -0
  30. package/src/controls/ControlMatrix.stories.less +13 -0
  31. package/src/controls/ControlMatrix.stories.tsx +135 -0
  32. package/src/controls/ControlMatrix.test.tsx +206 -0
  33. package/src/controls/ControlMatrix.tsx +446 -0
  34. package/src/controls/ControlPanel.tsx +4 -2
  35. package/src/controls/index.ts +6 -0
  36. package/src/dragdrop/dragDrop.test.tsx +34 -33
  37. package/src/index.ts +6 -0
  38. package/src/lab/Lab.chrome.test.tsx +15 -10
  39. package/src/lab/Lab.nebula.test.tsx +5 -5
  40. package/src/lab/Lab.surface.test.tsx +30 -21
  41. package/src/lab/Lab.test.tsx +75 -74
  42. package/src/lab/Lab.undock.test.tsx +15 -10
  43. package/src/lab/LabHeader.test.tsx +15 -14
  44. package/src/lab/LabPalette.test.tsx +12 -7
  45. package/src/lab/LabShell.less +1 -2
  46. package/src/lab/LabZoom.test.tsx +55 -44
  47. package/src/lab/Workspace.surface.test.tsx +16 -13
  48. package/src/lab/Workspace.test.tsx +32 -27
  49. package/src/overview/OverviewMarks.test.tsx +9 -8
  50. package/src/overview/TrialOverview.test.tsx +18 -17
  51. package/src/primitives/FpsMeter.less +1 -2
  52. package/src/primitives/JobProgress.less +1 -2
  53. package/src/primitives/ScaleIndicator.less +1 -1
  54. package/src/primitives/StatusBar.less +1 -1
  55. package/src/specimen/Specimen.less +1 -0
  56. package/src/styles.less +3 -0
  57. package/src/trial/Trial.annotations.persist.test.tsx +20 -9
  58. package/src/trial/Trial.annotations.test.tsx +46 -41
  59. package/src/trial/Trial.canvas.test.tsx +21 -17
  60. package/src/trial/Trial.config.test.tsx +34 -27
  61. package/src/trial/Trial.loupe.test.tsx +27 -18
  62. package/src/trial/Trial.targets.test.tsx +9 -8
  63. package/src/trial/Trial.test.tsx +51 -46
  64. package/src/trial/Trial.trialId.test.tsx +7 -6
  65. package/dist/chunk-4KSC7NXL.js.map +0 -1
  66. package/dist/chunk-FB4MIZOO.js.map +0 -1
  67. package/dist/chunk-JFOJESJF.js.map +0 -1
@@ -1,4 +1,5 @@
1
- import { render, screen } from '@testing-library/react';
1
+ import { 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 type { Instrument } from '../instrument/types';
4
5
  import { Lab } from './Lab';
@@ -12,13 +13,13 @@ const Stub: Instrument = {
12
13
  const Other: Instrument = { ...Stub, name: 'Other' };
13
14
 
14
15
  describe('<LabHeader>', () => {
15
- it('offers an add-trial button when the lab has one instrument', () => {
16
- render(<Lab instruments={[Stub]} defaultInstrument="Stub" />);
16
+ it('offers an add-trial button when the lab has one instrument', async () => {
17
+ await renderSettled(<Lab instruments={[Stub]} defaultInstrument="Stub" />);
17
18
  expect(screen.getByRole('button', { name: /add trial/i })).toBeInTheDocument();
18
19
  });
19
20
 
20
- it('offers a menu instead when the lab has several', () => {
21
- render(<Lab instruments={[Stub, Other]} defaultInstrument="Stub" />);
21
+ it('offers a menu instead when the lab has several', async () => {
22
+ await renderSettled(<Lab instruments={[Stub, Other]} defaultInstrument="Stub" />);
22
23
  // Which instrument to add is now a choice, so the button opens a list.
23
24
  expect(screen.getByRole('button', { name: 'Add trial' })).toHaveAttribute('aria-haspopup');
24
25
  });
@@ -26,7 +27,7 @@ describe('<LabHeader>', () => {
26
27
  it('adds a trial of the instrument chosen from the menu', async () => {
27
28
  const { default: userEvent } = await import('@testing-library/user-event');
28
29
  const user = userEvent.setup();
29
- render(<Lab instruments={[Stub, Other]} defaultInstrument="Stub" />);
30
+ await renderSettled(<Lab instruments={[Stub, Other]} defaultInstrument="Stub" />);
30
31
  expect(screen.queryAllByLabelText(/^Trial Other$/)).toHaveLength(0);
31
32
  await user.click(screen.getByRole('button', { name: 'Add trial' }));
32
33
  await user.click(screen.getByRole('menuitem', { name: 'Other' }));
@@ -37,7 +38,7 @@ describe('<LabHeader>', () => {
37
38
  const { default: userEvent } = await import('@testing-library/user-event');
38
39
  const user = userEvent.setup();
39
40
  const titled: Instrument = { ...Other, title: 'Other / Titled' };
40
- render(<Lab instruments={[Stub, titled]} defaultInstrument="Stub" />);
41
+ await renderSettled(<Lab instruments={[Stub, titled]} defaultInstrument="Stub" />);
41
42
  await user.click(screen.getByRole('button', { name: 'Add trial' }));
42
43
  expect(screen.getByRole('menuitem', { name: 'Stub' })).toBeInTheDocument();
43
44
  await user.click(screen.getByRole('menuitem', { name: 'Other / Titled' }));
@@ -47,27 +48,27 @@ describe('<LabHeader>', () => {
47
48
  it('adds a trial when the button is used', async () => {
48
49
  const { default: userEvent } = await import('@testing-library/user-event');
49
50
  const user = userEvent.setup();
50
- render(<Lab instruments={[Stub]} defaultInstrument="Stub" />);
51
+ await renderSettled(<Lab instruments={[Stub]} defaultInstrument="Stub" />);
51
52
  expect(screen.getAllByLabelText(/^Trial Stub$/)).toHaveLength(1);
52
53
  await user.click(screen.getByRole('button', { name: /add trial/i }));
53
54
  expect(screen.getAllByLabelText(/^Trial Stub$/)).toHaveLength(2);
54
55
  });
55
56
 
56
- it('leaves the add-trial control out when the lab says so', () => {
57
- const { unmount } = render(
57
+ it('leaves the add-trial control out when the lab says so', async () => {
58
+ const { unmount } = await renderSettled(
58
59
  <Lab instruments={[Stub, Other]} defaultInstrument="Stub" addTrial={false} />,
59
60
  );
60
61
  expect(screen.queryByRole('button', { name: /add trial/i })).toBeNull();
61
62
  expect(screen.getByRole('button', { name: /^Theme: Auto/ })).toBeInTheDocument();
62
63
  unmount();
63
- render(<Lab instruments={[Stub]} defaultInstrument="Stub" addTrial={false} />);
64
+ await renderSettled(<Lab instruments={[Stub]} defaultInstrument="Stub" addTrial={false} />);
64
65
  expect(screen.queryByRole('button', { name: /add trial/i })).toBeNull();
65
66
  });
66
67
 
67
68
  it('rotates the color mode from one button', async () => {
68
69
  const { default: userEvent } = await import('@testing-library/user-event');
69
70
  const user = userEvent.setup();
70
- render(<Lab instruments={[Stub]} defaultInstrument="Stub" />);
71
+ await renderSettled(<Lab instruments={[Stub]} defaultInstrument="Stub" />);
71
72
  expect(screen.queryByRole('radiogroup')).toBeNull();
72
73
  for (const [now, next] of [
73
74
  ['Auto', 'Light'],
@@ -82,8 +83,8 @@ describe('<LabHeader>', () => {
82
83
  expect(screen.getByRole('button', { name: /^Theme: Auto/ })).toBeInTheDocument();
83
84
  });
84
85
 
85
- it('puts the theme switch after the header content a consumer passes', () => {
86
- render(
86
+ it('puts the theme switch after the header content a consumer passes', async () => {
87
+ await renderSettled(
87
88
  <Lab instruments={[Stub]} defaultInstrument="Stub">
88
89
  <button type="button">Consumer</button>
89
90
  </Lab>,
@@ -1,4 +1,5 @@
1
- import { fireEvent, render, screen } from '@testing-library/react';
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 type { Instrument } from '../instrument/types';
4
5
  import { Lab } from './Lab';
@@ -16,19 +17,23 @@ const tools = [
16
17
  ];
17
18
 
18
19
  describe('the lab palette', () => {
19
- it('does not render when the lab declares no tools', () => {
20
- render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" />);
20
+ it('does not render when the lab declares no tools', async () => {
21
+ await renderSettled(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" />);
21
22
  expect(screen.queryByRole('toolbar', { name: 'Tools' })).toBeNull();
22
23
  });
23
24
 
24
- it('renders one button per declared tool', () => {
25
- render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" tools={tools} />);
25
+ it('renders one button per declared tool', async () => {
26
+ await renderSettled(
27
+ <Lab title="T" instruments={[bare]} defaultInstrument="Bare" tools={tools} />,
28
+ );
26
29
  expect(screen.getByRole('button', { name: 'Pick' })).toBeInTheDocument();
27
30
  expect(screen.getByRole('button', { name: 'Pan' })).toBeInTheDocument();
28
31
  });
29
32
 
30
- it('marks the chosen tool current after a click', () => {
31
- render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" tools={tools} />);
33
+ it('marks the chosen tool current after a click', async () => {
34
+ await renderSettled(
35
+ <Lab title="T" instruments={[bare]} defaultInstrument="Bare" tools={tools} />,
36
+ );
32
37
  fireEvent.click(screen.getByRole('button', { name: 'Pan' }));
33
38
  expect(screen.getByRole('button', { name: 'Pan' })).toHaveAttribute('aria-current', 'true');
34
39
  });
@@ -162,6 +162,5 @@
162
162
  display: inline-block;
163
163
  min-width: 5ch;
164
164
  text-align: center;
165
- font-variant-numeric: tabular-nums;
166
- font-family: var(--wzl-font-numeric);
165
+ .numeric();
167
166
  }
@@ -1,4 +1,4 @@
1
- import { act, fireEvent, render, screen, within } from '@testing-library/react';
1
+ import { act, fireEvent, screen, within } from '@testing-library/react';
2
2
  import {
3
3
  ActionsProvider,
4
4
  type ActionsRegistry,
@@ -8,6 +8,7 @@ import {
8
8
  useActionsRegistry,
9
9
  useGestureDispatcher,
10
10
  } from '@weasel-js/core';
11
+ import { renderSettled } from '@weasel-js/react/testing/renderSettled';
11
12
  import { type ReactNode, useEffect } from 'react';
12
13
  import { beforeAll, describe, expect, it, vi } from 'vitest';
13
14
  import { defineInstrument } from '../instrument/defineInstrument';
@@ -54,16 +55,16 @@ function Capture({ children }: { children?: ReactNode }) {
54
55
  const zoomBar = () => within(screen.getByRole('toolbar', { name: 'Zoom' }));
55
56
  const readout = () => zoomBar().getByRole('button', { name: /^Zoom \d+%|^Zoom unavailable/ });
56
57
  const press = (name: string) =>
57
- act(() => {
58
+ act(async () => {
58
59
  fireEvent.click(zoomBar().getByRole('button', { name }));
59
60
  });
60
61
 
61
62
  const MOD = /mac/i.test(navigator.platform || navigator.userAgent)
62
63
  ? { metaKey: true }
63
64
  : { ctrlKey: true };
64
- function key(k: string): boolean {
65
+ async function key(k: string): Promise<boolean> {
65
66
  let prevented = false;
66
- act(() => {
67
+ await act(async () => {
67
68
  prevented = !fireEvent.keyDown(window, { key: k, ...MOD });
68
69
  });
69
70
  return prevented;
@@ -111,113 +112,123 @@ function stageZoom(container: HTMLElement, index = 0): number {
111
112
  }
112
113
 
113
114
  describe("the lab header's zoom controls", () => {
114
- it('step the focused trial through its camera and read its zoom back', () => {
115
- const { container } = render(<Lab instruments={[staged]} defaultInstrument="Staged" />);
115
+ it('step the focused trial through its camera and read its zoom back', async () => {
116
+ const { container } = await renderSettled(
117
+ <Lab instruments={[staged]} defaultInstrument="Staged" />,
118
+ );
116
119
  expect(readout()).toHaveTextContent('100%');
117
- press('Zoom in');
120
+ await press('Zoom in');
118
121
  expect(stageZoom(container)).toBeCloseTo(1.25);
119
122
  expect(readout()).toHaveTextContent('125%');
120
- press('Zoom out');
123
+ await press('Zoom out');
121
124
  expect(stageZoom(container)).toBeCloseTo(1);
122
- press('Zoom out');
125
+ await press('Zoom out');
123
126
  expect(readout()).toHaveTextContent('80%');
124
127
  });
125
128
 
126
- it('reset to actual size from the readout', () => {
127
- const { container } = render(<Lab instruments={[staged]} defaultInstrument="Staged" />);
128
- press('Zoom out');
129
- press('Zoom out');
130
- act(() => {
129
+ it('reset to actual size from the readout', async () => {
130
+ const { container } = await renderSettled(
131
+ <Lab instruments={[staged]} defaultInstrument="Staged" />,
132
+ );
133
+ await press('Zoom out');
134
+ await press('Zoom out');
135
+ await act(async () => {
131
136
  fireEvent.click(readout());
132
137
  });
133
138
  expect(stageZoom(container)).toBe(1);
134
139
  expect(readout()).toHaveTextContent('100%');
135
140
  });
136
141
 
137
- it("stop at the camera's own limit, and say so", () => {
138
- const { container } = render(<Lab instruments={[staged]} defaultInstrument="Staged" />);
139
- press('Zoom in');
140
- press('Zoom in');
142
+ it("stop at the camera's own limit, and say so", async () => {
143
+ const { container } = await renderSettled(
144
+ <Lab instruments={[staged]} defaultInstrument="Staged" />,
145
+ );
146
+ await press('Zoom in');
147
+ await press('Zoom in');
141
148
  expect(stageZoom(container)).toBe(1.5);
142
149
  expect(zoomBar().getByRole('button', { name: 'Zoom in' })).toBeDisabled();
143
150
  expect(zoomBar().getByRole('button', { name: 'Zoom out' })).toBeEnabled();
144
151
  });
145
152
 
146
- it('answer Mod+=, Mod+- and Mod+0, claiming the key from the browser', () => {
147
- const { container } = render(<Lab instruments={[staged]} defaultInstrument="Staged" />);
148
- expect(key('=')).toBe(true);
153
+ it('answer Mod+=, Mod+- and Mod+0, claiming the key from the browser', async () => {
154
+ const { container } = await renderSettled(
155
+ <Lab instruments={[staged]} defaultInstrument="Staged" />,
156
+ );
157
+ expect(await key('=')).toBe(true);
149
158
  expect(stageZoom(container)).toBeCloseTo(1.25);
150
- key('-');
151
- key('-');
159
+ await key('-');
160
+ await key('-');
152
161
  expect(stageZoom(container)).toBeCloseTo(0.8);
153
- key('0');
162
+ await key('0');
154
163
  expect(stageZoom(container)).toBe(1);
155
164
  });
156
165
 
157
- it('go inert over a trial with no camera, and leave its keys to the browser', () => {
158
- render(<Lab instruments={[plain]} defaultInstrument="Plain" />);
166
+ it('go inert over a trial with no camera, and leave its keys to the browser', async () => {
167
+ await renderSettled(<Lab instruments={[plain]} defaultInstrument="Plain" />);
159
168
  for (const name of ['Zoom out', 'Zoom in']) {
160
169
  expect(zoomBar().getByRole('button', { name })).toBeDisabled();
161
170
  }
162
171
  expect(readout()).toBeDisabled();
163
172
  expect(readout()).toHaveTextContent('–');
164
- expect(key('=')).toBe(false);
173
+ expect(await key('=')).toBe(false);
165
174
  });
166
175
 
167
- it("take the key ahead of a story's own binding in a trial with a camera", () => {
176
+ it("take the key ahead of a story's own binding in a trial with a camera", async () => {
168
177
  const story = vi.fn();
169
178
  const zooming = defineInstrument<Record<string, never>, Record<string, never>>({
170
179
  ...staged,
171
180
  name: 'Zooming',
172
181
  render: () => <StoryZoomKey run={story} />,
173
182
  });
174
- const { container } = render(<Lab instruments={[zooming]} defaultInstrument="Zooming" />);
175
- key('=');
183
+ const { container } = await renderSettled(
184
+ <Lab instruments={[zooming]} defaultInstrument="Zooming" />,
185
+ );
186
+ await key('=');
176
187
  expect(stageZoom(container)).toBeCloseTo(1.25);
177
188
  expect(story).not.toHaveBeenCalled();
178
189
  });
179
190
 
180
- it("leave the key to a story's own binding in a trial with no camera", () => {
191
+ it("leave the key to a story's own binding in a trial with no camera", async () => {
181
192
  const story = vi.fn();
182
193
  const zooming = defineInstrument<Record<string, never>, Record<string, never>>({
183
194
  ...plain,
184
195
  name: 'ZoomingPlain',
185
196
  render: () => <StoryZoomKey run={story} />,
186
197
  });
187
- render(<Lab instruments={[zooming]} defaultInstrument="ZoomingPlain" />);
188
- key('=');
198
+ await renderSettled(<Lab instruments={[zooming]} defaultInstrument="ZoomingPlain" />);
199
+ await key('=');
189
200
  expect(story).toHaveBeenCalledTimes(1);
190
201
  });
191
202
 
192
- it('follow the focus from trial to trial', () => {
193
- const { container } = render(
203
+ it('follow the focus from trial to trial', async () => {
204
+ const { container } = await renderSettled(
194
205
  <Lab instruments={[staged, plain]} defaultInstrument="Staged">
195
206
  <Capture />
196
207
  </Lab>,
197
208
  );
198
- act(() => lab?.addTrial('Staged'));
199
- act(() => lab?.addTrial('Plain'));
209
+ await act(async () => lab?.addTrial('Staged'));
210
+ await act(async () => lab?.addTrial('Plain'));
200
211
  const [first, second, third] = lab?.trials.map((t) => t.id) ?? [];
201
212
  expect(zoomBar().getByRole('button', { name: 'Zoom in' })).toBeDisabled();
202
213
 
203
- act(() => lab?.focusTrial(second ?? ''));
204
- press('Zoom out');
214
+ await act(async () => lab?.focusTrial(second ?? ''));
215
+ await press('Zoom out');
205
216
  expect(stageZoom(container, 1)).toBeCloseTo(0.8);
206
217
  expect(stageZoom(container, 0)).toBe(1);
207
218
  expect(readout()).toHaveTextContent('80%');
208
219
 
209
- act(() => lab?.focusTrial(first ?? ''));
220
+ await act(async () => lab?.focusTrial(first ?? ''));
210
221
  expect(readout()).toHaveTextContent('100%');
211
- key('=');
222
+ await key('=');
212
223
  expect(stageZoom(container, 0)).toBeCloseTo(1.25);
213
224
  expect(stageZoom(container, 1)).toBeCloseTo(0.8);
214
225
 
215
- act(() => lab?.focusTrial(third ?? ''));
226
+ await act(async () => lab?.focusTrial(third ?? ''));
216
227
  expect(readout()).toBeDisabled();
217
228
  });
218
229
 
219
- it('are left out when the lab says so', () => {
220
- render(<Lab instruments={[staged]} defaultInstrument="Staged" zoom={false} />);
230
+ it('are left out when the lab says so', async () => {
231
+ await renderSettled(<Lab instruments={[staged]} defaultInstrument="Staged" zoom={false} />);
221
232
  expect(screen.queryByRole('toolbar', { name: 'Zoom' })).toBeNull();
222
233
  });
223
234
  });
@@ -1,4 +1,5 @@
1
- import { render } from '@testing-library/react';
1
+ import { act } from '@testing-library/react';
2
+ import { renderSettled } from '@weasel-js/react/testing/renderSettled';
2
3
  import { describe, expect, it, vi } from 'vitest';
3
4
  import { SurfaceContext } from '../surface/SurfaceContext';
4
5
  import type { SurfaceHandle } from '../surface/useTiledSurface';
@@ -18,9 +19,9 @@ function fakeHandle(): SurfaceHandle {
18
19
  }
19
20
 
20
21
  describe('Workspace with a surface above it', () => {
21
- it('invalidates rects when the tile set changes, because a re-tile moves tiles', () => {
22
+ it('invalidates rects when the tile set changes, because a re-tile moves tiles', async () => {
22
23
  const handle = fakeHandle();
23
- const { rerender } = render(
24
+ const { rerender } = await renderSettled(
24
25
  <SurfaceContext.Provider value={handle}>
25
26
  <Workspace ids={['a']} viewport={{ w: 800, h: 600 }}>
26
27
  <div>a</div>
@@ -29,20 +30,22 @@ describe('Workspace with a surface above it', () => {
29
30
  );
30
31
  (handle.invalidateRects as ReturnType<typeof vi.fn>).mockClear();
31
32
 
32
- rerender(
33
- <SurfaceContext.Provider value={handle}>
34
- <Workspace ids={['a', 'b']} viewport={{ w: 800, h: 600 }}>
35
- <div>a</div>
36
- <div>b</div>
37
- </Workspace>
38
- </SurfaceContext.Provider>,
39
- );
33
+ await act(async () => {
34
+ rerender(
35
+ <SurfaceContext.Provider value={handle}>
36
+ <Workspace ids={['a', 'b']} viewport={{ w: 800, h: 600 }}>
37
+ <div>a</div>
38
+ <div>b</div>
39
+ </Workspace>
40
+ </SurfaceContext.Provider>,
41
+ );
42
+ });
40
43
 
41
44
  expect(handle.invalidateRects).toHaveBeenCalled();
42
45
  });
43
46
 
44
- it('renders unchanged with no surface above it', () => {
45
- const { getByText } = render(
47
+ it('renders unchanged with no surface above it', async () => {
48
+ const { getByText } = await renderSettled(
46
49
  <Workspace ids={['a']} viewport={{ w: 800, h: 600 }}>
47
50
  <div>a</div>
48
51
  </Workspace>,
@@ -1,4 +1,5 @@
1
- import { render, screen } from '@testing-library/react';
1
+ import { act, screen } from '@testing-library/react';
2
+ import { renderSettled } from '@weasel-js/react/testing/renderSettled';
2
3
  import { describe, expect, test, vi } from 'vitest';
3
4
  import { useTrialDrag } from '../trial/TrialDragContext';
4
5
  import { Workspace } from './Workspace';
@@ -13,8 +14,8 @@ function DragProbe({ label }: { label: string }) {
13
14
  const VIEWPORT = { w: 800, h: 600 };
14
15
 
15
16
  describe('Workspace', () => {
16
- test('renders all children', () => {
17
- render(
17
+ test('renders all children', async () => {
18
+ await renderSettled(
18
19
  <Workspace viewport={VIEWPORT}>
19
20
  <div>one</div>
20
21
  <div>two</div>
@@ -26,9 +27,9 @@ describe('Workspace', () => {
26
27
  expect(screen.getByText('three')).toBeInTheDocument();
27
28
  });
28
29
 
29
- test('keeps a child with the tile its id names when an earlier one closes', () => {
30
+ test('keeps a child with the tile its id names when an earlier one closes', async () => {
30
31
  const ids = ['a', 'b', 'c'];
31
- const { rerender, container } = render(
32
+ const { rerender, container } = await renderSettled(
32
33
  <Workspace ids={ids} viewport={VIEWPORT}>
33
34
  <div>a</div>
34
35
  <div>b</div>
@@ -37,18 +38,20 @@ describe('Workspace', () => {
37
38
  );
38
39
  expect(container.querySelector('[data-node="c"]')).toHaveTextContent('c');
39
40
 
40
- rerender(
41
- <Workspace ids={['b', 'c']} viewport={VIEWPORT}>
42
- <div>b</div>
43
- <div>c</div>
44
- </Workspace>,
45
- );
41
+ await act(async () => {
42
+ rerender(
43
+ <Workspace ids={['b', 'c']} viewport={VIEWPORT}>
44
+ <div>b</div>
45
+ <div>c</div>
46
+ </Workspace>,
47
+ );
48
+ });
46
49
  expect(container.querySelector('[data-node="c"]')).toHaveTextContent('c');
47
50
  expect(container.querySelector('[data-node="a"]')).toBeNull();
48
51
  });
49
52
 
50
- test('uses lk-workspace class', () => {
51
- const { container } = render(
53
+ test('uses lk-workspace class', async () => {
54
+ const { container } = await renderSettled(
52
55
  <Workspace viewport={VIEWPORT}>
53
56
  <div />
54
57
  </Workspace>,
@@ -56,8 +59,8 @@ describe('Workspace', () => {
56
59
  expect((container.firstChild as HTMLElement).className).toContain('lk-workspace');
57
60
  });
58
61
 
59
- test('renders resize affordances only when resizable', () => {
60
- const { container, rerender } = render(
62
+ test('renders resize affordances only when resizable', async () => {
63
+ const { container, rerender } = await renderSettled(
61
64
  <Workspace ids={['a', 'b']} viewport={VIEWPORT}>
62
65
  <div>a</div>
63
66
  <div>b</div>
@@ -65,17 +68,19 @@ describe('Workspace', () => {
65
68
  );
66
69
  expect(container.querySelectorAll('[role="separator"]')).toHaveLength(0);
67
70
 
68
- rerender(
69
- <Workspace ids={['a', 'b']} resizable viewport={VIEWPORT}>
70
- <div>a</div>
71
- <div>b</div>
72
- </Workspace>,
73
- );
71
+ await act(async () => {
72
+ rerender(
73
+ <Workspace ids={['a', 'b']} resizable viewport={VIEWPORT}>
74
+ <div>a</div>
75
+ <div>b</div>
76
+ </Workspace>,
77
+ );
78
+ });
74
79
  expect(container.querySelectorAll('[role="separator"]').length).toBeGreaterThan(0);
75
80
  });
76
81
 
77
- test('applies a saved extent to a tile as it registers', () => {
78
- const { container } = render(
82
+ test('applies a saved extent to a tile as it registers', async () => {
83
+ const { container } = await renderSettled(
79
84
  <Workspace
80
85
  ids={['a', 'b']}
81
86
  resizable
@@ -92,9 +97,9 @@ describe('Workspace', () => {
92
97
  expect(Number.parseFloat(a.style.width)).toBeGreaterThan(Number.parseFloat(b.style.width));
93
98
  });
94
99
 
95
- test('reports the order a drop would produce instead of applying it', () => {
100
+ test('reports the order a drop would produce instead of applying it', async () => {
96
101
  const onReorder = vi.fn();
97
- const { container } = render(
102
+ const { container } = await renderSettled(
98
103
  <Workspace ids={['a', 'b']} reorderable onReorder={onReorder} viewport={VIEWPORT}>
99
104
  <DragProbe label="a" />
100
105
  <DragProbe label="b" />
@@ -111,8 +116,8 @@ describe('Workspace', () => {
111
116
  expect(nodes).toEqual(['a', 'b']);
112
117
  });
113
118
 
114
- test('offers no drag source unless reorderable', () => {
115
- render(
119
+ test('offers no drag source unless reorderable', async () => {
120
+ await renderSettled(
116
121
  <Workspace ids={['a', 'b']} viewport={VIEWPORT}>
117
122
  <DragProbe label="a" />
118
123
  <DragProbe label="b" />
@@ -3,7 +3,8 @@
3
3
  * and redraws when one is added. jsdom lays nothing out, so the target's rect
4
4
  * and the stage's are mocked; the stage's camera is the identity there.
5
5
  */
6
- import { act, render } from '@testing-library/react';
6
+ import { act } from '@testing-library/react';
7
+ import { renderSettled } from '@weasel-js/react/testing/renderSettled';
7
8
  import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
8
9
  import { useAnnotations } from '../annotations/AnnotationsContext';
9
10
  import type { AnnotationsApi } from '../annotations/types';
@@ -33,7 +34,7 @@ let restore: (() => void) | null = null;
33
34
  afterEach(() => restore?.());
34
35
 
35
36
  /** Stage 400×300, the pane at stage (50, 20) and 200×100. */
36
- function mount() {
37
+ async function mount() {
37
38
  const pane = { current: null as HTMLDivElement | null };
38
39
  let api: AnnotationsApi | null = null;
39
40
  function Body() {
@@ -68,21 +69,21 @@ function mount() {
68
69
  restore = () => {
69
70
  HTMLElement.prototype.getBoundingClientRect = original;
70
71
  };
71
- const r = render(<Lab instruments={[marked]} defaultInstrument="Marked" />);
72
+ const r = await renderSettled(<Lab instruments={[marked]} defaultInstrument="Marked" />);
72
73
  return { ...r, api: () => api as AnnotationsApi };
73
74
  }
74
75
 
75
76
  const px = (el: HTMLElement, name: string) => Number.parseFloat(el.style.getPropertyValue(name));
76
77
 
77
78
  describe('marks on the overview', () => {
78
- it('draws nothing for a target with no marks', () => {
79
- const h = mount();
79
+ it('draws nothing for a target with no marks', async () => {
80
+ const h = await mount();
80
81
  expect(h.container.querySelector('.lk-overview__marks')).toBeNull();
81
82
  });
82
83
 
83
- it("places a target's marks at the target's rect, through the fit", () => {
84
- const h = mount();
85
- act(() => {
84
+ it("places a target's marks at the target's rect, through the fit", async () => {
85
+ const h = await mount();
86
+ await act(async () => {
86
87
  h.api().add({ target: 'pane', kind: 'rect', frac: { x: 0.1, y: 0.1, w: 0.2, h: 0.2 } });
87
88
  });
88
89
  const marks = h.container.querySelector<HTMLElement>('.lk-overview__marks[data-target="pane"]');
@@ -7,8 +7,9 @@
7
7
  * centering puts the pressed world point at the stage's origin instead.
8
8
  */
9
9
 
10
- import { act, render } from '@testing-library/react';
10
+ import { act } from '@testing-library/react';
11
11
  import type { PointerContextValue } from '@weasel-js/core';
12
+ import { renderSettled } from '@weasel-js/react/testing/renderSettled';
12
13
  import { beforeAll, describe, expect, it, vi } from 'vitest';
13
14
  import { defineInstrument } from '../instrument/defineInstrument';
14
15
  import { Lab } from '../lab/Lab';
@@ -40,7 +41,7 @@ function pointer(el: Element, type: string, x: number, y: number, buttons = 1) {
40
41
 
41
42
  /** Content 400×300 in a 200×150 box, 6px in: scale 0.46, so box (100, 75)
42
43
  * is content (200, 150). */
43
- function mount() {
44
+ async function mount() {
44
45
  let store: PointerContextValue | null = null;
45
46
  const pictured = defineInstrument<Record<string, never>, Record<string, never>>({
46
47
  name: 'Pictured',
@@ -57,7 +58,7 @@ function mount() {
57
58
  ),
58
59
  },
59
60
  });
60
- const r = render(<Lab instruments={[pictured]} defaultInstrument="Pictured" />);
61
+ const r = await renderSettled(<Lab instruments={[pictured]} defaultInstrument="Pictured" />);
61
62
  const box = r.container.querySelector('.lk-overview__box');
62
63
  const content = r.container.querySelector('.lk-stage__content') as HTMLElement | null;
63
64
  if (!box || !content) throw new Error('no overview or stage');
@@ -67,15 +68,15 @@ function mount() {
67
68
  const px = (el: HTMLElement, name: string) => Number.parseFloat(el.style.getPropertyValue(name));
68
69
 
69
70
  describe('<TrialOverview>', () => {
70
- it('draws the instrument-supplied content, not the instrument itself', () => {
71
- const h = mount();
71
+ it('draws the instrument-supplied content, not the instrument itself', async () => {
72
+ const h = await mount();
72
73
  expect(h.box.querySelector('[data-testid="thumb"]')).not.toBeNull();
73
74
  expect(h.box.querySelector('[data-testid="picture"]')).toBeNull();
74
75
  });
75
76
 
76
- it('moves the stage camera to the pressed point', () => {
77
- const h = mount();
78
- act(() => {
77
+ it('moves the stage camera to the pressed point', async () => {
78
+ const h = await mount();
79
+ await act(async () => {
79
80
  pointer(h.box, 'pointerdown', 100, 75);
80
81
  pointer(h.box, 'pointerup', 100, 75, 0);
81
82
  });
@@ -83,9 +84,9 @@ describe('<TrialOverview>', () => {
83
84
  expect(px(h.content, '--lk-stage-y')).toBeCloseTo(-150);
84
85
  });
85
86
 
86
- it('follows a drag with the camera, and never pans the stage by it', () => {
87
- const h = mount();
88
- act(() => {
87
+ it('follows a drag with the camera, and never pans the stage by it', async () => {
88
+ const h = await mount();
89
+ await act(async () => {
89
90
  pointer(h.box, 'pointerdown', 100, 75);
90
91
  pointer(h.box, 'pointermove', 110, 75);
91
92
  pointer(h.box, 'pointermove', 123, 75);
@@ -96,9 +97,9 @@ describe('<TrialOverview>', () => {
96
97
  expect(px(h.content, '--lk-stage-x')).toBeCloseTo(-250);
97
98
  });
98
99
 
99
- it("publishes the pointer over it as the overview's, and the stage draws it", () => {
100
- const h = mount();
101
- act(() => pointer(h.box, 'pointermove', 100, 75, 0));
100
+ it("publishes the pointer over it as the overview's, and the stage draws it", async () => {
101
+ const h = await mount();
102
+ await act(async () => pointer(h.box, 'pointermove', 100, 75, 0));
102
103
  const p = h.store()?.get();
103
104
  expect(p?.viewId).toBe('overview');
104
105
  expect(p?.worldX).toBeCloseTo(200);
@@ -106,10 +107,10 @@ describe('<TrialOverview>', () => {
106
107
  expect(h.container.querySelector('.lk-stage .lk-linked-cursor')).not.toBeNull();
107
108
  });
108
109
 
109
- it("publishes the stage's pointer as the stage's, so the stage draws none", () => {
110
- const h = mount();
110
+ it("publishes the stage's pointer as the stage's, so the stage draws none", async () => {
111
+ const h = await mount();
111
112
  const host = h.container.querySelector('.lk-stage') as HTMLElement;
112
- act(() => pointer(host, 'pointermove', 10, 10, 0));
113
+ await act(async () => pointer(host, 'pointermove', 10, 10, 0));
113
114
  expect(h.store()?.get()?.viewId).toBe('stage');
114
115
  expect(h.container.querySelector('.lk-stage .lk-linked-cursor')).toBeNull();
115
116
  });