@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
1
  import { render, screen } 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 type { TrialContribution } from '../chrome/types';
4
5
  import type { Instrument } from '../instrument/types';
@@ -19,23 +20,27 @@ function silenceRenderError(): void {
19
20
  }
20
21
 
21
22
  describe('<Lab> chrome', () => {
22
- it('renders a consumer contribution', () => {
23
+ it('renders a consumer contribution', async () => {
23
24
  const extra: TrialContribution = {
24
25
  id: 'export',
25
26
  region: 'toolbar',
26
27
  item: { icon: Glyph, label: 'Export', onActivate: () => {} },
27
28
  };
28
- render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" chrome={[extra]} />);
29
+ await renderSettled(
30
+ <Lab title="T" instruments={[bare]} defaultInstrument="Bare" chrome={[extra]} />,
31
+ );
29
32
  expect(screen.getByRole('button', { name: 'Export' })).toBeInTheDocument();
30
33
  });
31
34
 
32
- it('suppresses a built-in by id', () => {
33
- render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" suppress={['snapshot']} />);
35
+ it('suppresses a built-in by id', async () => {
36
+ await renderSettled(
37
+ <Lab title="T" instruments={[bare]} defaultInstrument="Bare" suppress={['snapshot']} />,
38
+ );
34
39
  expect(screen.queryByRole('button', { name: 'Save snapshot' })).toBeNull();
35
40
  expect(screen.getByRole('button', { name: 'Clone trial' })).toBeInTheDocument();
36
41
  });
37
42
 
38
- it('throws when a consumer id collides with a built-in', () => {
43
+ it('throws when a consumer id collides with a built-in', async () => {
39
44
  silenceRenderError();
40
45
  const clash: TrialContribution = {
41
46
  id: 'clone',
@@ -47,24 +52,24 @@ describe('<Lab> chrome', () => {
47
52
  ).toThrow(/duplicate contribution id "clone"/);
48
53
  });
49
54
 
50
- it('throws when suppressing an id that is not there', () => {
55
+ it('throws when suppressing an id that is not there', async () => {
51
56
  silenceRenderError();
52
57
  expect(() =>
53
58
  render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" suppress={['nope']} />),
54
59
  ).toThrow(/cannot suppress "nope"/);
55
60
  });
56
61
 
57
- it('renders an instrument-declared contribution', () => {
62
+ it('renders an instrument-declared contribution', async () => {
58
63
  const withChrome: Instrument = {
59
64
  ...bare,
60
65
  chrome: [{ id: 'mine', region: 'status', item: { text: 'ready' } }],
61
66
  };
62
- render(<Lab title="T" instruments={[withChrome]} defaultInstrument="Bare" />);
67
+ await renderSettled(<Lab title="T" instruments={[withChrome]} defaultInstrument="Bare" />);
63
68
  expect(screen.getByText('ready')).toBeInTheDocument();
64
69
  });
65
70
 
66
- it('renders an aside contribution in its own pane, after the workspace', () => {
67
- render(
71
+ it('renders an aside contribution in its own pane, after the workspace', async () => {
72
+ await renderSettled(
68
73
  <Lab
69
74
  title="T"
70
75
  instruments={[bare]}
@@ -1,4 +1,4 @@
1
- import { render } from '@testing-library/react';
1
+ import { renderSettled } from '@weasel-js/react/testing/renderSettled';
2
2
  import { resolveTheme } from '@weasel-js/theme';
3
3
  import { describe, expect, it } from 'vitest';
4
4
  import type { Instrument } from '../instrument/types';
@@ -18,8 +18,8 @@ const backdrop = (container: HTMLElement) =>
18
18
  .find(Boolean) ?? '';
19
19
 
20
20
  describe('<Lab nebula>', () => {
21
- it('draws one blob per literal color', () => {
22
- const { container } = render(
21
+ it('draws one blob per literal color', async () => {
22
+ const { container } = await renderSettled(
23
23
  <Lab
24
24
  instruments={[bare]}
25
25
  defaultInstrument="Bare"
@@ -30,8 +30,8 @@ describe('<Lab nebula>', () => {
30
30
  expect(backdrop(container).match(/radial-gradient/g)).toHaveLength(3);
31
31
  });
32
32
 
33
- it('resolves a theme ramp against the lab’s theme', () => {
34
- const { container } = render(
33
+ it('resolves a theme ramp against the lab’s theme', async () => {
34
+ const { container } = await renderSettled(
35
35
  <Lab instruments={[bare]} defaultInstrument="Bare" mode="dark" nebula={{ ramp: 'swatch' }} />,
36
36
  );
37
37
  const sky = resolveTheme(interstellarTheme, { mode: 'dark' })['--wzl-swatch-sky'];
@@ -7,7 +7,8 @@
7
7
  * workspace: `.lk-lab__body > .lk-workspace { min-width: 0 }` is a direct-child
8
8
  * selector, so an extra element between them silently drops that rule.
9
9
  */
10
- import { act, render } from '@testing-library/react';
10
+ import { act } from '@testing-library/react';
11
+ import { renderSettled } from '@weasel-js/react/testing/renderSettled';
11
12
  import type { ReactNode } from 'react';
12
13
  import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
13
14
  import { defineInstrument } from '../instrument/defineInstrument';
@@ -75,16 +76,18 @@ const probeInstrument = defineInstrument<Record<string, never>, Record<string, n
75
76
  });
76
77
 
77
78
  describe('<Lab> surface provider', () => {
78
- it('puts a surface handle in reach of an instrument', () => {
79
+ it('puts a surface handle in reach of an instrument', async () => {
79
80
  seen = 'not-rendered';
80
- render(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
81
+ await renderSettled(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
81
82
  expect(seen).not.toBe('not-rendered');
82
83
  expect(seen).not.toBeNull();
83
84
  expect(typeof (seen as unknown as SurfaceHandle).registerTile).toBe('function');
84
85
  });
85
86
 
86
- it('stacks an under and an over buffer inside the lab body', () => {
87
- const { container } = render(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
87
+ it('stacks an under and an over buffer inside the lab body', async () => {
88
+ const { container } = await renderSettled(
89
+ <Lab instruments={[probeInstrument]} defaultInstrument="Probe" />,
90
+ );
88
91
  const canvases = container.querySelectorAll('.lk-lab__body > canvas.lk-lab__surface');
89
92
  expect(canvases).toHaveLength(2);
90
93
  // Source order, because the z-index that separates them is in a stylesheet
@@ -96,24 +99,28 @@ describe('<Lab> surface provider', () => {
96
99
  ]);
97
100
  });
98
101
 
99
- it('hands a tenant whichever of the two it asks for', () => {
100
- const { container } = render(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
102
+ it('hands a tenant whichever of the two it asks for', async () => {
103
+ const { container } = await renderSettled(
104
+ <Lab instruments={[probeInstrument]} defaultInstrument="Probe" />,
105
+ );
101
106
  const [underEl, overEl] = [...container.querySelectorAll('canvas.lk-lab__surface')];
102
107
  expect(seenCanvases.over).toBe(overEl);
103
108
  expect(seenCanvases.under).toBe(underEl);
104
109
  expect(overEl).not.toBe(underEl);
105
110
  });
106
111
 
107
- it('defaults a tenant to the buffer over the trials', () => {
112
+ it('defaults a tenant to the buffer over the trials', async () => {
108
113
  // Every tenant predating the split wants the over one: the surface was
109
114
  // built for marks that annotate an instrument.
110
- const { container } = render(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
115
+ const { container } = await renderSettled(
116
+ <Lab instruments={[probeInstrument]} defaultInstrument="Probe" />,
117
+ );
111
118
  const [underEl, overEl] = [...container.querySelectorAll('canvas.lk-lab__surface')];
112
119
  expect(seenCanvases.byDefault).toBe(overEl);
113
120
  expect(seenCanvases.byDefault).not.toBe(underEl);
114
121
  });
115
122
 
116
- it('mounts no buffer of its own when a host already owns the surface', () => {
123
+ it('mounts no buffer of its own when a host already owns the surface', async () => {
117
124
  function Host({ children }: { children: ReactNode }) {
118
125
  const surface = useTiledSurface({ onFrame: () => {} });
119
126
  return (
@@ -122,7 +129,7 @@ describe('<Lab> surface provider', () => {
122
129
  </SurfaceContext.Provider>
123
130
  );
124
131
  }
125
- const { container } = render(
132
+ const { container } = await renderSettled(
126
133
  <Host>
127
134
  <Lab instruments={[probeInstrument]} defaultInstrument="Probe" />
128
135
  </Host>,
@@ -130,11 +137,13 @@ describe('<Lab> surface provider', () => {
130
137
  expect(container.querySelectorAll('canvas.lk-lab__surface')).toHaveLength(0);
131
138
  });
132
139
 
133
- it('keeps the workspace a direct child of the lab body', () => {
140
+ it('keeps the workspace a direct child of the lab body', async () => {
134
141
  // The min-width:0 rule that keeps the flex row from overflowing is
135
142
  // `.lk-lab__body > .lk-workspace`. An element inserted between them drops
136
143
  // it, and jsdom cannot see the overflow that follows.
137
- const { container } = render(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
144
+ const { container } = await renderSettled(
145
+ <Lab instruments={[probeInstrument]} defaultInstrument="Probe" />,
146
+ );
138
147
  const body = container.querySelector('.lk-lab__body');
139
148
  expect(body).not.toBeNull();
140
149
  expect(body?.querySelector(':scope > .lk-workspace')).not.toBeNull();
@@ -155,7 +164,7 @@ describe('<Lab> tile round trip', () => {
155
164
  vi.restoreAllMocks();
156
165
  });
157
166
 
158
- it('publishes a rect for a tile an instrument registered', () => {
167
+ it('publishes a rect for a tile an instrument registered', async () => {
159
168
  const frames: SurfaceFrame[] = [];
160
169
  let handle: SurfaceHandle | null = null;
161
170
 
@@ -177,7 +186,7 @@ describe('<Lab> tile round trip', () => {
177
186
  );
178
187
  }
179
188
 
180
- render(
189
+ await renderSettled(
181
190
  <Host>
182
191
  <Lab instruments={[probeInstrument]} defaultInstrument="Probe" />
183
192
  </Host>,
@@ -189,11 +198,11 @@ describe('<Lab> tile round trip', () => {
189
198
  expect(pane).not.toBeNull();
190
199
  stubBox(pane, 240, 100, 320, 200);
191
200
 
192
- act(() => {
201
+ await act(async () => {
193
202
  handle?.invalidateRects();
194
203
  handle?.invalidate(paneTileId);
195
204
  });
196
- act(() => {
205
+ await act(async () => {
197
206
  vi.advanceTimersByTime(64);
198
207
  });
199
208
 
@@ -208,8 +217,8 @@ describe('<Lab> tile round trip', () => {
208
217
  });
209
218
 
210
219
  describe('<Lab> pages', () => {
211
- it("gives the title a way back to the project's other labs", () => {
212
- const { getByRole } = render(
220
+ it("gives the title a way back to the project's other labs", async () => {
221
+ const { getByRole } = await renderSettled(
213
222
  <Lab
214
223
  instruments={[probeInstrument]}
215
224
  defaultInstrument="Probe"
@@ -225,8 +234,8 @@ describe('<Lab> pages', () => {
225
234
  expect(getByRole('button', { name: /kliegsminister/ })).toBeTruthy();
226
235
  });
227
236
 
228
- it('leaves the title a plain heading when there are no other labs', () => {
229
- const { getByRole, queryByRole } = render(
237
+ it('leaves the title a plain heading when there are no other labs', async () => {
238
+ const { getByRole, queryByRole } = await renderSettled(
230
239
  <Lab instruments={[probeInstrument]} defaultInstrument="Probe" title="solo" />,
231
240
  );
232
241
  expect(getByRole('heading', { name: 'solo' })).toBeTruthy();
@@ -1,4 +1,5 @@
1
1
  import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
2
+ import { renderSettled } from '@weasel-js/react/testing/renderSettled';
2
3
  import { type ReactNode, useEffect } from 'react';
3
4
  import { describe, expect, it } from 'vitest';
4
5
  import type { Instrument } from '../instrument/types';
@@ -32,9 +33,9 @@ function CaptureLab({ children }: { children?: ReactNode }) {
32
33
  );
33
34
  }
34
35
 
35
- function mountLab(props: Partial<Record<keyof LabProps, unknown>> = {}) {
36
+ async function mountLab(props: Partial<Record<keyof LabProps, unknown>> = {}) {
36
37
  labRef = null;
37
- return render(
38
+ return renderSettled(
38
39
  <Lab {...({ instruments: [stub, stubB], defaultInstrument: 'Stub', ...props } as LabProps)}>
39
40
  <CaptureLab />
40
41
  </Lab>,
@@ -42,86 +43,86 @@ function mountLab(props: Partial<Record<keyof LabProps, unknown>> = {}) {
42
43
  }
43
44
 
44
45
  describe('<Lab>', () => {
45
- it('renders one trial by default', () => {
46
- mountLab();
46
+ it('renders one trial by default', async () => {
47
+ await mountLab();
47
48
  expect(screen.getAllByRole('region', { name: /trial/i })).toHaveLength(1);
48
49
  });
49
50
 
50
- it('addTrial adds a second trial', () => {
51
- mountLab();
52
- act(() => labRef?.addTrial('Stub'));
51
+ it('addTrial adds a second trial', async () => {
52
+ await mountLab();
53
+ await act(async () => labRef?.addTrial('Stub'));
53
54
  expect(screen.getAllByRole('region', { name: /trial/i })).toHaveLength(2);
54
55
  });
55
56
 
56
- it('addTrial opens each trial on the config it was given', () => {
57
- mountLab();
58
- act(() => labRef?.addTrial('Stub', { config: { count: 7 } }));
57
+ it('addTrial opens each trial on the config it was given', async () => {
58
+ await mountLab();
59
+ await act(async () => labRef?.addTrial('Stub', { config: { count: 7 } }));
59
60
  expect(labRef?.trials.map((t) => (t.config as { count: number }).count)).toEqual([0, 7]);
60
61
  expect(labRef?.trials.map((t) => (t.state as { value: number }).value)).toEqual([0, 7]);
61
62
  });
62
63
 
63
- it('closeTrial removes one when more than one exists', () => {
64
- mountLab();
65
- act(() => labRef?.addTrial('Stub'));
64
+ it('closeTrial removes one when more than one exists', async () => {
65
+ await mountLab();
66
+ await act(async () => labRef?.addTrial('Stub'));
66
67
  const first = labRef?.trials[0];
67
- act(() => labRef?.closeTrial(first?.id ?? ''));
68
+ await act(async () => labRef?.closeTrial(first?.id ?? ''));
68
69
  expect(screen.getAllByRole('region', { name: /trial/i })).toHaveLength(1);
69
70
  });
70
71
 
71
- it('closeTrial is a no-op on the last trial', () => {
72
- mountLab();
72
+ it('closeTrial is a no-op on the last trial', async () => {
73
+ await mountLab();
73
74
  const only = labRef?.trials[0];
74
- act(() => labRef?.closeTrial(only?.id ?? ''));
75
+ await act(async () => labRef?.closeTrial(only?.id ?? ''));
75
76
  expect(screen.getAllByRole('region', { name: /trial/i })).toHaveLength(1);
76
77
  });
77
78
 
78
- it('cloneTrial inserts immediately after source', () => {
79
- mountLab();
80
- act(() => labRef?.addTrial('StubB'));
79
+ it('cloneTrial inserts immediately after source', async () => {
80
+ await mountLab();
81
+ await act(async () => labRef?.addTrial('StubB'));
81
82
  const ws0 = labRef?.trials[0];
82
83
  const ws1 = labRef?.trials[1];
83
- act(() => labRef?.cloneTrial(ws0?.id ?? ''));
84
+ await act(async () => labRef?.cloneTrial(ws0?.id ?? ''));
84
85
  const ids = labRef?.trials.map((w) => w.id) ?? [];
85
86
  expect(ids).toHaveLength(3);
86
87
  expect(ids[0]).toBe(ws0?.id);
87
88
  expect(ids[2]).toBe(ws1?.id);
88
89
  });
89
90
 
90
- it('resetTrial restores defaults', () => {
91
- mountLab();
91
+ it('resetTrial restores defaults', async () => {
92
+ await mountLab();
92
93
  const ws = labRef?.trials[0];
93
94
  if (!ws) throw new Error('no trial');
94
- act(() => labRef?.resetTrial(ws.id));
95
+ await act(async () => labRef?.resetTrial(ws.id));
95
96
  const reset = labRef?.trials[0];
96
97
  expect(reset?.config).toEqual({ count: 0 });
97
98
  expect(reset?.state).toEqual({ value: 0 });
98
99
  });
99
100
 
100
- it('mode="light" stamps light on the lab root', () => {
101
- const { container } = mountLab({ mode: 'light' });
101
+ it('mode="light" stamps light on the lab root', async () => {
102
+ const { container } = await mountLab({ mode: 'light' });
102
103
  expect(container.querySelector('.lk-lab')?.getAttribute('data-wzl-mode')).toBe('light');
103
104
  });
104
105
 
105
- it('mode="dark" stamps dark on the lab root', () => {
106
- const { container } = mountLab({ mode: 'dark' });
106
+ it('mode="dark" stamps dark on the lab root', async () => {
107
+ const { container } = await mountLab({ mode: 'dark' });
107
108
  expect(container.querySelector('.lk-lab')?.getAttribute('data-wzl-mode')).toBe('dark');
108
109
  });
109
110
 
110
- it('mode="auto" resolves to a concrete mode', () => {
111
- const { container } = mountLab({ mode: 'auto' });
111
+ it('mode="auto" resolves to a concrete mode', async () => {
112
+ const { container } = await mountLab({ mode: 'auto' });
112
113
  expect(container.querySelector('.lk-lab')?.getAttribute('data-wzl-mode')).toMatch(
113
114
  /^(light|dark)$/,
114
115
  );
115
116
  });
116
117
 
117
- it('applies the interstellar theme in every mode', () => {
118
- const { container } = mountLab({ mode: 'light' });
118
+ it('applies the interstellar theme in every mode', async () => {
119
+ const { container } = await mountLab({ mode: 'light' });
119
120
  expect(container.querySelector('.lk-lab')?.getAttribute('data-wzl-theme')).toBe('interstellar');
120
121
  });
121
122
 
122
- it('setMode updates the stamped mode at runtime', () => {
123
- const { container } = mountLab({ mode: 'auto' });
124
- act(() => labRef?.setMode('light'));
123
+ it('setMode updates the stamped mode at runtime', async () => {
124
+ const { container } = await mountLab({ mode: 'auto' });
125
+ await act(async () => labRef?.setMode('light'));
125
126
  expect(container.querySelector('.lk-lab')?.getAttribute('data-wzl-mode')).toBe('light');
126
127
  });
127
128
 
@@ -152,13 +153,13 @@ describe('<Lab>', () => {
152
153
  };
153
154
  const storage = () => createMemoryAdapter(backing);
154
155
 
155
- const first = render(
156
+ const first = await renderSettled(
156
157
  <Lab instruments={[mapped]} defaultInstrument="Mapped" storage={storage()} storageKey="s" />,
157
158
  );
158
159
  await waitFor(() => expect(JSON.stringify([...backing.values()])).toContain('"seen":["a"]'));
159
160
  first.unmount();
160
161
 
161
- mountLab({
162
+ await mountLab({
162
163
  instruments: [mapped],
163
164
  defaultInstrument: 'Mapped',
164
165
  storage: storage(),
@@ -172,7 +173,7 @@ describe('<Lab>', () => {
172
173
  });
173
174
 
174
175
  describe('instruments that change while mounted', () => {
175
- it('fills an open trial’s config before its replaced instrument renders', () => {
176
+ it('fills an open trial’s config before its replaced instrument renders', async () => {
176
177
  const v1: Instrument = {
177
178
  name: 'Grow',
178
179
  defaultConfig: () => ({ size: 1 }),
@@ -188,7 +189,7 @@ describe('instruments that change while mounted', () => {
188
189
  return null;
189
190
  },
190
191
  };
191
- const view = render(
192
+ const view = await renderSettled(
192
193
  <Lab instruments={[v1]} defaultInstrument="Grow">
193
194
  <CaptureLab />
194
195
  </Lab>,
@@ -203,9 +204,9 @@ describe('instruments that change while mounted', () => {
203
204
  expect(seenByV2.every((c) => c.color === 'red')).toBe(true);
204
205
  });
205
206
 
206
- it('opens a trial of an instrument added after mount', () => {
207
+ it('opens a trial of an instrument added after mount', async () => {
207
208
  const later: Instrument = { ...stub, name: 'Later' };
208
- const view = render(
209
+ const view = await renderSettled(
209
210
  <Lab instruments={[stub]} defaultInstrument="Stub">
210
211
  <CaptureLab />
211
212
  </Lab>,
@@ -215,12 +216,12 @@ describe('instruments that change while mounted', () => {
215
216
  <CaptureLab />
216
217
  </Lab>,
217
218
  );
218
- act(() => labRef?.addTrial('Later'));
219
+ await act(async () => labRef?.addTrial('Later'));
219
220
  expect(labRef?.trials.map((t) => t.instrumentName)).toEqual(['Stub', 'Later']);
220
221
  expect(screen.queryByText(/Unknown instrument/)).toBeNull();
221
222
  });
222
223
 
223
- it('keeps a trial’s content mounted across an instrument swap', () => {
224
+ it('keeps a trial’s content mounted across an instrument swap', async () => {
224
225
  let mounts = 0;
225
226
  function Probe() {
226
227
  useEffect(() => {
@@ -235,7 +236,7 @@ describe('instruments that change while mounted', () => {
235
236
  render: () => <Probe />,
236
237
  };
237
238
  const v2: Instrument = { ...v1, defaultConfig: () => ({ extra: 1 }) };
238
- const view = render(<Lab instruments={[v1]} defaultInstrument="Keep" />);
239
+ const view = await renderSettled(<Lab instruments={[v1]} defaultInstrument="Keep" />);
239
240
  view.rerender(<Lab instruments={[v2]} defaultInstrument="Keep" />);
240
241
  expect(mounts).toBe(1);
241
242
  });
@@ -249,72 +250,72 @@ describe('focused trial', () => {
249
250
  render: () => <iframe title="story frame" />,
250
251
  };
251
252
 
252
- it('is the only trial until another opens, then the one that opened', () => {
253
- mountLab();
253
+ it('is the only trial until another opens, then the one that opened', async () => {
254
+ await mountLab();
254
255
  expect(labRef?.focusedTrialId).toBe(labRef?.trials[0]?.id);
255
- act(() => labRef?.addTrial('StubB'));
256
+ await act(async () => labRef?.addTrial('StubB'));
256
257
  expect(labRef?.focusedTrialId).toBe(labRef?.trials[1]?.id);
257
- act(() => labRef?.cloneTrial(labRef?.trials[0]?.id ?? ''));
258
+ await act(async () => labRef?.cloneTrial(labRef?.trials[0]?.id ?? ''));
258
259
  expect(labRef?.focusedTrialId).toBe(labRef?.trials[1]?.id);
259
260
  expect(labRef?.trials.map((t) => t.instrumentName)).toEqual(['Stub', 'Stub', 'StubB']);
260
261
  });
261
262
 
262
- it('moves to a trial a pointer goes down in', () => {
263
- mountLab();
264
- act(() => labRef?.addTrial('StubB'));
263
+ it('moves to a trial a pointer goes down in', async () => {
264
+ await mountLab();
265
+ await act(async () => labRef?.addTrial('StubB'));
265
266
  fireEvent.pointerDown(within(regions()[0] as HTMLElement).getByTestId('stub-content'));
266
267
  expect(labRef?.focusedTrialId).toBe(labRef?.trials[0]?.id);
267
268
  });
268
269
 
269
- it('moves to a trial focus moves into', () => {
270
- mountLab();
271
- act(() => labRef?.addTrial('StubB'));
272
- act(() => (regions()[0] as HTMLElement).focus());
270
+ it('moves to a trial focus moves into', async () => {
271
+ await mountLab();
272
+ await act(async () => labRef?.addTrial('StubB'));
273
+ await act(async () => (regions()[0] as HTMLElement).focus());
273
274
  expect(labRef?.focusedTrialId).toBe(labRef?.trials[0]?.id);
274
275
  });
275
276
 
276
277
  it('moves to a trial whose frame takes focus, which fires nothing in the lab’s document', async () => {
277
- mountLab({ instruments: [framed, stubB], defaultInstrument: 'Framed' });
278
- act(() => labRef?.addTrial('StubB'));
279
- act(() => screen.getByTitle('story frame').focus());
278
+ await mountLab({ instruments: [framed, stubB], defaultInstrument: 'Framed' });
279
+ await act(async () => labRef?.addTrial('StubB'));
280
+ await act(async () => screen.getByTitle('story frame').focus());
280
281
  fireEvent.blur(window);
281
282
  await waitFor(() => expect(labRef?.focusedTrialId).toBe(labRef?.trials[0]?.id));
282
283
  });
283
284
 
284
- it('falls back to the first trial when the focused one closes', () => {
285
- mountLab();
286
- act(() => labRef?.addTrial('StubB'));
287
- act(() => labRef?.closeTrial(labRef?.trials[1]?.id ?? ''));
285
+ it('falls back to the first trial when the focused one closes', async () => {
286
+ await mountLab();
287
+ await act(async () => labRef?.addTrial('StubB'));
288
+ await act(async () => labRef?.closeTrial(labRef?.trials[1]?.id ?? ''));
288
289
  expect(labRef?.focusedTrialId).toBe(labRef?.trials[0]?.id);
289
290
  });
290
291
 
291
- it('is marked on its trial only when there is more than one', () => {
292
- mountLab();
292
+ it('is marked on its trial only when there is more than one', async () => {
293
+ await mountLab();
293
294
  expect(regions()[0]).not.toHaveAttribute('data-focused');
294
- act(() => labRef?.addTrial('StubB'));
295
+ await act(async () => labRef?.addTrial('StubB'));
295
296
  expect(regions()[1]).toHaveAttribute('data-focused', 'true');
296
297
  expect(regions()[0]).not.toHaveAttribute('data-focused');
297
298
  });
298
299
  });
299
300
 
300
301
  describe('swapTrial', () => {
301
- it('runs another instrument in the trial’s place, and keeps focus on that place', () => {
302
- mountLab();
303
- act(() => labRef?.addTrial('Stub'));
302
+ it('runs another instrument in the trial’s place, and keeps focus on that place', async () => {
303
+ await mountLab();
304
+ await act(async () => labRef?.addTrial('Stub'));
304
305
  const second = labRef?.trials[1]?.id;
305
- act(() => labRef?.focusTrial(labRef?.trials[0]?.id ?? ''));
306
- act(() => labRef?.swapTrial(labRef?.trials[0]?.id ?? '', 'StubB'));
306
+ await act(async () => labRef?.focusTrial(labRef?.trials[0]?.id ?? ''));
307
+ await act(async () => labRef?.swapTrial(labRef?.trials[0]?.id ?? '', 'StubB'));
307
308
  expect(labRef?.trials.map((t) => t.instrumentName)).toEqual(['StubB', 'Stub']);
308
309
  expect(labRef?.trials[1]?.id).toBe(second);
309
310
  expect(labRef?.focusedTrialId).toBe(labRef?.trials[0]?.id);
310
311
  expect(screen.getByTestId('stub-b-content')).toBeInTheDocument();
311
312
  });
312
313
 
313
- it('leaves focus where it was when the swapped trial did not have it', () => {
314
- mountLab();
315
- act(() => labRef?.addTrial('Stub'));
314
+ it('leaves focus where it was when the swapped trial did not have it', async () => {
315
+ await mountLab();
316
+ await act(async () => labRef?.addTrial('Stub'));
316
317
  const focused = labRef?.focusedTrialId;
317
- act(() => labRef?.swapTrial(labRef?.trials[0]?.id ?? '', 'StubB'));
318
+ await act(async () => labRef?.swapTrial(labRef?.trials[0]?.id ?? '', 'StubB'));
318
319
  expect(labRef?.focusedTrialId).toBe(focused);
319
320
  });
320
321
  });
@@ -1,5 +1,6 @@
1
- import { render, screen } from '@testing-library/react';
1
+ import { 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 { describe, expect, it } from 'vitest';
4
5
  import type { TrialContribution } from '../chrome/types';
5
6
  import type { Instrument } from '../instrument/types';
@@ -34,19 +35,21 @@ const pinned: TrialContribution = {
34
35
  item: { title: 'Pinned', undockable: false, body: <p>stay</p> },
35
36
  };
36
37
 
37
- function renderLab() {
38
- return render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" chrome={[notes]} />);
38
+ async function renderLab() {
39
+ return renderSettled(
40
+ <Lab title="T" instruments={[bare]} defaultInstrument="Bare" chrome={[notes]} />,
41
+ );
39
42
  }
40
43
 
41
44
  describe('undocking a sidebar section', () => {
42
- it('starts docked inside the trial sidebar', () => {
43
- const { container } = renderLab();
45
+ it('starts docked inside the trial sidebar', async () => {
46
+ const { container } = await renderLab();
44
47
  const sidebar = container.querySelector('.lk-trial__sidebar');
45
48
  expect(sidebar).toContainElement(screen.getByTestId('notes-body'));
46
49
  });
47
50
 
48
51
  it('moves the section out of the sidebar and into a workspace panel', async () => {
49
- const { container } = renderLab();
52
+ const { container } = await renderLab();
50
53
  await userEvent.click(screen.getByRole('button', { name: 'Undock Notes' }));
51
54
 
52
55
  expect(container.querySelector('.lk-trial__sidebar')).not.toContainElement(
@@ -57,7 +60,7 @@ describe('undocking a sidebar section', () => {
57
60
  });
58
61
 
59
62
  it('docks it back', async () => {
60
- const { container } = renderLab();
63
+ const { container } = await renderLab();
61
64
  await userEvent.click(screen.getByRole('button', { name: 'Undock Notes' }));
62
65
  await userEvent.click(screen.getByRole('button', { name: 'Dock Notes' }));
63
66
 
@@ -68,7 +71,7 @@ describe('undocking a sidebar section', () => {
68
71
  });
69
72
 
70
73
  it('sends a section declaring undockAs floating to the floating layer', async () => {
71
- const { container } = render(
74
+ const { container } = await renderSettled(
72
75
  <Lab title="T" instruments={[bare]} defaultInstrument="Bare" chrome={[floater]} />,
73
76
  );
74
77
  await userEvent.click(screen.getByRole('button', { name: 'Undock Floater' }));
@@ -78,8 +81,10 @@ describe('undocking a sidebar section', () => {
78
81
  expect(container.querySelector('.lk-panel-tile--floating')).not.toBeNull();
79
82
  });
80
83
 
81
- it('offers no tear-out control on a section that opts out', () => {
82
- render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" chrome={[pinned]} />);
84
+ it('offers no tear-out control on a section that opts out', async () => {
85
+ await renderSettled(
86
+ <Lab title="T" instruments={[bare]} defaultInstrument="Bare" chrome={[pinned]} />,
87
+ );
83
88
  expect(screen.queryByRole('button', { name: 'Undock Pinned' })).toBeNull();
84
89
  });
85
90
  });