@weasel-js/labkit 1.7.0 → 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 (242) hide show
  1. package/README.md +3 -3
  2. package/dist/_dts/{CanvasStackContext-D2jnIwfk.d.ts → CanvasStackContext-Cjy6I-Iv.d.ts} +4 -1
  3. package/dist/_dts/{Stage-CCWfQQWM.d.ts → Stage-D6t7o0pF.d.ts} +3 -2
  4. package/dist/_dts/{index-CsYqUHD3.d.ts → index-BTmaRy6a.d.ts} +10 -6
  5. package/dist/_dts/{types-CSm9jgol.d.ts → index-DCMA4Fy1.d.ts} +59 -113
  6. package/dist/_dts/{types-CRPSGFAp.d.ts → types-BUBSIZuV.d.ts} +8 -7
  7. package/dist/_dts/{useTrialState-CTZPgeZ7.d.ts → useTrialState-Bh8bzFgE.d.ts} +1 -1
  8. package/dist/canvas/index.d.ts +8 -6
  9. package/dist/canvas/index.js +6 -6
  10. package/dist/chrome/index.d.ts +7 -5
  11. package/dist/chrome/index.js +10 -9
  12. package/dist/chunk-2ES4W47N.js +8 -0
  13. package/dist/chunk-2ES4W47N.js.map +1 -0
  14. package/dist/{chunk-WPLAANH4.js → chunk-2N7UBSS2.js} +12 -12
  15. package/dist/chunk-2N7UBSS2.js.map +1 -0
  16. package/dist/{chunk-275JRRVD.js → chunk-E23Q6A3Y.js} +232 -37
  17. package/dist/chunk-E23Q6A3Y.js.map +1 -0
  18. package/dist/{chunk-GOZLNM4L.js → chunk-E6NARGU7.js} +5 -5
  19. package/dist/{chunk-GOZLNM4L.js.map → chunk-E6NARGU7.js.map} +1 -1
  20. package/dist/{chunk-IJJUK4FU.js → chunk-EESJUSYR.js} +5 -6
  21. package/dist/chunk-EESJUSYR.js.map +1 -0
  22. package/dist/{chunk-HXXTULDN.js → chunk-EFXRM6XQ.js} +8 -11
  23. package/dist/chunk-EFXRM6XQ.js.map +1 -0
  24. package/dist/chunk-F4ACIK7Z.js +210 -0
  25. package/dist/chunk-F4ACIK7Z.js.map +1 -0
  26. package/dist/{chunk-AWQG7FCC.js → chunk-FG3EXZDV.js} +3 -3
  27. package/dist/{chunk-AWQG7FCC.js.map → chunk-FG3EXZDV.js.map} +1 -1
  28. package/dist/{chunk-YT75VYRD.js → chunk-IBVRMHW5.js} +5 -5
  29. package/dist/{chunk-YT75VYRD.js.map → chunk-IBVRMHW5.js.map} +1 -1
  30. package/dist/{chunk-RWNDDKOH.js → chunk-PMLZP7RT.js} +18 -15
  31. package/dist/chunk-PMLZP7RT.js.map +1 -0
  32. package/dist/{chunk-Y23G2VZL.js → chunk-READTDQX.js} +2 -2
  33. package/dist/chunk-READTDQX.js.map +1 -0
  34. package/dist/{chunk-I5DUQ5HM.js → chunk-RODIY7HI.js} +75 -72
  35. package/dist/chunk-RODIY7HI.js.map +1 -0
  36. package/dist/{chunk-JVBYV6TL.js → chunk-ROGJWDQA.js} +3 -3
  37. package/dist/{chunk-JVBYV6TL.js.map → chunk-ROGJWDQA.js.map} +1 -1
  38. package/dist/{chunk-ZIFPHIWZ.js → chunk-T6DLGEJB.js} +12 -14
  39. package/dist/chunk-T6DLGEJB.js.map +1 -0
  40. package/dist/chunk-U6UQVGFP.js +15583 -0
  41. package/dist/chunk-U6UQVGFP.js.map +1 -0
  42. package/dist/{chunk-24W5VEJW.js → chunk-ULYYNWGP.js} +2 -2
  43. package/dist/{chunk-24W5VEJW.js.map → chunk-ULYYNWGP.js.map} +1 -1
  44. package/dist/{chunk-UNAZSLWE.js → chunk-VYVGVBGY.js} +17 -20
  45. package/dist/chunk-VYVGVBGY.js.map +1 -0
  46. package/dist/chunk-WFALF2X2.js +302 -0
  47. package/dist/chunk-WFALF2X2.js.map +1 -0
  48. package/dist/{chunk-KMJ7RWD3.js → chunk-WLMDTI2Q.js} +3 -3
  49. package/dist/{chunk-KMJ7RWD3.js.map → chunk-WLMDTI2Q.js.map} +1 -1
  50. package/dist/{chunk-T5IBSPAM.js → chunk-WWJDPNZZ.js} +43 -33
  51. package/dist/chunk-WWJDPNZZ.js.map +1 -0
  52. package/dist/{chunk-J7OWWTCW.js → chunk-XE5HJOPL.js} +5 -6
  53. package/dist/chunk-XE5HJOPL.js.map +1 -0
  54. package/dist/{chunk-7BO7HNGF.js → chunk-XRX5S5NT.js} +8 -61
  55. package/dist/chunk-XRX5S5NT.js.map +1 -0
  56. package/dist/chunk-XVUKDSYV.js +38 -0
  57. package/dist/chunk-XVUKDSYV.js.map +1 -0
  58. package/dist/{chunk-FMK7UQ6V.js → chunk-XZDCEMEL.js} +64 -47
  59. package/dist/chunk-XZDCEMEL.js.map +1 -0
  60. package/dist/{chunk-FZ7YFQN7.js → chunk-YCFKW5RX.js} +3 -3
  61. package/dist/chunk-YCFKW5RX.js.map +1 -0
  62. package/dist/{chunk-CQLQPQ4P.js → chunk-ZQIHJXP4.js} +2 -2
  63. package/dist/chunk-ZQIHJXP4.js.map +1 -0
  64. package/dist/config/index.d.ts +18 -10
  65. package/dist/config/index.js +4 -4
  66. package/dist/controls/index.d.ts +58 -9
  67. package/dist/controls/index.js +7 -5
  68. package/dist/dragdrop/index.d.ts +23 -3
  69. package/dist/dragdrop/index.js +4 -3
  70. package/dist/fonts/oswald-tabular-digits.woff2 +0 -0
  71. package/dist/index.d.ts +61 -715
  72. package/dist/index.js +456 -432
  73. package/dist/index.js.map +1 -1
  74. package/dist/job/index.d.ts +8 -0
  75. package/dist/job/index.js +1 -1
  76. package/dist/layers/index.d.ts +6 -5
  77. package/dist/layers/index.js +3 -2
  78. package/dist/loupe/index.d.ts +100 -30
  79. package/dist/loupe/index.js +585 -6
  80. package/dist/loupe/index.js.map +1 -1
  81. package/dist/overview/index.d.ts +4 -2
  82. package/dist/overview/index.js +15 -14
  83. package/dist/overview/index.js.map +1 -1
  84. package/dist/passthrough/weasel-canvas.d.ts +2 -0
  85. package/dist/passthrough/weasel-ui.d.ts +2 -2871
  86. package/dist/passthrough/weasel-ui.js +3 -2
  87. package/dist/primitives/index.d.ts +213 -3
  88. package/dist/primitives/index.js +8 -7
  89. package/dist/state/index.d.ts +7 -5
  90. package/dist/state/index.js +6 -6
  91. package/dist/styles.css +284 -51
  92. package/dist/surface/index.d.ts +10 -2
  93. package/dist/surface/index.js +1 -1
  94. package/dist/undo/index.d.ts +5 -3
  95. package/package.json +11 -11
  96. package/src/annotations/AnnotationOverlay.abandon.test.tsx +143 -0
  97. package/src/annotations/AnnotationOverlay.tsx +50 -31
  98. package/src/annotations/Annotations.overlay.test.tsx +31 -0
  99. package/src/annotations/capture.browser.test.ts +47 -0
  100. package/src/annotations/capture.ts +14 -7
  101. package/src/annotations/drawOne.ts +2 -2
  102. package/src/annotations/frac.test.ts +44 -1
  103. package/src/annotations/frac.ts +46 -18
  104. package/src/annotations/index.ts +1 -0
  105. package/src/annotations/paint.test.ts +74 -33
  106. package/src/annotations/paint.ts +70 -32
  107. package/src/annotations/store.test.ts +212 -2
  108. package/src/annotations/store.ts +130 -27
  109. package/src/annotations/svgNodes.test.ts +22 -10
  110. package/src/annotations/svgNodes.ts +3 -3
  111. package/src/annotations/types.ts +29 -10
  112. package/src/canvas/CameraInput.test.tsx +30 -0
  113. package/src/canvas/CameraInput.tsx +16 -15
  114. package/src/canvas/CanvasStack.tsx +5 -5
  115. package/src/canvas/CanvasStackContext.ts +4 -1
  116. package/src/canvas/Stage.test.tsx +34 -25
  117. package/src/canvas/Stage.tsx +3 -3
  118. package/src/canvas/cameraRegistry.ts +1 -0
  119. package/src/canvas/worldSpec.ts +3 -1
  120. package/src/chrome/builtins.test.ts +7 -6
  121. package/src/chrome/builtins.tsx +5 -1
  122. package/src/chrome/index.ts +1 -1
  123. package/src/chrome/labRegions.test.tsx +37 -26
  124. package/src/chrome/merge.test.ts +5 -0
  125. package/src/chrome/merge.ts +4 -2
  126. package/src/chrome/regions/PaletteRegion.test.tsx +4 -5
  127. package/src/chrome/regions/PaletteRegion.tsx +3 -4
  128. package/src/chrome/regions/ViewportRegion.browser.test.less +6 -0
  129. package/src/chrome/regions/ViewportRegion.browser.test.tsx +38 -0
  130. package/src/chrome/regions/ViewportRegion.less +6 -0
  131. package/src/chrome/regions/ViewportRegion.tsx +10 -4
  132. package/src/chrome/types.ts +4 -2
  133. package/src/config/auto.ts +1 -0
  134. package/src/config/builder.ts +6 -0
  135. package/src/config/types.ts +2 -1
  136. package/src/controls/ControlMatrix.less +245 -0
  137. package/src/controls/ControlMatrix.stories.less +13 -0
  138. package/src/controls/ControlMatrix.stories.tsx +135 -0
  139. package/src/controls/ControlMatrix.test.tsx +206 -0
  140. package/src/controls/ControlMatrix.tsx +446 -0
  141. package/src/controls/ControlPanel.test.tsx +37 -3
  142. package/src/controls/ControlPanel.tsx +16 -5
  143. package/src/controls/index.ts +6 -0
  144. package/src/dragdrop/DragDropRuntime.tsx +22 -13
  145. package/src/dragdrop/DragGhost.tsx +3 -0
  146. package/src/dragdrop/Palette.tsx +2 -0
  147. package/src/dragdrop/dragDrop.test.tsx +34 -33
  148. package/src/index.test.ts +5 -0
  149. package/src/index.ts +7 -1
  150. package/src/instrument/types.ts +0 -6
  151. package/src/job/useJob.ts +14 -9
  152. package/src/lab/Lab.chrome.test.tsx +15 -10
  153. package/src/lab/Lab.nebula.test.tsx +5 -5
  154. package/src/lab/Lab.surface.test.tsx +30 -21
  155. package/src/lab/Lab.test.tsx +75 -74
  156. package/src/lab/Lab.tsx +5 -1
  157. package/src/lab/Lab.undock.test.tsx +15 -10
  158. package/src/lab/LabHeader.test.tsx +15 -14
  159. package/src/lab/LabPalette.test.tsx +12 -7
  160. package/src/lab/LabShell.less +1 -1
  161. package/src/lab/LabSwitcher.browser.test.tsx +27 -0
  162. package/src/lab/LabSwitcher.less +2 -0
  163. package/src/lab/LabZoom.test.tsx +55 -44
  164. package/src/lab/LabZoom.tsx +4 -4
  165. package/src/lab/Workspace.surface.test.tsx +16 -13
  166. package/src/lab/Workspace.test.tsx +32 -27
  167. package/src/lab/Workspace.tsx +48 -52
  168. package/src/loupe/AGENTS.md +28 -15
  169. package/src/loupe/CanvasLoupe.tsx +2 -3
  170. package/src/loupe/DomLoupe.tsx +1 -7
  171. package/src/loupe/Loupe.less +16 -0
  172. package/src/loupe/TrialLoupe.tsx +120 -57
  173. package/src/loupe/index.ts +1 -6
  174. package/src/loupe/types.test.ts +3 -3
  175. package/src/loupe/types.ts +24 -27
  176. package/src/loupe/useLoupe.test.tsx +29 -4
  177. package/src/loupe/useLoupe.ts +20 -17
  178. package/src/overview/OverviewMarks.test.tsx +9 -8
  179. package/src/overview/OverviewMarks.tsx +12 -4
  180. package/src/overview/TrialOverview.test.tsx +18 -17
  181. package/src/overview/TrialOverview.tsx +3 -3
  182. package/src/passthrough/weasel-ui.test.ts +1 -1
  183. package/src/passthrough/weasel-ui.ts +1 -1
  184. package/src/primitives/FloatingPanel.tsx +7 -3
  185. package/src/primitives/FpsMeter.less +1 -2
  186. package/src/primitives/JobProgress.less +1 -1
  187. package/src/primitives/Readout.test.tsx +2 -2
  188. package/src/primitives/ScaleIndicator.less +1 -1
  189. package/src/primitives/Split.tsx +69 -41
  190. package/src/primitives/StatusBar.less +1 -1
  191. package/src/primitives/Toolbar.tsx +3 -3
  192. package/src/primitives/ZoomControl.less +1 -2
  193. package/src/specimen/Specimen.less +1 -0
  194. package/src/specimen/Specimen.tsx +0 -1
  195. package/src/state/undock.ts +3 -0
  196. package/src/state/useOpenOnce.ts +20 -16
  197. package/src/state/view.ts +1 -0
  198. package/src/styles.less +3 -0
  199. package/src/surface/index.ts +1 -1
  200. package/src/surface/useTiledSurface.test.tsx +14 -0
  201. package/src/surface/useTiledSurface.ts +11 -3
  202. package/src/trial/Trial.annotations.persist.test.tsx +20 -9
  203. package/src/trial/Trial.annotations.test.tsx +105 -27
  204. package/src/trial/Trial.canvas.test.tsx +21 -17
  205. package/src/trial/Trial.config.test.tsx +34 -27
  206. package/src/trial/Trial.less +0 -16
  207. package/src/trial/Trial.loupe.test.tsx +51 -25
  208. package/src/trial/Trial.targets.test.tsx +9 -8
  209. package/src/trial/Trial.test.tsx +51 -46
  210. package/src/trial/Trial.trialId.test.tsx +7 -6
  211. package/src/trial/Trial.tsx +74 -62
  212. package/src/trial/TrialChrome.tsx +6 -3
  213. package/src/trial/index.ts +2 -0
  214. package/src/trial/loupeSwitch.ts +15 -0
  215. package/dist/_dts/LayerList.d-DiuEArKq.d.ts +0 -83
  216. package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +0 -79
  217. package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +0 -141
  218. package/dist/_dts/Select.d-Bensr_MT.d.ts +0 -579
  219. package/dist/_dts/index-B2Yj5aA0.d.ts +0 -274
  220. package/dist/chunk-275JRRVD.js.map +0 -1
  221. package/dist/chunk-4LJSC7MB.js +0 -143
  222. package/dist/chunk-4LJSC7MB.js.map +0 -1
  223. package/dist/chunk-7BO7HNGF.js.map +0 -1
  224. package/dist/chunk-CQLQPQ4P.js.map +0 -1
  225. package/dist/chunk-FMK7UQ6V.js.map +0 -1
  226. package/dist/chunk-FZ7YFQN7.js.map +0 -1
  227. package/dist/chunk-HXXTULDN.js.map +0 -1
  228. package/dist/chunk-I5DUQ5HM.js.map +0 -1
  229. package/dist/chunk-IJJUK4FU.js.map +0 -1
  230. package/dist/chunk-J7OWWTCW.js.map +0 -1
  231. package/dist/chunk-M7HP3FTK.js +0 -549
  232. package/dist/chunk-M7HP3FTK.js.map +0 -1
  233. package/dist/chunk-MIM5R4ZT.js +0 -14972
  234. package/dist/chunk-MIM5R4ZT.js.map +0 -1
  235. package/dist/chunk-RWNDDKOH.js.map +0 -1
  236. package/dist/chunk-T5IBSPAM.js.map +0 -1
  237. package/dist/chunk-UNAZSLWE.js.map +0 -1
  238. package/dist/chunk-WPLAANH4.js.map +0 -1
  239. package/dist/chunk-Y23G2VZL.js.map +0 -1
  240. package/dist/chunk-ZIFPHIWZ.js.map +0 -1
  241. package/src/primitives/useRovingTabIndex.test.ts +0 -48
  242. package/src/primitives/useRovingTabIndex.ts +0 -88
