@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,8 +1,10 @@
1
- import { act, fireEvent, render, screen } from '@testing-library/react';
1
+ import { act, fireEvent, screen } from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
+ import { renderSettled } from '@weasel-js/react/testing/renderSettled';
3
4
  import { beforeAll, describe, expect, it, vi } from 'vitest';
4
5
  import { defineInstrument } from '../instrument/defineInstrument';
5
6
  import { Lab } from '../lab/Lab';
7
+ import { TrialLoupe } from '../loupe';
6
8
 
7
9
  beforeAll(() => {
8
10
  HTMLCanvasElement.prototype.getContext = vi.fn(() => ({
@@ -21,17 +23,19 @@ const canvasInstrument = defineInstrument<Record<string, never>, Record<string,
21
23
  name: 'Drawn',
22
24
  defaultConfig: () => ({}),
23
25
  initialState: () => ({}),
24
- render: () => null,
26
+ render: () => <TrialLoupe />,
25
27
  canvas: { layers: [{ id: 'main', draw: () => undefined }] },
26
- loupe: true,
27
28
  });
28
29
 
29
30
  const domInstrument = defineInstrument<Record<string, never>, Record<string, never>>({
30
31
  name: 'Written',
31
32
  defaultConfig: () => ({}),
32
33
  initialState: () => ({}),
33
- render: () => <p data-testid="body">the content</p>,
34
- loupe: { render: ({ view }) => <p data-testid="lens-body">zoom {view.zoom}</p> },
34
+ render: () => (
35
+ <TrialLoupe render={({ view }) => <p data-testid="lens-body">zoom {view.zoom}</p>}>
36
+ <p data-testid="body">the content</p>
37
+ </TrialLoupe>
38
+ ),
35
39
  });
36
40
 
37
41
  const noLoupe = defineInstrument<Record<string, never>, Record<string, never>>({
@@ -49,28 +53,30 @@ function pointAt(host: Element, x: number, y: number): void {
49
53
  fireEvent.pointerMove(host, { clientX: x, clientY: y });
50
54
  }
51
55
 
52
- describe('the loupe capability', () => {
53
- it('gives a trial a toolbar toggle, and no lens until it is on', () => {
54
- const { container } = render(
56
+ describe('a lens the instrument mounts', () => {
57
+ it('gives a trial a toolbar toggle, and no lens until it is on', async () => {
58
+ const { container } = await renderSettled(
55
59
  <Lab instruments={[canvasInstrument]} defaultInstrument="Drawn" />,
56
60
  );
57
61
  expect(screen.getByRole('button', { name: 'Loupe' })).toHaveAttribute('aria-pressed', 'false');
58
62
  expect(lens(container)).toBeNull();
59
63
  });
60
64
 
61
- it('offers nothing to an instrument that declares none', () => {
62
- render(<Lab instruments={[noLoupe]} defaultInstrument="Plain" />);
65
+ it('offers nothing to an instrument that mounts none', async () => {
66
+ await renderSettled(<Lab instruments={[noLoupe]} defaultInstrument="Plain" />);
63
67
  expect(screen.queryByRole('button', { name: 'Loupe' })).toBeNull();
64
68
  });
65
69
 
66
- it('drops out of a trial that suppresses it', () => {
67
- render(<Lab instruments={[canvasInstrument]} defaultInstrument="Drawn" suppress={['loupe']} />);
70
+ it('drops out of a trial that suppresses it', async () => {
71
+ await renderSettled(
72
+ <Lab instruments={[canvasInstrument]} defaultInstrument="Drawn" suppress={['loupe']} />,
73
+ );
68
74
  expect(screen.queryByRole('button', { name: 'Loupe' })).toBeNull();
69
75
  });
70
76
 
71
77
  it('raises the lens over a canvas stack once it is on and aimed', async () => {
72
78
  const user = userEvent.setup();
73
- const { container } = render(
79
+ const { container } = await renderSettled(
74
80
  <Lab instruments={[canvasInstrument]} defaultInstrument="Drawn" />,
75
81
  );
76
82
  await user.click(screen.getByRole('button', { name: 'Loupe' }));
@@ -92,10 +98,12 @@ describe('the loupe capability', () => {
92
98
 
93
99
  it('asks a DOM instrument to draw itself again at the magnified camera', async () => {
94
100
  const user = userEvent.setup();
95
- const { container } = render(<Lab instruments={[domInstrument]} defaultInstrument="Written" />);
101
+ const { container } = await renderSettled(
102
+ <Lab instruments={[domInstrument]} defaultInstrument="Written" />,
103
+ );
96
104
  await user.click(screen.getByRole('button', { name: 'Loupe' }));
97
105
 
98
- const host = container.querySelector('.lk-trial__loupe-host');
106
+ const host = container.querySelector('.lk-loupe-host');
99
107
  if (!host) throw new Error('no loupe host');
100
108
  pointAt(host, 60, 40);
101
109
 
@@ -105,14 +113,33 @@ describe('the loupe capability', () => {
105
113
  expect(screen.getByTestId('body')).toBeInTheDocument();
106
114
  });
107
115
 
108
- it('wraps a DOM instrument only when it declares a loupe', () => {
109
- const { container } = render(<Lab instruments={[noLoupe]} defaultInstrument="Plain" />);
110
- expect(container.querySelector('.lk-trial__loupe-host')).toBeNull();
116
+ it('wraps nothing for an instrument that mounts no lens', async () => {
117
+ const { container } = await renderSettled(
118
+ <Lab instruments={[noLoupe]} defaultInstrument="Plain" />,
119
+ );
120
+ expect(container.querySelector('.lk-loupe-host')).toBeNull();
121
+ });
122
+
123
+ it('raises a lens told `enabled` without the trial toggle', async () => {
124
+ const always = defineInstrument<Record<string, never>, Record<string, never>>({
125
+ name: 'Always',
126
+ defaultConfig: () => ({}),
127
+ initialState: () => ({}),
128
+ render: () => <TrialLoupe enabled />,
129
+ canvas: { layers: [{ id: 'main', draw: () => undefined }] },
130
+ });
131
+ const { container } = await renderSettled(
132
+ <Lab instruments={[always]} defaultInstrument="Always" />,
133
+ );
134
+ const stack = container.querySelector('.lk-canvas-stack');
135
+ if (!stack) throw new Error('no canvas stack');
136
+ pointAt(stack, 60, 40);
137
+ expect(lens(container)).not.toBeNull();
111
138
  });
112
139
 
113
140
  it('puts the lens away again when the toggle goes off', async () => {
114
141
  const user = userEvent.setup();
115
- const { container } = render(
142
+ const { container } = await renderSettled(
116
143
  <Lab instruments={[canvasInstrument]} defaultInstrument="Drawn" />,
117
144
  );
118
145
  const toggle = screen.getByRole('button', { name: 'Loupe' });
@@ -130,13 +157,13 @@ describe('the loupe capability', () => {
130
157
  describe('the loupe and the trial camera share one dispatcher', () => {
131
158
  const frame = () => new Promise<void>((r) => requestAnimationFrame(() => r()));
132
159
 
133
- function mountDrawn() {
160
+ async function mountDrawn() {
134
161
  const zooms: number[] = [];
135
162
  const drawn = defineInstrument<Record<string, never>, Record<string, never>>({
136
163
  name: 'Zoomed',
137
164
  defaultConfig: () => ({}),
138
165
  initialState: () => ({}),
139
- render: () => null,
166
+ render: () => <TrialLoupe />,
140
167
  canvas: {
141
168
  layers: [
142
169
  {
@@ -147,9 +174,8 @@ describe('the loupe and the trial camera share one dispatcher', () => {
147
174
  },
148
175
  ],
149
176
  },
150
- loupe: true,
151
177
  });
152
- const r = render(<Lab instruments={[drawn]} defaultInstrument="Zoomed" />);
178
+ const r = await renderSettled(<Lab instruments={[drawn]} defaultInstrument="Zoomed" />);
153
179
  const stack = r.container.querySelector('.lk-canvas-stack');
154
180
  if (!stack) throw new Error('no canvas stack');
155
181
  return { ...r, stack, zooms };
@@ -168,7 +194,7 @@ describe('the loupe and the trial camera share one dispatcher', () => {
168
194
  };
169
195
 
170
196
  it('zooms the camera while the lens is down', async () => {
171
- const h = mountDrawn();
197
+ const h = await mountDrawn();
172
198
  await act(async () => {
173
199
  await frame();
174
200
  });
@@ -181,7 +207,7 @@ describe('the loupe and the trial camera share one dispatcher', () => {
181
207
 
182
208
  it('gives the wheel to the lens while it is up, and leaves the camera alone', async () => {
183
209
  const user = userEvent.setup();
184
- const h = mountDrawn();
210
+ const h = await mountDrawn();
185
211
  await user.click(screen.getByRole('button', { name: 'Loupe' }));
186
212
  pointAt(h.stack, 60, 40);
187
213
  expect(lens(h.container)).not.toBeNull();
@@ -4,7 +4,8 @@
4
4
  * trial open, a `targets` that ignored the trial it was asked about would look
5
5
  * identical.
6
6
  */
7
- import { act, fireEvent, render, screen } from '@testing-library/react';
7
+ import { act, fireEvent, screen } from '@testing-library/react';
8
+ import { renderSettled } from '@weasel-js/react/testing/renderSettled';
8
9
  import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
9
10
  import type { AnnotationTarget } from '../annotations/types';
10
11
  import { defineInstrument } from '../instrument/defineInstrument';
@@ -63,13 +64,13 @@ function CaptureLab() {
63
64
  );
64
65
  }
65
66
 
66
- function mountTwoTrials(): [string, string] {
67
- render(
67
+ async function mountTwoTrials(): Promise<[string, string]> {
68
+ await renderSettled(
68
69
  <Lab instruments={[subject]} defaultInstrument="Subject">
69
70
  <CaptureLab />
70
71
  </Lab>,
71
72
  );
72
- act(() => labRef?.addTrial('Subject'));
73
+ await act(async () => labRef?.addTrial('Subject'));
73
74
  const ids = labRef?.trials.map((t) => t.id) ?? [];
74
75
  const [a, b] = ids;
75
76
  if (!a || !b) throw new Error('expected two trials');
@@ -82,13 +83,13 @@ describe('annotations.targets', () => {
82
83
  labRef = null;
83
84
  });
84
85
 
85
- it('is told which trial is asking', () => {
86
- const [a, b] = mountTwoTrials();
86
+ it('is told which trial is asking', async () => {
87
+ const [a, b] = await mountTwoTrials();
87
88
  expect(new Set(asked.map((t) => t.id))).toEqual(new Set([a, b]));
88
89
  });
89
90
 
90
- it("is told that trial's own view, not the other's", () => {
91
- const [a, b] = mountTwoTrials();
91
+ it("is told that trial's own view, not the other's", async () => {
92
+ const [a, b] = await mountTwoTrials();
92
93
  fireEvent.click(screen.getByTestId(`zoom-${a}`));
93
94
  expect(lastAsk(a).view).toEqual({ zoom: 3, pan: { x: 0, y: 0 } });
94
95
  expect(lastAsk(b).view).toEqual({ zoom: 1, pan: { x: 0, y: 0 } });
@@ -1,4 +1,5 @@
1
- import { fireEvent, render, screen, within } from '@testing-library/react';
1
+ import { fireEvent, screen, within } from '@testing-library/react';
2
+ import { renderSettled } from '@weasel-js/react/testing/renderSettled';
2
3
  import { type ReactNode, useState } from 'react';
3
4
  import { describe, expect, it, vi } from 'vitest';
4
5
  import { useStore } from 'zustand/react';
@@ -102,19 +103,19 @@ function ChromeFromStore({
102
103
  }
103
104
 
104
105
  describe('<TrialChrome>', () => {
105
- it('renders children in the content area', () => {
106
- render(<ChromeHarness />);
106
+ it('renders children in the content area', async () => {
107
+ await renderSettled(<ChromeHarness />);
107
108
  expect(screen.getByTestId('content')).toBeInTheDocument();
108
109
  });
109
110
 
110
- it('renders the built-in trial actions', () => {
111
- render(<ChromeHarness />);
111
+ it('renders the built-in trial actions', async () => {
112
+ await renderSettled(<ChromeHarness />);
112
113
  expect(screen.getByRole('button', { name: 'Close trial' })).toBeInTheDocument();
113
114
  expect(screen.getByRole('button', { name: 'Clone trial' })).toBeInTheDocument();
114
115
  });
115
116
 
116
- it('puts clone, reset and snapshot at the end of the title bar, beside close', () => {
117
- const { container } = render(<ChromeHarness />);
117
+ it('puts clone, reset and snapshot at the end of the title bar, beside close', async () => {
118
+ const { container } = await renderSettled(<ChromeHarness />);
118
119
  const titlebar = container.querySelector('.lk-trial__titlebar');
119
120
  if (!titlebar) throw new Error('no title bar');
120
121
  expect(titlebar).toContainElement(screen.getByRole('button', { name: 'Clone trial' }));
@@ -125,15 +126,15 @@ describe('<TrialChrome>', () => {
125
126
  expect(labels).toEqual(['Clone trial', 'Reset trial', 'Save snapshot', 'Close trial']);
126
127
  });
127
128
 
128
- it('disables close on the last trial', () => {
129
- render(<ChromeHarness isLastTrial />);
129
+ it('disables close on the last trial', async () => {
130
+ await renderSettled(<ChromeHarness isLastTrial />);
130
131
  expect(screen.getByRole('button', { name: /close/i })).toBeDisabled();
131
132
  });
132
133
 
133
- it('omits undo and redo unless the instrument declares undo', () => {
134
- render(<ChromeHarness />);
134
+ it('omits undo and redo unless the instrument declares undo', async () => {
135
+ await renderSettled(<ChromeHarness />);
135
136
  expect(screen.queryByRole('button', { name: 'Undo' })).toBeNull();
136
- render(
137
+ await renderSettled(
137
138
  <ChromeHarness
138
139
  instrument={{ ...stubInstrument, undo: {} }}
139
140
  undoBindings={{ canUndo: true, canRedo: false, undo: vi.fn(), redo: vi.fn() }}
@@ -143,7 +144,7 @@ describe('<TrialChrome>', () => {
143
144
  expect(screen.getByRole('button', { name: 'Redo' })).toBeDisabled();
144
145
  });
145
146
 
146
- it('offers the snapshot loader once a snapshot exists', () => {
147
+ it('offers the snapshot loader once a snapshot exists', async () => {
147
148
  const snapshot: SavedSnapshot = {
148
149
  id: 's1',
149
150
  name: 'First',
@@ -153,17 +154,19 @@ describe('<TrialChrome>', () => {
153
154
  state: {},
154
155
  savedAt: 1,
155
156
  };
156
- render(<ChromeHarness labOverrides={{ savedSnapshots: [snapshot] }} />);
157
+ await renderSettled(<ChromeHarness labOverrides={{ savedSnapshots: [snapshot] }} />);
157
158
  expect(screen.getByRole('button', { name: /load snapshot/i })).toBeInTheDocument();
158
159
  });
159
160
 
160
- it('renders a consumer contribution alongside the built-ins', () => {
161
- render(<ChromeHarness chrome={[{ id: 'mine', region: 'status', item: { text: 'ready' } }]} />);
161
+ it('renders a consumer contribution alongside the built-ins', async () => {
162
+ await renderSettled(
163
+ <ChromeHarness chrome={[{ id: 'mine', region: 'status', item: { text: 'ready' } }]} />,
164
+ );
162
165
  expect(screen.getByText('ready')).toBeInTheDocument();
163
166
  });
164
167
 
165
- it('leads the title bar with a contribution that does not set `end`', () => {
166
- const { container } = render(
168
+ it('leads the title bar with a contribution that does not set `end`', async () => {
169
+ const { container } = await renderSettled(
167
170
  <ChromeHarness
168
171
  chrome={[
169
172
  {
@@ -183,8 +186,8 @@ describe('<TrialChrome>', () => {
183
186
  expect(lead.compareDocumentPosition(heading) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
184
187
  });
185
188
 
186
- it('reads the instrument name until a contribution calls setTitle', () => {
187
- render(
189
+ it('reads the instrument name until a contribution calls setTitle', async () => {
190
+ await renderSettled(
188
191
  <ChromeHarness
189
192
  chrome={[
190
193
  {
@@ -205,9 +208,9 @@ describe('<TrialChrome>', () => {
205
208
  expect(screen.getByRole('region', { name: 'Trial Sprocket 7' })).toBeInTheDocument();
206
209
  });
207
210
 
208
- it("reads the instrument's title over its name, and returns to it on setTitle(null)", () => {
211
+ it("reads the instrument's title over its name, and returns to it on setTitle(null)", async () => {
209
212
  const titled: Instrument = { ...stubInstrument, title: 'Stub / Default' };
210
- render(
213
+ await renderSettled(
211
214
  <ChromeHarness
212
215
  instrument={titled}
213
216
  chrome={[
@@ -232,9 +235,9 @@ describe('<TrialChrome>', () => {
232
235
  expect(screen.getByRole('region', { name: 'Trial Stub / Default' })).toBeInTheDocument();
233
236
  });
234
237
 
235
- it('Cmd+S triggers saveSnapshot', () => {
238
+ it('Cmd+S triggers saveSnapshot', async () => {
236
239
  const saveSnapshot = vi.fn();
237
- render(<ChromeHarness labOverrides={{ saveSnapshot }} />);
240
+ await renderSettled(<ChromeHarness labOverrides={{ saveSnapshot }} />);
238
241
  const region = screen.getByRole('region', { name: /trial/i });
239
242
  fireEvent.keyDown(region, { key: 's', metaKey: true });
240
243
  expect(saveSnapshot).toHaveBeenCalledWith('ws-1', undefined);
@@ -242,30 +245,32 @@ describe('<TrialChrome>', () => {
242
245
  });
243
246
 
244
247
  describe('chrome regions in a mounted lab', () => {
245
- function renderLabWith(instrument: Instrument) {
246
- return render(<Lab title="T" instruments={[instrument]} defaultInstrument={instrument.name} />);
248
+ async function renderLabWith(instrument: Instrument) {
249
+ return renderSettled(
250
+ <Lab title="T" instruments={[instrument]} defaultInstrument={instrument.name} />,
251
+ );
247
252
  }
248
253
 
249
- it('puts zoom in the viewport region and not in the toolbar', () => {
250
- renderLabWith({ ...stubInstrument, canvas: { layers: [] }, undo: {} });
254
+ it('puts zoom in the viewport region and not in the toolbar', async () => {
255
+ await renderLabWith({ ...stubInstrument, canvas: { layers: [] }, undo: {} });
251
256
  const toolbar = document.querySelector('.lk-trial__toolbar') as HTMLElement;
252
257
  const viewport = document.querySelector('.lk-viewport-controls') as HTMLElement;
253
258
  expect(within(viewport).getByRole('button', { name: 'Zoom in' })).toBeInTheDocument();
254
259
  expect(within(toolbar).queryByRole('button', { name: 'Zoom in' })).toBeNull();
255
260
  });
256
261
 
257
- it('renders no undo group for an instrument that does not declare undo', () => {
258
- renderLabWith({ ...stubInstrument, canvas: { layers: [] } });
262
+ it('renders no undo group for an instrument that does not declare undo', async () => {
263
+ await renderLabWith({ ...stubInstrument, canvas: { layers: [] } });
259
264
  expect(screen.queryByRole('button', { name: 'Undo' })).toBeNull();
260
265
  });
261
266
 
262
- it('renders no viewport region for an instrument with no canvas', () => {
263
- renderLabWith(stubInstrument);
267
+ it('renders no viewport region for an instrument with no canvas', async () => {
268
+ await renderLabWith(stubInstrument);
264
269
  expect(document.querySelector('.lk-viewport-controls')).toBeNull();
265
270
  });
266
271
 
267
- it('Lab provides context for nested TrialChrome', () => {
268
- renderLabWith(stubInstrument);
272
+ it('Lab provides context for nested TrialChrome', async () => {
273
+ await renderLabWith(stubInstrument);
269
274
  expect(document.querySelector('.lk-lab')).toBeTruthy();
270
275
  });
271
276
  });
@@ -291,46 +296,46 @@ describe('<TrialChrome> — state that outlives a mount', () => {
291
296
  },
292
297
  ];
293
298
 
294
- it('keeps a title across a remount', () => {
299
+ it('keeps a title across a remount', async () => {
295
300
  const store = makeChromeStore();
296
- const first = render(<ChromeHarness store={store} chrome={renameChrome} />);
301
+ const first = await renderSettled(<ChromeHarness store={store} chrome={renameChrome} />);
297
302
  fireEvent.click(screen.getByRole('button', { name: 'Rename' }));
298
303
  first.unmount();
299
304
 
300
- render(<ChromeHarness store={store} chrome={renameChrome} />);
305
+ await renderSettled(<ChromeHarness store={store} chrome={renameChrome} />);
301
306
  expect(screen.getByRole('region', { name: 'Trial Sprocket 7' })).toBeInTheDocument();
302
307
  });
303
308
 
304
- it('keeps a folded sidebar section across a remount', () => {
309
+ it('keeps a folded sidebar section across a remount', async () => {
305
310
  const store = makeChromeStore();
306
- const first = render(<ChromeHarness store={store} chrome={sectionChrome} />);
311
+ const first = await renderSettled(<ChromeHarness store={store} chrome={sectionChrome} />);
307
312
  expect(screen.getByTestId('notes-body')).toBeInTheDocument();
308
313
  fireEvent.click(screen.getByRole('button', { name: 'Notes' }));
309
314
  expect(screen.queryByTestId('notes-body')).toBeNull();
310
315
  first.unmount();
311
316
 
312
- render(<ChromeHarness store={store} chrome={sectionChrome} />);
317
+ await renderSettled(<ChromeHarness store={store} chrome={sectionChrome} />);
313
318
  expect(screen.getByRole('button', { name: 'Notes' })).toHaveAttribute('aria-expanded', 'false');
314
319
  expect(screen.queryByTestId('notes-body')).toBeNull();
315
320
  });
316
321
 
317
- it('keeps a folded settings group across a remount', () => {
322
+ it('keeps a folded settings group across a remount', async () => {
318
323
  const sectioned: Instrument = {
319
324
  ...stubInstrument,
320
325
  config: f.schema({ size: f.number(1).section('Shape') }),
321
326
  };
322
327
  const store = makeChromeStore();
323
- const first = render(<ChromeHarness store={store} instrument={sectioned} />);
328
+ const first = await renderSettled(<ChromeHarness store={store} instrument={sectioned} />);
324
329
  const twisty = screen.getByRole('button', { name: 'Shape' });
325
330
  expect(twisty).toHaveAttribute('aria-expanded', 'true');
326
331
  fireEvent.click(twisty);
327
332
  first.unmount();
328
333
 
329
- render(<ChromeHarness store={store} instrument={sectioned} />);
334
+ await renderSettled(<ChromeHarness store={store} instrument={sectioned} />);
330
335
  expect(screen.getByRole('button', { name: 'Shape' })).toHaveAttribute('aria-expanded', 'false');
331
336
  });
332
337
 
333
- it('reopens a section that starts collapsed, and keeps it open across a remount', () => {
338
+ it('reopens a section that starts collapsed, and keeps it open across a remount', async () => {
334
339
  const collapsedChrome: ChromeProps['chrome'] = [
335
340
  {
336
341
  id: 'notes',
@@ -343,12 +348,12 @@ describe('<TrialChrome> — state that outlives a mount', () => {
343
348
  },
344
349
  ];
345
350
  const store = makeChromeStore();
346
- const first = render(<ChromeHarness store={store} chrome={collapsedChrome} />);
351
+ const first = await renderSettled(<ChromeHarness store={store} chrome={collapsedChrome} />);
347
352
  expect(screen.queryByTestId('notes-body')).toBeNull();
348
353
  fireEvent.click(screen.getByRole('button', { name: 'Notes' }));
349
354
  first.unmount();
350
355
 
351
- render(<ChromeHarness store={store} chrome={collapsedChrome} />);
356
+ await renderSettled(<ChromeHarness store={store} chrome={collapsedChrome} />);
352
357
  expect(screen.getByTestId('notes-body')).toBeInTheDocument();
353
358
  });
354
359
  });
@@ -4,7 +4,8 @@
4
4
  * `SingletonExperiment` — so `useTrialState()` threw everywhere else, which
5
5
  * made the documented hook pattern unreachable from an instrument's `render`.
6
6
  */
7
- import { act, render, screen } from '@testing-library/react';
7
+ import { act, screen } from '@testing-library/react';
8
+ import { renderSettled } from '@weasel-js/react/testing/renderSettled';
8
9
  import { beforeAll, describe, expect, it, vi } from 'vitest';
9
10
  import { defineInstrument } from '../instrument/defineInstrument';
10
11
  import { Lab } from '../lab/Lab';
@@ -30,16 +31,16 @@ const reader = defineInstrument<{ hits: number }, Record<string, never>>({
30
31
  });
31
32
 
32
33
  describe('<Trial> trial-scoped hooks', () => {
33
- it('lets an instrument read its own trial state through useTrialState', () => {
34
- render(<Lab instruments={[reader]} defaultInstrument="Reader" />);
34
+ it('lets an instrument read its own trial state through useTrialState', async () => {
35
+ await renderSettled(<Lab instruments={[reader]} defaultInstrument="Reader" />);
35
36
  expect(screen.getByTestId('probe')).toHaveTextContent('hits:7');
36
37
  });
37
38
  });
38
39
 
39
40
  describe('<Trial> root element', () => {
40
- it('carries its trial id, so a lab can find a trial from outside', () => {
41
+ it('carries its trial id, so a lab can find a trial from outside', async () => {
41
42
  let labRef: LabContextValue | null = null;
42
- const { container } = render(
43
+ const { container } = await renderSettled(
43
44
  <Lab instruments={[reader]} defaultInstrument="Reader">
44
45
  <LabContext.Consumer>
45
46
  {(value) => {
@@ -49,7 +50,7 @@ describe('<Trial> root element', () => {
49
50
  </LabContext.Consumer>
50
51
  </Lab>,
51
52
  );
52
- act(() => (labRef as LabContextValue | null)?.addTrial('Reader'));
53
+ await act(async () => (labRef as LabContextValue | null)?.addTrial('Reader'));
53
54
  const ids = (labRef as LabContextValue | null)?.trials.map((t) => t.id) ?? [];
54
55
  expect(ids).toHaveLength(2);
55
56
  const roots = container.querySelectorAll<HTMLElement>('.lk-trial[data-trial-id]');