@weasel-js/labkit 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -4
- package/dist/_dts/{CanvasStackContext-CqkxkFJW.d.ts → CanvasStackContext-CMMo180l.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-ZTiwzYyT.d.ts +479 -0
- package/dist/_dts/{frac-8R6UmlvN.d.ts → frac-B4hFJMiN.d.ts} +54 -6
- package/dist/_dts/{index-retnAut7.d.ts → index-8LiKe3A-.d.ts} +77 -10
- package/dist/_dts/{index-B8uu2ba7.d.ts → index-DlKolEIZ.d.ts} +20 -8
- package/dist/_dts/{types-Ca2LCnPq.d.ts → types-Db-3NNk1.d.ts} +28 -4
- package/dist/_dts/{usePanZoom-nS798rOF.d.ts → usePanZoom-SSTZe410.d.ts} +4 -47
- package/dist/_dts/{useTrialState-jcx_TdU6.d.ts → useTrialState-pnAE-UBD.d.ts} +1 -1
- package/dist/canvas/index.d.ts +5 -5
- package/dist/canvas/index.js +1 -1
- package/dist/chrome/index.d.ts +4 -4
- package/dist/chrome/index.js +7 -7
- package/dist/chunk-6BUQ77ZV.js +18 -0
- package/dist/chunk-6BUQ77ZV.js.map +1 -0
- package/dist/{chunk-KJALCDWE.js → chunk-753Y6HE6.js} +9 -9
- package/dist/chunk-753Y6HE6.js.map +1 -0
- package/dist/{chunk-6ZDGOZQV.js → chunk-A2ACAGW6.js} +72 -20
- package/dist/chunk-A2ACAGW6.js.map +1 -0
- package/dist/{chunk-NS64DXMN.js → chunk-CKUKF72R.js} +44 -21
- package/dist/chunk-CKUKF72R.js.map +1 -0
- package/dist/{chunk-MUKOW3TC.js → chunk-D35PFTS7.js} +4 -4
- package/dist/{chunk-MUKOW3TC.js.map → chunk-D35PFTS7.js.map} +1 -1
- package/dist/{chunk-UU3NJO6N.js → chunk-EMXAIRUJ.js} +3 -3
- package/dist/{chunk-UU3NJO6N.js.map → chunk-EMXAIRUJ.js.map} +1 -1
- package/dist/{chunk-ZPH5WTWF.js → chunk-ERFMC6WU.js} +142 -66
- package/dist/chunk-ERFMC6WU.js.map +1 -0
- package/dist/chunk-FKLT3ITC.js +590 -0
- package/dist/chunk-FKLT3ITC.js.map +1 -0
- package/dist/{chunk-QLSV2N3G.js → chunk-IJJUK4FU.js} +25 -13
- package/dist/chunk-IJJUK4FU.js.map +1 -0
- package/dist/{chunk-EKIICY6X.js → chunk-KJCUERQF.js} +37 -25
- package/dist/chunk-KJCUERQF.js.map +1 -0
- package/dist/chunk-LASCBWW7.js +13456 -0
- package/dist/chunk-LASCBWW7.js.map +1 -0
- package/dist/{chunk-67SJLMC7.js → chunk-LRI3MBGR.js} +45 -12
- package/dist/chunk-LRI3MBGR.js.map +1 -0
- package/dist/chunk-REOBGYMC.js +150 -0
- package/dist/chunk-REOBGYMC.js.map +1 -0
- package/dist/{chunk-PGETSEDK.js → chunk-TT5C5S6R.js} +11 -90
- package/dist/chunk-TT5C5S6R.js.map +1 -0
- package/dist/{chunk-NDRYLVOW.js → chunk-Z76KEZAX.js} +2 -2
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/config/index.d.ts +62 -19
- package/dist/config/index.js +6 -4
- package/dist/controls/index.d.ts +3 -3
- package/dist/controls/index.js +4 -4
- package/dist/dragdrop/index.d.ts +3 -3
- package/dist/index.d.ts +230 -62
- package/dist/index.js +1367 -1698
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -4
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +67 -16
- package/dist/loupe/index.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +57 -230
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +3 -1
- package/dist/primitives/index.js +3 -3
- package/dist/state/index.d.ts +5 -5
- package/dist/state/index.js +5 -3
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +266 -57
- package/dist/surface/index.js +1 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +3 -3
- package/package.json +9 -9
- package/src/annotations/AnnotationOverlay.tsx +3 -0
- package/src/annotations/Annotations.less +4 -0
- package/src/annotations/Annotations.overlay.test.tsx +20 -0
- package/src/annotations/svgNodes.ts +1 -0
- package/src/annotations/toolMap.test.ts +9 -5
- package/src/annotations/toolMap.ts +3 -0
- package/src/canvas/CanvasStack.stories.tsx +1 -1
- package/src/canvas/index.ts +0 -9
- package/src/canvas/usePanZoom.test.ts +28 -0
- package/src/canvas/usePanZoom.ts +11 -4
- package/src/chrome/ChromeRegions.stories.tsx +1 -1
- package/src/chrome/LabChrome.tsx +21 -5
- package/src/chrome/builtins.test.ts +1 -0
- package/src/chrome/builtins.tsx +1 -0
- package/src/chrome/index.ts +1 -0
- package/src/chrome/labTypes.ts +3 -3
- package/src/chrome/regions/PaletteRegion.test.tsx +18 -0
- package/src/chrome/regions/PaletteRegion.tsx +6 -10
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/TitleBarRegion.tsx +1 -1
- package/src/chrome/regions/ToolbarRegion.tsx +11 -7
- package/src/chrome/types.ts +11 -2
- package/src/config/auto.test.ts +12 -0
- package/src/config/auto.ts +15 -0
- package/src/config/autoConfig.test.ts +107 -0
- package/src/config/autoConfig.ts +153 -0
- package/src/config/builder.test.ts +38 -0
- package/src/config/builder.ts +55 -11
- package/src/config/index.ts +3 -0
- package/src/config/path.ts +7 -2
- package/src/config/resolve.test.ts +25 -0
- package/src/config/resolve.ts +17 -1
- package/src/config/types.ts +32 -2
- package/src/config/useResolvedConfig.ts +26 -0
- package/src/controls/AutoControls.stories.tsx +88 -0
- package/src/controls/ControlPanel.less +23 -0
- package/src/controls/ControlPanel.stories.tsx +83 -1
- package/src/controls/ControlPanel.test.tsx +457 -4
- package/src/controls/ControlPanel.tsx +534 -73
- package/src/index.test.ts +0 -2
- package/src/index.ts +33 -11
- package/src/instrument/serializers.ts +29 -1
- package/src/lab/Lab.chrome.test.tsx +16 -0
- package/src/lab/Lab.stories.tsx +1 -1
- package/src/lab/Lab.surface.test.tsx +7 -2
- package/src/lab/Lab.test.tsx +79 -1
- package/src/lab/Lab.tsx +124 -16
- package/src/lab/LabContext.ts +8 -0
- package/src/lab/LabFit.stories.tsx +4 -4
- package/src/lab/LabFullChrome.stories.tsx +1 -1
- package/src/lab/LabHeader.tsx +0 -1
- package/src/lab/LabRoot.test.tsx +56 -0
- package/src/lab/LabRoot.tsx +47 -0
- package/src/lab/LabShell.less +24 -3
- package/src/lab/LabShell.stories.tsx +50 -1
- package/src/lab/LabShell.tsx +18 -27
- package/src/lab/LabSwitcher.less +10 -10
- package/src/lab/LabSwitcher.test.tsx +14 -9
- package/src/lab/LabSwitcher.tsx +8 -4
- package/src/lab/Workspace.less +1 -1
- package/src/lab/Workspace.stories.tsx +1 -1
- package/src/lab/Workspace.surface.test.tsx +1 -1
- package/src/lab/index.ts +2 -0
- package/src/layers/LayerList.less +1 -1
- package/src/layers/LayerList.stories.tsx +2 -2
- package/src/loupe/AGENTS.md +16 -4
- package/src/loupe/LoupeGestures.tsx +67 -0
- package/src/loupe/TrialLoupe.tsx +43 -28
- package/src/loupe/index.ts +4 -0
- package/src/loupe/loupeActions.ts +98 -0
- package/src/loupe/useLoupe.test.tsx +59 -2
- package/src/loupe/useLoupe.ts +28 -48
- package/src/passthrough/weasel-ui.ts +3 -3
- package/src/primitives/FloatingPanel.stories.tsx +1 -1
- package/src/primitives/FpsMeter.less +6 -1
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/FpsMeter.test.tsx +12 -4
- package/src/primitives/FpsMeter.tsx +6 -1
- package/src/primitives/JobProgress.stories.tsx +2 -2
- package/src/primitives/Legend.stories.tsx +1 -1
- package/src/primitives/ScaleIndicator.stories.tsx +1 -1
- package/src/primitives/Sidebar.less +7 -0
- package/src/primitives/Sidebar.stories.tsx +1 -1
- package/src/primitives/Split.test.tsx +25 -0
- package/src/primitives/Split.tsx +36 -20
- package/src/primitives/StatusBar.less +6 -0
- package/src/primitives/StatusBar.stories.tsx +1 -1
- package/src/primitives/Toolbar.stories.tsx +1 -1
- package/src/specimen/Specimen.less +95 -0
- package/src/specimen/Specimen.stories.tsx +13 -0
- package/src/specimen/Specimen.test.tsx +13 -0
- package/src/specimen/Specimen.tsx +888 -0
- package/src/state/Persistence.stories.tsx +4 -4
- package/src/state/store.test.ts +65 -0
- package/src/state/store.ts +32 -8
- package/src/state/types.ts +31 -2
- package/src/state/useTrialState.test.tsx +85 -0
- package/src/state/useTrialState.ts +9 -1
- package/src/state/view.test.ts +4 -4
- package/src/styles.less +1 -0
- package/src/surface/index.ts +1 -1
- package/src/surface/useSurfaceTile.test.tsx +1 -1
- package/src/surface/useTiledSurface.test.tsx +65 -10
- package/src/surface/useTiledSurface.ts +23 -11
- package/src/theme/Interstellar.stories.tsx +2 -2
- package/src/theme/interstellar.test.ts +16 -5
- package/src/theme/interstellar.theme.json +180 -0
- package/src/theme/interstellar.ts +8 -5
- package/src/tools/labTool.ts +3 -3
- package/src/trial/Trial.annotations.test.tsx +13 -4
- package/src/trial/Trial.canvas.test.tsx +36 -0
- package/src/trial/Trial.config.test.tsx +75 -2
- package/src/trial/Trial.less +4 -0
- package/src/trial/Trial.stories.tsx +4 -1
- package/src/trial/Trial.test.tsx +3 -0
- package/src/trial/Trial.tsx +21 -14
- package/src/trial/TrialChrome.tsx +17 -3
- package/src/trial/trialOps.test.ts +101 -1
- package/src/trial/trialOps.ts +62 -4
- package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +0 -116
- package/dist/chunk-67SJLMC7.js.map +0 -1
- package/dist/chunk-6ZDGOZQV.js.map +0 -1
- package/dist/chunk-E44SU6XS.js +0 -50
- package/dist/chunk-E44SU6XS.js.map +0 -1
- package/dist/chunk-EKIICY6X.js.map +0 -1
- package/dist/chunk-KJALCDWE.js.map +0 -1
- package/dist/chunk-NDRYLVOW.js.map +0 -1
- package/dist/chunk-NS64DXMN.js.map +0 -1
- package/dist/chunk-PGETSEDK.js.map +0 -1
- package/dist/chunk-QLSV2N3G.js.map +0 -1
- package/dist/chunk-S2SHZRD7.js +0 -7681
- package/dist/chunk-S2SHZRD7.js.map +0 -1
- package/dist/chunk-TQBBYTAF.js +0 -280
- package/dist/chunk-TQBBYTAF.js.map +0 -1
- package/dist/chunk-ZPH5WTWF.js.map +0 -1
- package/src/canvas/useOrbit.test.ts +0 -167
- package/src/canvas/useOrbit.ts +0 -156
- package/src/theme/interstellar.tokens.json +0 -61
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
1
|
+
import { act, fireEvent, render, screen, within } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
2
3
|
import { TOOL_PREF_KINDS, type ToolPrefKind } from '@weasel-js/core';
|
|
3
4
|
import type { PrefLeaf } from '@weasel-js/ui';
|
|
5
|
+
import { useState } from 'react';
|
|
4
6
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { auto } from '../config/auto';
|
|
5
8
|
import { f } from '../config/builder';
|
|
6
9
|
import { resolveConfigSchema } from '../config/resolve';
|
|
7
10
|
import type { ResolvedConfig } from '../config/types';
|
|
@@ -68,9 +71,12 @@ describe('<ControlPanel> select', () => {
|
|
|
68
71
|
},
|
|
69
72
|
];
|
|
70
73
|
render(<ControlPanel fields={fields} config={{ wave: 'sine' }} setConfig={setConfig} />);
|
|
71
|
-
const
|
|
72
|
-
expect(
|
|
73
|
-
|
|
74
|
+
const trigger = screen.getByRole('button', { name: 'Sine Wave' });
|
|
75
|
+
expect(trigger).toHaveTextContent('Sine');
|
|
76
|
+
act(() => {
|
|
77
|
+
fireEvent.click(trigger);
|
|
78
|
+
});
|
|
79
|
+
fireEvent.click(within(screen.getByRole('listbox')).getByRole('option', { name: 'Square' }));
|
|
74
80
|
expect(setConfig).toHaveBeenCalledWith('wave', 'square');
|
|
75
81
|
});
|
|
76
82
|
});
|
|
@@ -174,6 +180,22 @@ describe('<ControlPanel> schema', () => {
|
|
|
174
180
|
expect(setConfig).toHaveBeenCalledWith('showGrid', false);
|
|
175
181
|
});
|
|
176
182
|
|
|
183
|
+
it('draws a switch for a toggle() leaf and a checkbox for a plain one', () => {
|
|
184
|
+
const setConfig = vi.fn();
|
|
185
|
+
const schema = resolveConfigSchema(
|
|
186
|
+
f.schema({ showGrid: f.boolean(true), snap: f.boolean(true).toggle() }),
|
|
187
|
+
[],
|
|
188
|
+
);
|
|
189
|
+
render(
|
|
190
|
+
<ControlPanel schema={schema} config={{ showGrid: true, snap: false }} setConfig={setConfig} />,
|
|
191
|
+
);
|
|
192
|
+
expect(screen.getByRole('checkbox', { name: 'Show grid' })).toBeInTheDocument();
|
|
193
|
+
const sw = screen.getByRole('switch', { name: 'Snap' });
|
|
194
|
+
expect(sw).toBeInTheDocument();
|
|
195
|
+
fireEvent.click(sw);
|
|
196
|
+
expect(setConfig).toHaveBeenCalledWith('snap', true);
|
|
197
|
+
});
|
|
198
|
+
|
|
177
199
|
it('picks a slider for a bounded number and an input for an open one', () => {
|
|
178
200
|
const schema = resolveConfigSchema(
|
|
179
201
|
f.schema({ a: f.number(5).range(0, 10), b: f.number(5) }),
|
|
@@ -460,6 +482,64 @@ describe('<ControlPanel> format', () => {
|
|
|
460
482
|
});
|
|
461
483
|
});
|
|
462
484
|
|
|
485
|
+
describe('<ControlPanel> group descriptions', () => {
|
|
486
|
+
it('draws a named group’s describe() above its rows', () => {
|
|
487
|
+
const schema = resolveConfigSchema(
|
|
488
|
+
f.schema({
|
|
489
|
+
grid: f
|
|
490
|
+
.group({ cellSize: f.number(20) })
|
|
491
|
+
.label('Grid')
|
|
492
|
+
.describe('How the background lattice is spaced.'),
|
|
493
|
+
}),
|
|
494
|
+
[],
|
|
495
|
+
);
|
|
496
|
+
render(<ControlPanel schema={schema} config={{ grid: { cellSize: 20 } }} setConfig={vi.fn()} />);
|
|
497
|
+
const note = screen.getByText('How the background lattice is spaced.');
|
|
498
|
+
const field = screen.getByLabelText('Cell size');
|
|
499
|
+
expect(note.compareDocumentPosition(field) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
describe('<ControlPanel> section layout', () => {
|
|
504
|
+
const schema = resolveConfigSchema(
|
|
505
|
+
f.schema({
|
|
506
|
+
seed: f.number(0).section('Advanced'),
|
|
507
|
+
mode: f
|
|
508
|
+
.enum('a', [
|
|
509
|
+
{ value: 'a', label: 'A' },
|
|
510
|
+
{ value: 'b', label: 'B' },
|
|
511
|
+
])
|
|
512
|
+
.label('Mode')
|
|
513
|
+
.section('Globals', { layout: 'inline', pack: 'one-up' }),
|
|
514
|
+
font: f
|
|
515
|
+
.enum('x', [{ value: 'x', label: 'X' }])
|
|
516
|
+
.label('Font')
|
|
517
|
+
.section('Globals'),
|
|
518
|
+
}),
|
|
519
|
+
[],
|
|
520
|
+
);
|
|
521
|
+
const config = { seed: 0, mode: 'a', font: 'x' };
|
|
522
|
+
// The row class comes through the CSS-module proxy, so this proves which layout the row asked for, not a rule.
|
|
523
|
+
const rowOf = (label: string) => screen.getByText(label).closest('label') as HTMLElement;
|
|
524
|
+
|
|
525
|
+
it('lays out every row in a section the way the section asks, and leaves other sections alone', () => {
|
|
526
|
+
render(<ControlPanel schema={schema} config={config} setConfig={vi.fn()} />);
|
|
527
|
+
expect(rowOf('Mode').className).toMatch(/rowInline/);
|
|
528
|
+
expect(rowOf('Font').className).toMatch(/rowInline/);
|
|
529
|
+
expect(rowOf('Seed').className).not.toMatch(/rowInline/);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('carries a section’s layout into its resolved spec', () => {
|
|
533
|
+
expect(schema.sections.find((section) => section.label === 'Globals')).toMatchObject({
|
|
534
|
+
layout: 'inline',
|
|
535
|
+
pack: 'one-up',
|
|
536
|
+
});
|
|
537
|
+
expect(schema.sections.find((section) => section.label === 'Advanced')).not.toHaveProperty(
|
|
538
|
+
'layout',
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
463
543
|
describe('<ControlPanel> collapsible sections', () => {
|
|
464
544
|
const schema = resolveConfigSchema(
|
|
465
545
|
f.schema({ showGrid: f.boolean(true), seed: f.number(0).section('Advanced') }),
|
|
@@ -611,3 +691,376 @@ describe('<ControlPanel> nested groups', () => {
|
|
|
611
691
|
expect(screen.getByText('size:44')).toBeInTheDocument();
|
|
612
692
|
});
|
|
613
693
|
});
|
|
694
|
+
|
|
695
|
+
describe('<ControlPanel> auto', () => {
|
|
696
|
+
it('renders a label toggle whether or not it was given a set of auto paths', () => {
|
|
697
|
+
render(
|
|
698
|
+
<ControlPanel
|
|
699
|
+
schema={resolveConfigSchema(f.schema({ gap: f.number(12).range(0, 48) }), [])}
|
|
700
|
+
config={{ gap: 12 }}
|
|
701
|
+
setConfig={() => {}}
|
|
702
|
+
/>,
|
|
703
|
+
);
|
|
704
|
+
expect(screen.getByRole('button', { name: 'Pin Gap' })).toBeInTheDocument();
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it('hands a custom renderer the auto state and a working toggle', async () => {
|
|
708
|
+
const setConfig = vi.fn();
|
|
709
|
+
render(
|
|
710
|
+
<ControlPanel
|
|
711
|
+
schema={resolveConfigSchema(f.schema({ gap: f.number(12).range(0, 48) }), [])}
|
|
712
|
+
config={{ gap: 12 }}
|
|
713
|
+
auto={new Set(['gap'])}
|
|
714
|
+
setConfig={setConfig}
|
|
715
|
+
renderers={{
|
|
716
|
+
gap: (ctx) => (
|
|
717
|
+
<button type="button" onClick={() => ctx.setAuto(false)}>
|
|
718
|
+
{ctx.auto ? 'is auto' : 'is pinned'}
|
|
719
|
+
</button>
|
|
720
|
+
),
|
|
721
|
+
}}
|
|
722
|
+
/>,
|
|
723
|
+
);
|
|
724
|
+
const control = screen.getByRole('button', { name: 'is auto' });
|
|
725
|
+
await userEvent.click(control);
|
|
726
|
+
expect(setConfig).toHaveBeenCalledWith('gap', 12);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it('reads out the bare word, resolver or not', () => {
|
|
730
|
+
render(
|
|
731
|
+
<ControlPanel
|
|
732
|
+
schema={resolveConfigSchema(
|
|
733
|
+
f.schema({
|
|
734
|
+
gap: f
|
|
735
|
+
.number(12)
|
|
736
|
+
.auto(() => 18)
|
|
737
|
+
.range(0, 48),
|
|
738
|
+
}),
|
|
739
|
+
[],
|
|
740
|
+
)}
|
|
741
|
+
config={{ gap: 12 }}
|
|
742
|
+
auto={new Set(['gap'])}
|
|
743
|
+
setConfig={() => {}}
|
|
744
|
+
/>,
|
|
745
|
+
);
|
|
746
|
+
expect(screen.getByText('auto')).toBeInTheDocument();
|
|
747
|
+
expect(screen.queryByText(/auto · /)).toBeNull();
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('writes the sentinel when the dot turns a row auto', async () => {
|
|
751
|
+
const setConfig = vi.fn();
|
|
752
|
+
render(
|
|
753
|
+
<ControlPanel
|
|
754
|
+
schema={resolveConfigSchema(f.schema({ gap: f.number(12).range(0, 48) }), [])}
|
|
755
|
+
config={{ gap: 12 }}
|
|
756
|
+
auto={new Set()}
|
|
757
|
+
setConfig={setConfig}
|
|
758
|
+
/>,
|
|
759
|
+
);
|
|
760
|
+
await userEvent.click(screen.getByRole('button', { name: /Gap/ }));
|
|
761
|
+
expect(setConfig).toHaveBeenCalledWith('gap', auto);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it('keeps the sentinel out of a config it is not given an auto set for', async () => {
|
|
765
|
+
const setConfig = vi.fn();
|
|
766
|
+
render(
|
|
767
|
+
<ControlPanel
|
|
768
|
+
schema={resolveConfigSchema(f.schema({ gap: f.number(12).range(0, 48) }), [])}
|
|
769
|
+
config={{ gap: 12 }}
|
|
770
|
+
setConfig={setConfig}
|
|
771
|
+
/>,
|
|
772
|
+
);
|
|
773
|
+
await userEvent.click(screen.getByRole('button', { name: /Gap/ }));
|
|
774
|
+
expect(setConfig).not.toHaveBeenCalled();
|
|
775
|
+
expect(screen.getByText('auto')).toBeInTheDocument();
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('writes the pinned value back when the dot un-autos a row', async () => {
|
|
779
|
+
const setConfig = vi.fn();
|
|
780
|
+
render(
|
|
781
|
+
<ControlPanel
|
|
782
|
+
schema={resolveConfigSchema(f.schema({ gap: f.number(12).range(0, 48) }), [])}
|
|
783
|
+
config={{ gap: 24 }}
|
|
784
|
+
auto={new Set(['gap'])}
|
|
785
|
+
setConfig={setConfig}
|
|
786
|
+
/>,
|
|
787
|
+
);
|
|
788
|
+
await userEvent.click(screen.getByRole('button', { name: /Gap/ }));
|
|
789
|
+
expect(setConfig).toHaveBeenCalledWith('gap', 24);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it('gives a manual row no dot', () => {
|
|
793
|
+
render(
|
|
794
|
+
<ControlPanel
|
|
795
|
+
schema={resolveConfigSchema(f.schema({ seed: f.number(1).manual().range(0, 8) }), [])}
|
|
796
|
+
config={{ seed: 1 }}
|
|
797
|
+
auto={new Set()}
|
|
798
|
+
setConfig={() => {}}
|
|
799
|
+
/>,
|
|
800
|
+
);
|
|
801
|
+
expect(screen.queryByRole('button', { name: /Seed/ })).toBeNull();
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it('draws what the instrument reads when a resolver depends on another auto path', () => {
|
|
805
|
+
// `width` is auto with no resolver, so the instrument sees `undefined` and
|
|
806
|
+
// `gap` resolves from that. Re-running gap's resolver against the raw
|
|
807
|
+
// config would instead read the pinned 640 and draw a number nobody got.
|
|
808
|
+
const { container } = render(
|
|
809
|
+
<ControlPanel
|
|
810
|
+
schema={resolveConfigSchema(
|
|
811
|
+
f.schema({
|
|
812
|
+
width: f.number(640),
|
|
813
|
+
gap: f
|
|
814
|
+
.number(12)
|
|
815
|
+
.range(0, 48)
|
|
816
|
+
.auto((c) => (c.width === undefined ? 5 : (c.width as number) / 64)),
|
|
817
|
+
}),
|
|
818
|
+
[],
|
|
819
|
+
)}
|
|
820
|
+
config={{ width: 640, gap: 12 }}
|
|
821
|
+
auto={new Set(['width', 'gap'])}
|
|
822
|
+
setConfig={() => {}}
|
|
823
|
+
/>,
|
|
824
|
+
);
|
|
825
|
+
expect(container.querySelector('input[type=range]')).toHaveValue('5');
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('draws an auto control at the resolved value, not the value underneath it', () => {
|
|
829
|
+
const { container } = render(
|
|
830
|
+
<ControlPanel
|
|
831
|
+
schema={resolveConfigSchema(
|
|
832
|
+
f.schema({
|
|
833
|
+
gap: f
|
|
834
|
+
.number(12)
|
|
835
|
+
.range(0, 48)
|
|
836
|
+
.auto(() => 18),
|
|
837
|
+
}),
|
|
838
|
+
[],
|
|
839
|
+
)}
|
|
840
|
+
config={{ gap: 12 }}
|
|
841
|
+
auto={new Set(['gap'])}
|
|
842
|
+
setConfig={() => {}}
|
|
843
|
+
/>,
|
|
844
|
+
);
|
|
845
|
+
expect(container.querySelector('input[type=range]')).toHaveValue('18');
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it('pins at the value it was drawing, not the stale one underneath', async () => {
|
|
849
|
+
const setConfig = vi.fn();
|
|
850
|
+
render(
|
|
851
|
+
<ControlPanel
|
|
852
|
+
schema={resolveConfigSchema(
|
|
853
|
+
f.schema({
|
|
854
|
+
gap: f
|
|
855
|
+
.number(12)
|
|
856
|
+
.range(0, 48)
|
|
857
|
+
.auto(() => 18),
|
|
858
|
+
}),
|
|
859
|
+
[],
|
|
860
|
+
)}
|
|
861
|
+
config={{ gap: 12 }}
|
|
862
|
+
auto={new Set(['gap'])}
|
|
863
|
+
setConfig={setConfig}
|
|
864
|
+
/>,
|
|
865
|
+
);
|
|
866
|
+
await userEvent.click(screen.getByRole('button', { name: 'Pin Gap' }));
|
|
867
|
+
expect(setConfig).toHaveBeenCalledWith('gap', 18);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
// Every kind, because the panel wraps some rows before rendering them and a
|
|
871
|
+
// wrapper that drops the auto props leaves that kind silently pinned.
|
|
872
|
+
it.each([
|
|
873
|
+
['number', f.number(3).range(0, 8)],
|
|
874
|
+
['slider', f.number(3).range(0, 8).slider()],
|
|
875
|
+
['boolean', f.boolean(true)],
|
|
876
|
+
['enum', f.enum('a', ['a', 'b'])],
|
|
877
|
+
['radio', f.enum('a', ['a', 'b']).radio()],
|
|
878
|
+
['string', f.string('hi')],
|
|
879
|
+
['color', f.color('#3a86ff')],
|
|
880
|
+
])('marks a %s row auto and gives it a label toggle', (_kind, node) => {
|
|
881
|
+
const { container } = render(
|
|
882
|
+
<ControlPanel
|
|
883
|
+
schema={resolveConfigSchema(f.schema({ thing: node.label('Thing') }), [])}
|
|
884
|
+
config={{ thing: node.default }}
|
|
885
|
+
auto={new Set(['thing'])}
|
|
886
|
+
setConfig={() => {}}
|
|
887
|
+
/>,
|
|
888
|
+
);
|
|
889
|
+
expect(container.querySelector('[class*=rowAuto]')).not.toBeNull();
|
|
890
|
+
expect(screen.getByRole('button', { name: 'Pin Thing' })).toBeInTheDocument();
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
describe('<ControlPanel> pair', () => {
|
|
895
|
+
const paired = () =>
|
|
896
|
+
resolveConfigSchema(
|
|
897
|
+
f.schema({
|
|
898
|
+
x: f.number(10).label('X').pair('Offset'),
|
|
899
|
+
y: f.number(20).label('Y').pair('Offset'),
|
|
900
|
+
scale: f.number(1).label('Scale'),
|
|
901
|
+
}),
|
|
902
|
+
[],
|
|
903
|
+
);
|
|
904
|
+
const config = { x: 10, y: 20, scale: 1 };
|
|
905
|
+
|
|
906
|
+
it('draws two leaves sharing a pair id as one row named by the pair', () => {
|
|
907
|
+
render(<ControlPanel schema={paired()} config={config} setConfig={vi.fn()} />);
|
|
908
|
+
const row = screen.getByText('Offset').closest('div') as HTMLElement;
|
|
909
|
+
expect(within(row).getByLabelText('X')).toHaveValue(10);
|
|
910
|
+
expect(within(row).getByLabelText('Y')).toHaveValue(20);
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
it('captions each cell of a pair with its own name, so a paired switch is not left unnamed', () => {
|
|
914
|
+
render(<ControlPanel schema={paired()} config={config} setConfig={vi.fn()} />);
|
|
915
|
+
const row = screen.getByText('Offset').closest('div') as HTMLElement;
|
|
916
|
+
expect(within(row).getByText('X')).toHaveClass('lk-pair-label');
|
|
917
|
+
expect(within(row).getByText('Y')).toHaveClass('lk-pair-label');
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it('writes each cell of a paired row to its own path', () => {
|
|
921
|
+
const setConfig = vi.fn();
|
|
922
|
+
render(<ControlPanel schema={paired()} config={config} setConfig={setConfig} />);
|
|
923
|
+
fireEvent.change(screen.getByLabelText('Y'), { target: { value: '30' } });
|
|
924
|
+
expect(setConfig).toHaveBeenCalledWith('y', 30);
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
it('keeps a segmented leaf segmented when it shares a row, and gives that row the width', () => {
|
|
928
|
+
const schema = resolveConfigSchema(
|
|
929
|
+
f.schema({
|
|
930
|
+
camera: f.enum<'2d' | '3d'>('3d', ['2d', '3d']).radio().label('').pair('Camera'),
|
|
931
|
+
hold: f.boolean(true).toggle().label('Hold front').pair('Camera'),
|
|
932
|
+
}),
|
|
933
|
+
[],
|
|
934
|
+
);
|
|
935
|
+
render(
|
|
936
|
+
<ControlPanel schema={schema} config={{ camera: '3d', hold: true }} setConfig={vi.fn()} />,
|
|
937
|
+
);
|
|
938
|
+
const row = screen.getByText('Camera').closest('div') as HTMLElement;
|
|
939
|
+
expect(within(row).getByRole('radio', { name: '2d' })).toBeInTheDocument();
|
|
940
|
+
expect(within(row).queryByRole('combobox')).toBeNull();
|
|
941
|
+
expect(row.className).toMatch(/span/);
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it('leaves an unpaired sibling on its own row', () => {
|
|
945
|
+
render(<ControlPanel schema={paired()} config={config} setConfig={vi.fn()} />);
|
|
946
|
+
const row = screen.getByText('Offset').closest('div') as HTMLElement;
|
|
947
|
+
expect(within(row).queryByLabelText('Scale')).toBeNull();
|
|
948
|
+
expect(screen.getByLabelText('Scale')).toHaveValue(1);
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
describe('<ControlPanel> unit', () => {
|
|
953
|
+
const degrees = {
|
|
954
|
+
toDisplay: (stored: number) => (stored * 180) / Math.PI,
|
|
955
|
+
fromDisplay: (shown: number) => (shown * Math.PI) / 180,
|
|
956
|
+
suffix: '°',
|
|
957
|
+
};
|
|
958
|
+
const angle = (node: ReturnType<typeof f.number>, stored: number, setConfig = vi.fn()) => {
|
|
959
|
+
render(
|
|
960
|
+
<ControlPanel
|
|
961
|
+
schema={resolveConfigSchema(f.schema({ angle: node.label('Angle') }), [])}
|
|
962
|
+
config={{ angle: stored }}
|
|
963
|
+
setConfig={setConfig}
|
|
964
|
+
/>,
|
|
965
|
+
);
|
|
966
|
+
return setConfig;
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
it('shows a radians-backed number in its display unit', () => {
|
|
970
|
+
angle(f.number(0).input().unit(degrees), Math.PI / 2);
|
|
971
|
+
expect((screen.getByLabelText('Angle') as HTMLInputElement).value).toBe('90');
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it('stores a typed display value back in the canonical unit', () => {
|
|
975
|
+
const setConfig = angle(f.number(0).input().unit(degrees), Math.PI / 2);
|
|
976
|
+
fireEvent.change(screen.getByLabelText('Angle'), { target: { value: '45' } });
|
|
977
|
+
expect(setConfig.mock.calls[0][0]).toBe('angle');
|
|
978
|
+
expect(setConfig.mock.calls[0][1] as number).toBeCloseTo(Math.PI / 4, 10);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
it('round-trips a value edited away and back', () => {
|
|
982
|
+
// Live config, because the corruption this guards against only shows on
|
|
983
|
+
// the second edit: what the field stores has to read back as what it showed.
|
|
984
|
+
function Live() {
|
|
985
|
+
const [config, setConfig] = useState<Record<string, unknown>>({ angle: Math.PI / 2 });
|
|
986
|
+
return (
|
|
987
|
+
<ControlPanel
|
|
988
|
+
schema={resolveConfigSchema(
|
|
989
|
+
f.schema({ angle: f.number(0).input().unit(degrees).label('Angle') }),
|
|
990
|
+
[],
|
|
991
|
+
)}
|
|
992
|
+
config={config}
|
|
993
|
+
setConfig={(path, value) => setConfig((prev) => ({ ...prev, [path]: value }))}
|
|
994
|
+
/>
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
render(<Live />);
|
|
998
|
+
const field = () => screen.getByLabelText('Angle') as HTMLInputElement;
|
|
999
|
+
expect(field().value).toBe('90');
|
|
1000
|
+
fireEvent.change(field(), { target: { value: '45' } });
|
|
1001
|
+
expect(field().value).toBe('45');
|
|
1002
|
+
fireEvent.change(field(), { target: { value: '90' } });
|
|
1003
|
+
expect(field().value).toBe('90');
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
it('converts a slider’s bounds into the display unit', () => {
|
|
1007
|
+
angle(f.number(0).range(0, Math.PI).unit(degrees), Math.PI / 2);
|
|
1008
|
+
const slider = screen.getByRole('slider') as HTMLInputElement;
|
|
1009
|
+
expect(slider.min).toBe('0');
|
|
1010
|
+
expect(slider.max).toBe('180');
|
|
1011
|
+
expect(slider.value).toBe('90');
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('clamps a typed value against the bounds in display space', () => {
|
|
1015
|
+
// 100 is inside 0..180 degrees and outside 0..π radians, so clamping
|
|
1016
|
+
// against the stored bounds would cut a legal angle down to π.
|
|
1017
|
+
const setConfig = angle(f.number(0).range(0, Math.PI).input().unit(degrees), Math.PI / 2);
|
|
1018
|
+
fireEvent.change(screen.getByLabelText('Angle'), { target: { value: '100' } });
|
|
1019
|
+
expect(setConfig.mock.calls[0][1] as number).toBeCloseTo((100 * Math.PI) / 180, 10);
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
describe('<ControlPanel> color alpha', () => {
|
|
1024
|
+
const panel = (setConfig = vi.fn(), stored = '#336699cc') => {
|
|
1025
|
+
render(
|
|
1026
|
+
<ControlPanel
|
|
1027
|
+
schema={resolveConfigSchema(
|
|
1028
|
+
f.schema({ tint: f.color('#000000ff').alpha().label('Tint') }),
|
|
1029
|
+
[],
|
|
1030
|
+
)}
|
|
1031
|
+
config={{ tint: stored }}
|
|
1032
|
+
setConfig={setConfig}
|
|
1033
|
+
/>,
|
|
1034
|
+
);
|
|
1035
|
+
return setConfig;
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
it('splits the stored hex into a swatch and an alpha track', () => {
|
|
1039
|
+
panel();
|
|
1040
|
+
expect((screen.getByLabelText('Tint') as HTMLInputElement).value).toBe('#336699');
|
|
1041
|
+
expect((screen.getByLabelText('Tint opacity') as HTMLInputElement).value).toBe('0.8');
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
it('keeps the stored alpha when the swatch moves', () => {
|
|
1045
|
+
const setConfig = panel();
|
|
1046
|
+
fireEvent.change(screen.getByLabelText('Tint'), { target: { value: '#00ff00' } });
|
|
1047
|
+
expect(setConfig).toHaveBeenCalledWith('tint', '#00ff00cc');
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
it('keeps the stored color when the alpha track moves', () => {
|
|
1051
|
+
const setConfig = panel();
|
|
1052
|
+
fireEvent.change(screen.getByLabelText('Tint opacity'), { target: { value: '0.5' } });
|
|
1053
|
+
expect(setConfig).toHaveBeenCalledWith('tint', '#33669980');
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
it('leaves a color leaf without alpha as a bare swatch', () => {
|
|
1057
|
+
render(
|
|
1058
|
+
<ControlPanel
|
|
1059
|
+
schema={resolveConfigSchema(f.schema({ tint: f.color('#336699').label('Tint') }), [])}
|
|
1060
|
+
config={{ tint: '#336699' }}
|
|
1061
|
+
setConfig={vi.fn()}
|
|
1062
|
+
/>,
|
|
1063
|
+
);
|
|
1064
|
+
expect(screen.queryByLabelText('Tint opacity')).toBeNull();
|
|
1065
|
+
});
|
|
1066
|
+
});
|