@@ -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
  });
package/src/lab/Lab.tsx CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  type ReactNode,
6
6
  useCallback,
7
7
  useEffect,
8
+ useInsertionEffect,
8
9
  useLayoutEffect,
9
10
  useMemo,
10
11
  useRef,
@@ -418,7 +419,10 @@ function LabRuntime({
418
419
  }, []);
419
420
 
420
421
  const ownSurface = useTiledSurface({ onFrame });
421
- surfaceRef.current = ownSurface;
422
+ // Not `useLatest`: `onFrame` reads it, and is needed before the surface exists.
423
+ useInsertionEffect(() => {
424
+ surfaceRef.current = ownSurface;
425
+ }, [ownSurface]);
422
426
  const [labBody, setLabBody] = useState<HTMLDivElement | null>(null);
423
427
  const attachOwnSurface = ownSurface.containerRef;
424
428
  const labBodyRef = useCallback(
@@ -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
  });
@@ -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,5 +162,5 @@
162
162
  display: inline-block;
163
163
  min-width: 5ch;
164
164
  text-align: center;
165
- font-variant-numeric: tabular-nums;
165
+ .numeric();
166
166
  }
@@ -0,0 +1,27 @@
1
+ import '@weasel-js/theme/tokens.css';
2
+ import '../styles.less';
3
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react';
4
+ import { afterEach, expect, test } from 'vitest';
5
+ import { LabShell } from './LabShell';
6
+
7
+ // The menu sits under the shell's title, so what it inherits from the title is
8
+ // cascade, which only a real browser computes.
9
+
10
+ afterEach(cleanup);
11
+
12
+ const PAGES = [
13
+ { href: '/corpus', label: 'Wall' },
14
+ { href: '/stats', label: 'Dashboard' },
15
+ ];
16
+
17
+ test('only the open page is bold in the shell title menu', () => {
18
+ render(
19
+ <LabShell title="corpus" pages={PAGES} path="/corpus">
20
+ <p>body</p>
21
+ </LabShell>,
22
+ );
23
+ fireEvent.click(screen.getByRole('button', { name: /corpus/ }));
24
+ const weight = (name: string) =>
25
+ Number(getComputedStyle(screen.getByRole('menuitem', { name })).fontWeight);
26
+ expect(weight('Wall')).toBeGreaterThan(weight('Dashboard'));
27
+ });
@@ -59,6 +59,8 @@
59
59
  display: flex;
60
60
  flex-direction: column;
61
61
  padding: var(--wzl-space-xs);
62
+ // A host's title class lands on the wrapper, so the menu would inherit its weight.
63
+ font-weight: var(--wzl-font-weight-normal);
62
64
  // `--wzl-surface-raised` is translucent by design -- it is for panels that
63
65
  // blur what sits behind them. A menu has to be read, so it takes the opaque
64
66
  // surface, the way `FloatingPanel` does.