@weasel-js/labkit 1.4.4 → 1.5.0
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 +13 -2
- package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CqkxkFJW.d.ts} +1 -1
- package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +116 -0
- package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-8R6UmlvN.d.ts} +253 -31
- package/dist/_dts/index-B8uu2ba7.d.ts +237 -0
- package/dist/_dts/{index-Blv9uQzu.d.ts → index-retnAut7.d.ts} +18 -10
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-Ca2LCnPq.d.ts} +65 -2
- package/dist/_dts/usePanZoom-nS798rOF.d.ts +160 -0
- package/dist/_dts/useTrialState-jcx_TdU6.d.ts +212 -0
- package/dist/canvas/index.d.ts +17 -128
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +4 -92
- package/dist/chrome/index.js +9 -5
- package/dist/chunk-67SJLMC7.js +704 -0
- package/dist/chunk-67SJLMC7.js.map +1 -0
- package/dist/chunk-6ZDGOZQV.js +310 -0
- package/dist/chunk-6ZDGOZQV.js.map +1 -0
- package/dist/{chunk-MOM3GOVY.js → chunk-EKIICY6X.js} +111 -25
- package/dist/chunk-EKIICY6X.js.map +1 -0
- package/dist/{chunk-CTRKTLYZ.js → chunk-H6ZAOWNE.js} +11 -11
- package/dist/chunk-H6ZAOWNE.js.map +1 -0
- package/dist/{chunk-SMHP6XZ4.js → chunk-KJALCDWE.js} +9 -7
- package/dist/chunk-KJALCDWE.js.map +1 -0
- package/dist/{chunk-BKFVHKJH.js → chunk-MUKOW3TC.js} +3 -3
- package/dist/{chunk-BKFVHKJH.js.map → chunk-MUKOW3TC.js.map} +1 -1
- package/dist/chunk-NDRYLVOW.js +15 -0
- package/dist/chunk-NDRYLVOW.js.map +1 -0
- package/dist/{chunk-64ZCN3DA.js → chunk-NS64DXMN.js} +226 -159
- package/dist/chunk-NS64DXMN.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-PGETSEDK.js} +135 -13
- package/dist/chunk-PGETSEDK.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-QLSV2N3G.js} +32 -37
- package/dist/chunk-QLSV2N3G.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-S2SHZRD7.js +7681 -0
- package/dist/chunk-S2SHZRD7.js.map +1 -0
- package/dist/{chunk-U3IYHIAE.js → chunk-TQBBYTAF.js} +19 -82
- package/dist/chunk-TQBBYTAF.js.map +1 -0
- package/dist/chunk-UU3NJO6N.js +72 -0
- package/dist/chunk-UU3NJO6N.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ZPH5WTWF.js} +21 -15
- package/dist/chunk-ZPH5WTWF.js.map +1 -0
- package/dist/config/index.d.ts +232 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +2 -3
- package/dist/controls/index.js +3 -2
- package/dist/dragdrop/index.d.ts +2 -4
- package/dist/index.d.ts +110 -381
- package/dist/index.js +691 -817
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +3 -5
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +6 -8
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +164 -122
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +53 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +12 -7
- package/dist/state/index.js +16 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +84 -50
- package/dist/surface/index.d.ts +44 -7
- package/dist/surface/index.js +2 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +2 -4
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +53 -5
- package/src/annotations/Annotations.overlay.test.tsx +85 -6
- package/src/annotations/drawOne.test.ts +1 -1
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/paint.test.ts +2 -2
- package/src/annotations/paint.ts +1 -1
- package/src/annotations/preload.ts +6 -0
- package/src/annotations/store.test.ts +18 -0
- package/src/annotations/store.ts +5 -4
- package/src/annotations/svgNodes.test.ts +2 -2
- package/src/annotations/toolMap.ts +3 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- package/src/canvas/CanvasStack.test.tsx +25 -0
- package/src/canvas/CanvasStack.tsx +31 -4
- package/src/canvas/Stage.less +18 -0
- package/src/canvas/Stage.test.tsx +186 -0
- package/src/canvas/Stage.tsx +134 -0
- package/src/canvas/camera.test.ts +17 -2
- package/src/canvas/camera.ts +9 -9
- package/src/canvas/index.ts +4 -0
- package/src/canvas/usePanZoom.ts +10 -5
- package/src/chrome/ChromeRegions.stories.tsx +0 -3
- package/src/chrome/LabChrome.tsx +114 -0
- package/src/chrome/builtins.tsx +20 -31
- package/src/chrome/index.ts +15 -0
- package/src/chrome/labRegions.test.tsx +208 -0
- package/src/chrome/labTypes.ts +50 -0
- package/src/chrome/merge.ts +7 -9
- package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
- package/src/chrome/regions/PaletteRegion.tsx +21 -9
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +30 -16
- package/src/chrome/types.ts +47 -20
- package/src/config/builder.test.ts +4 -0
- package/src/config/builder.ts +16 -10
- package/src/config/declarationEmit.fixture.ts +11 -0
- package/src/config/declarationEmit.test.ts +51 -0
- package/src/config/entry.test.ts +46 -0
- package/src/config/index.ts +13 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/types.ts +3 -1
- package/src/controls/ControlPanel.test.tsx +16 -0
- package/src/controls/ControlPanel.tsx +5 -2
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +38 -5
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +11 -0
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +0 -2
- package/src/lab/Lab.surface.test.tsx +66 -3
- package/src/lab/Lab.test.tsx +91 -17
- package/src/lab/Lab.tsx +337 -124
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +0 -3
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -13
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabShell.less +49 -12
- package/src/lab/LabSwitcher.less +8 -1
- package/src/lab/Workspace.surface.test.tsx +2 -1
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/loupe/AGENTS.md +1 -1
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +1 -1
- package/src/loupe/useLoupe.ts +1 -1
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +13 -1
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +108 -0
- package/src/primitives/Split.tsx +159 -0
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -0
- package/src/state/Persistence.stories.tsx +75 -0
- package/src/state/Persistence.tsx +33 -0
- package/src/state/SingletonExperiment.test.tsx +47 -53
- package/src/state/SingletonExperiment.tsx +24 -13
- package/src/state/adapterContract.ts +116 -0
- package/src/state/adapters.test.ts +113 -61
- package/src/state/adapters.ts +328 -69
- package/src/state/document.test.ts +71 -96
- package/src/state/document.ts +66 -52
- package/src/state/helpers.test.ts +1 -1
- package/src/state/index.ts +8 -0
- package/src/state/labRecords.test.ts +105 -0
- package/src/state/labRecords.ts +160 -0
- package/src/state/openLabStore.test.ts +658 -0
- package/src/state/openLabStore.ts +403 -0
- package/src/state/records.test.ts +234 -0
- package/src/state/records.ts +219 -0
- package/src/state/store.test.ts +104 -635
- package/src/state/store.ts +116 -157
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +39 -15
- package/src/state/useOpenOnce.ts +68 -0
- package/src/state/usePersistedState.test.tsx +125 -0
- package/src/state/usePersistedState.ts +66 -0
- package/src/state/useTrialState.test.tsx +2 -3
- package/src/state/view.test.ts +49 -15
- package/src/state/view.ts +18 -1
- package/src/styles.less +2 -0
- package/src/surface/AGENTS.md +39 -7
- package/src/surface/SurfaceContext.ts +26 -3
- package/src/surface/index.ts +2 -0
- package/src/surface/useSurfaceTile.test.tsx +2 -1
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +175 -0
- package/src/surface/useTiledSurface.ts +54 -5
- package/src/theme/Interstellar.stories.tsx +7 -7
- package/src/theme/interstellar.test.ts +1 -1
- package/src/theme/interstellar.tokens.json +2 -2
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +47 -4
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +1 -26
- package/src/trial/Trial.stories.tsx +1 -2
- package/src/trial/Trial.test.tsx +28 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +118 -38
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +7 -4
- package/src/trial/trialOps.ts +5 -3
- package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
- package/dist/chunk-2P6PP5N4.js +0 -553
- package/dist/chunk-2P6PP5N4.js.map +0 -1
- package/dist/chunk-64ZCN3DA.js.map +0 -1
- package/dist/chunk-CTRKTLYZ.js.map +0 -1
- package/dist/chunk-FQMJUVHJ.js.map +0 -1
- package/dist/chunk-I6JCVE24.js.map +0 -1
- package/dist/chunk-MOM3GOVY.js.map +0 -1
- package/dist/chunk-SMHP6XZ4.js.map +0 -1
- package/dist/chunk-TJ7QY3OC.js.map +0 -1
- package/dist/chunk-U3IYHIAE.js.map +0 -1
- package/dist/chunk-W3ECWC2K.js +0 -7335
- package/dist/chunk-W3ECWC2K.js.map +0 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { type ReactNode, useLayoutEffect, useState } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
|
5
|
+
import type { LabContribution } from '../chrome/labTypes';
|
|
6
|
+
import type { TrialContribution } from '../chrome/types';
|
|
7
|
+
import type { Instrument } from '../instrument/types';
|
|
8
|
+
import { Lab } from './Lab';
|
|
9
|
+
import './LabFit.stories.less';
|
|
10
|
+
|
|
11
|
+
// A lab must fit whatever it is mounted in without scrolling. jsdom cannot see
|
|
12
|
+
// layout, so these run in the storybook vitest project's real browser.
|
|
13
|
+
|
|
14
|
+
const Stub: Instrument = {
|
|
15
|
+
name: 'Stub',
|
|
16
|
+
defaultConfig: () => ({}),
|
|
17
|
+
initialState: () => ({}),
|
|
18
|
+
render: () => <div>stub</div>,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const floater: TrialContribution = {
|
|
22
|
+
id: 'floater',
|
|
23
|
+
region: 'sidebar',
|
|
24
|
+
item: { title: 'Floater', undockAs: 'floating', body: <p>floating panel</p> },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const sidebarTree: LabContribution = {
|
|
28
|
+
id: 'tree',
|
|
29
|
+
region: 'sidebar',
|
|
30
|
+
item: {
|
|
31
|
+
title: 'Stories',
|
|
32
|
+
body: Array.from({ length: 40 }, (_, i) => `Story ${i + 1}`).map((name) => (
|
|
33
|
+
<p key={name}>{name}</p>
|
|
34
|
+
)),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const VIEWPORTS = {
|
|
39
|
+
wide: { name: '1280x800', styles: { width: '1280px', height: '800px' } },
|
|
40
|
+
medium: { name: '800x600', styles: { width: '800px', height: '600px' } },
|
|
41
|
+
narrow: { name: '400x700', styles: { width: '400px', height: '700px' } },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type Host = 'reset' | 'wrapped' | 'framed';
|
|
45
|
+
|
|
46
|
+
// Storybook wraps every story in divs of its own, so a fixture that needs a
|
|
47
|
+
// known ancestor chain builds that chain directly on <body> and portals into it.
|
|
48
|
+
function BodyHost({ host, children }: { host: Host; children: ReactNode }) {
|
|
49
|
+
const [mount, setMount] = useState<HTMLElement | null>(null);
|
|
50
|
+
useLayoutEffect(() => {
|
|
51
|
+
const outer = document.createElement('div');
|
|
52
|
+
outer.className = `lk-fit-host lk-fit-host--${host}`;
|
|
53
|
+
let target = outer;
|
|
54
|
+
if (host === 'wrapped') {
|
|
55
|
+
target = document.createElement('div');
|
|
56
|
+
outer.appendChild(target);
|
|
57
|
+
}
|
|
58
|
+
if (host === 'framed') {
|
|
59
|
+
// apps/site's shape: page content above a fixed-height frame.
|
|
60
|
+
const heading = document.createElement('div');
|
|
61
|
+
heading.className = 'lk-fit-heading';
|
|
62
|
+
target = document.createElement('div');
|
|
63
|
+
target.className = 'lk-fit-frame';
|
|
64
|
+
outer.append(heading, target);
|
|
65
|
+
}
|
|
66
|
+
document.body.appendChild(outer);
|
|
67
|
+
setMount(target);
|
|
68
|
+
return () => outer.remove();
|
|
69
|
+
}, [host]);
|
|
70
|
+
return mount ? createPortal(children, mount) : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type Fixture = 'fullscreen' | 'floating' | 'wrapped' | 'framed' | 'sidebar';
|
|
74
|
+
|
|
75
|
+
const HOST: Record<Fixture, Host> = {
|
|
76
|
+
fullscreen: 'reset',
|
|
77
|
+
sidebar: 'reset',
|
|
78
|
+
floating: 'reset',
|
|
79
|
+
wrapped: 'wrapped',
|
|
80
|
+
framed: 'framed',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function scrolls(el: Element): string[] {
|
|
84
|
+
const out: string[] = [];
|
|
85
|
+
if (el.scrollHeight > el.clientHeight)
|
|
86
|
+
out.push(`scrollHeight ${el.scrollHeight} > ${el.clientHeight}`);
|
|
87
|
+
if (el.scrollWidth > el.clientWidth)
|
|
88
|
+
out.push(`scrollWidth ${el.scrollWidth} > ${el.clientWidth}`);
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function fitProblems(fixture: Fixture, viewport: keyof typeof VIEWPORTS): string[] {
|
|
93
|
+
const problems: string[] = [];
|
|
94
|
+
const want = VIEWPORTS[viewport].styles;
|
|
95
|
+
const got = `${window.innerWidth}px x ${window.innerHeight}px`;
|
|
96
|
+
if (got !== `${want.width} x ${want.height}`) problems.push(`viewport is ${got}`);
|
|
97
|
+
|
|
98
|
+
// A box that spills out of the lab is overflow even where the page has room
|
|
99
|
+
// to absorb it, which is how a 100vh shell hid inside an embedded lab.
|
|
100
|
+
const labBox = document.querySelector('.lk-lab')?.getBoundingClientRect();
|
|
101
|
+
const shellBox = document.querySelector('.lk-shell')?.getBoundingClientRect();
|
|
102
|
+
if (!labBox || !shellBox) return [...problems, 'no .lk-lab or .lk-shell'];
|
|
103
|
+
if (shellBox.bottom > labBox.bottom + 1 || shellBox.right > labBox.right + 1) {
|
|
104
|
+
problems.push(
|
|
105
|
+
`.lk-shell ${shellBox.bottom}/${shellBox.right} spills out of .lk-lab ${labBox.bottom}/${labBox.right}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const doc = document.scrollingElement ?? document.documentElement;
|
|
110
|
+
for (const p of scrolls(doc)) problems.push(`document: ${p}`);
|
|
111
|
+
|
|
112
|
+
const body = document.querySelector('.lk-shell-body');
|
|
113
|
+
if (!body) return ['no .lk-shell-body'];
|
|
114
|
+
for (const p of scrolls(body)) problems.push(`.lk-shell-body: ${p}`);
|
|
115
|
+
|
|
116
|
+
const tiles = [...document.querySelectorAll('.lk-trial-tile')];
|
|
117
|
+
if (tiles.length === 0) problems.push('no .lk-trial-tile');
|
|
118
|
+
tiles.forEach((t, i) => {
|
|
119
|
+
const h = t.getBoundingClientRect().height;
|
|
120
|
+
if (!(h > 0)) problems.push(`tile ${i}: height ${h}`);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (fixture === 'framed') {
|
|
124
|
+
const frame = document.querySelector('.lk-fit-frame');
|
|
125
|
+
const lab = document.querySelector('.lk-lab');
|
|
126
|
+
const f = frame?.getBoundingClientRect();
|
|
127
|
+
const l = lab?.getBoundingClientRect();
|
|
128
|
+
if (!f || !l) return [...problems, 'no frame or lab'];
|
|
129
|
+
if (Math.abs(l.height - f.height) > 1)
|
|
130
|
+
problems.push(`lab height ${l.height} != frame ${f.height}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (fixture === 'floating') {
|
|
134
|
+
const panel = document.querySelector('.lk-panel-tile--floating')?.getBoundingClientRect();
|
|
135
|
+
const host = document.querySelector('.lk-workspace-host')?.getBoundingClientRect();
|
|
136
|
+
if (!panel || !host) return [...problems, 'no floating panel or workspace host'];
|
|
137
|
+
if (panel.top < host.top || panel.right > host.right + 1) {
|
|
138
|
+
problems.push(
|
|
139
|
+
`floating panel ${panel.top},${panel.right} outside workspace ${host.top},${host.right}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (fixture === 'sidebar') {
|
|
145
|
+
const side = document.querySelector('.lk-lab__sidebar')?.getBoundingClientRect();
|
|
146
|
+
// The host wraps the grid only once a panel is undocked.
|
|
147
|
+
const work = document
|
|
148
|
+
.querySelector('.lk-lab__main > .lk-workspace-host, .lk-lab__main > .lk-workspace')
|
|
149
|
+
?.getBoundingClientRect();
|
|
150
|
+
if (!side) return [...problems, 'no .lk-lab__sidebar'];
|
|
151
|
+
if (!work) return [...problems, 'no workspace in .lk-lab__main'];
|
|
152
|
+
if (!(side.width > 0)) problems.push(`lab sidebar width ${side.width}`);
|
|
153
|
+
if (!(work.width > 0)) problems.push(`workspace width ${work.width}`);
|
|
154
|
+
if (work.left < side.right - 1) {
|
|
155
|
+
problems.push(`workspace left ${work.left} overlaps sidebar right ${side.right}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return problems;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function fit(fixture: Fixture, viewport: keyof typeof VIEWPORTS): Story {
|
|
162
|
+
return {
|
|
163
|
+
globals: { viewport: { value: viewport, isRotated: false } },
|
|
164
|
+
render: () => (
|
|
165
|
+
<BodyHost host={HOST[fixture]}>
|
|
166
|
+
<Lab
|
|
167
|
+
title={`Fit: ${fixture}`}
|
|
168
|
+
instruments={[Stub]}
|
|
169
|
+
defaultInstrument="Stub"
|
|
170
|
+
chrome={fixture === 'floating' ? [floater] : undefined}
|
|
171
|
+
labChrome={fixture === 'sidebar' ? [sidebarTree] : undefined}
|
|
172
|
+
/>
|
|
173
|
+
</BodyHost>
|
|
174
|
+
),
|
|
175
|
+
play: async () => {
|
|
176
|
+
if (fixture === 'floating') {
|
|
177
|
+
const undock = await within(document.body).findByRole('button', { name: 'Undock Floater' });
|
|
178
|
+
await userEvent.click(undock);
|
|
179
|
+
}
|
|
180
|
+
await waitFor(
|
|
181
|
+
() => {
|
|
182
|
+
const problems = fitProblems(fixture, viewport);
|
|
183
|
+
if (problems.length > 0) throw new Error(problems.join('; '));
|
|
184
|
+
},
|
|
185
|
+
{ timeout: 2000 },
|
|
186
|
+
);
|
|
187
|
+
await expect(fitProblems(fixture, viewport)).toEqual([]);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const meta: Meta<typeof Lab> = {
|
|
193
|
+
title: 'labkit/Lab/Fit',
|
|
194
|
+
component: Lab,
|
|
195
|
+
parameters: { layout: 'fullscreen', viewport: { options: VIEWPORTS } },
|
|
196
|
+
};
|
|
197
|
+
export default meta;
|
|
198
|
+
|
|
199
|
+
type Story = StoryObj<typeof Lab>;
|
|
200
|
+
|
|
201
|
+
export const FullscreenWide = fit('fullscreen', 'wide');
|
|
202
|
+
export const FullscreenMedium = fit('fullscreen', 'medium');
|
|
203
|
+
export const FullscreenNarrow = fit('fullscreen', 'narrow');
|
|
204
|
+
|
|
205
|
+
export const FloatingPanelWide = fit('floating', 'wide');
|
|
206
|
+
export const FloatingPanelMedium = fit('floating', 'medium');
|
|
207
|
+
export const FloatingPanelNarrow = fit('floating', 'narrow');
|
|
208
|
+
|
|
209
|
+
export const WrappedNoResetWide = fit('wrapped', 'wide');
|
|
210
|
+
export const WrappedNoResetMedium = fit('wrapped', 'medium');
|
|
211
|
+
export const WrappedNoResetNarrow = fit('wrapped', 'narrow');
|
|
212
|
+
|
|
213
|
+
export const FramedWide = fit('framed', 'wide');
|
|
214
|
+
export const FramedMedium = fit('framed', 'medium');
|
|
215
|
+
export const FramedNarrow = fit('framed', 'narrow');
|
|
216
|
+
|
|
217
|
+
export const SidebarWide = fit('sidebar', 'wide');
|
|
218
|
+
export const SidebarMedium = fit('sidebar', 'medium');
|
|
219
|
+
export const SidebarNarrow = fit('sidebar', 'narrow');
|
|
@@ -268,7 +268,6 @@ export const AllChrome: Story = {
|
|
|
268
268
|
instruments: [spectrometer, beamProfile, thermalDrift],
|
|
269
269
|
defaultInstrument: 'Spectrometer',
|
|
270
270
|
title: 'Optics bench',
|
|
271
|
-
storage: null,
|
|
272
271
|
mode: 'dark',
|
|
273
272
|
nebula: ['#3b5bdb', '#7048e8', '#0ca678'],
|
|
274
273
|
children: (
|
|
@@ -309,7 +308,6 @@ export const SingleInstrument: Story = {
|
|
|
309
308
|
instruments: [runLog],
|
|
310
309
|
defaultInstrument: 'Run log',
|
|
311
310
|
title: 'Run log',
|
|
312
|
-
storage: null,
|
|
313
311
|
},
|
|
314
312
|
};
|
|
315
313
|
|
|
@@ -321,7 +319,6 @@ export const WithFooter: Story = {
|
|
|
321
319
|
instruments: [spectrometer, beamProfile],
|
|
322
320
|
defaultInstrument: 'Spectrometer',
|
|
323
321
|
title: 'With footer',
|
|
324
|
-
storage: null,
|
|
325
322
|
mode: 'dark',
|
|
326
323
|
footer: <span className="lk-run-tag">Footer — reached through LabProps.footer</span>,
|
|
327
324
|
},
|
|
@@ -13,28 +13,59 @@ const Other: Instrument = { ...Stub, name: 'Other' };
|
|
|
13
13
|
|
|
14
14
|
describe('<LabHeader>', () => {
|
|
15
15
|
it('offers an add-trial button when the lab has one instrument', () => {
|
|
16
|
-
render(<Lab instruments={[Stub]} defaultInstrument="Stub"
|
|
16
|
+
render(<Lab instruments={[Stub]} defaultInstrument="Stub" />);
|
|
17
17
|
expect(screen.getByRole('button', { name: /add trial/i })).toBeInTheDocument();
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
it('offers a
|
|
21
|
-
render(<Lab instruments={[Stub, Other]} defaultInstrument="Stub"
|
|
22
|
-
//
|
|
23
|
-
expect(screen.
|
|
24
|
-
|
|
20
|
+
it('offers a menu instead when the lab has several', () => {
|
|
21
|
+
render(<Lab instruments={[Stub, Other]} defaultInstrument="Stub" />);
|
|
22
|
+
// Which instrument to add is now a choice, so the button opens a list.
|
|
23
|
+
expect(screen.getByRole('button', { name: 'Add trial' })).toHaveAttribute('aria-haspopup');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('adds a trial of the instrument chosen from the menu', async () => {
|
|
27
|
+
const { default: userEvent } = await import('@testing-library/user-event');
|
|
28
|
+
const user = userEvent.setup();
|
|
29
|
+
render(<Lab instruments={[Stub, Other]} defaultInstrument="Stub" />);
|
|
30
|
+
expect(screen.queryAllByLabelText(/^Trial Other$/)).toHaveLength(0);
|
|
31
|
+
await user.click(screen.getByRole('button', { name: 'Add trial' }));
|
|
32
|
+
await user.click(screen.getByRole('menuitem', { name: 'Other' }));
|
|
33
|
+
expect(screen.getAllByLabelText(/^Trial Other$/)).toHaveLength(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('lists each instrument by its title where it has one', async () => {
|
|
37
|
+
const { default: userEvent } = await import('@testing-library/user-event');
|
|
38
|
+
const user = userEvent.setup();
|
|
39
|
+
const titled: Instrument = { ...Other, title: 'Other / Titled' };
|
|
40
|
+
render(<Lab instruments={[Stub, titled]} defaultInstrument="Stub" />);
|
|
41
|
+
await user.click(screen.getByRole('button', { name: 'Add trial' }));
|
|
42
|
+
expect(screen.getByRole('menuitem', { name: 'Stub' })).toBeInTheDocument();
|
|
43
|
+
await user.click(screen.getByRole('menuitem', { name: 'Other / Titled' }));
|
|
44
|
+
expect(screen.getAllByLabelText(/^Trial Other \/ Titled$/)).toHaveLength(1);
|
|
25
45
|
});
|
|
26
46
|
|
|
27
47
|
it('adds a trial when the button is used', async () => {
|
|
28
48
|
const { default: userEvent } = await import('@testing-library/user-event');
|
|
29
49
|
const user = userEvent.setup();
|
|
30
|
-
render(<Lab instruments={[Stub]} defaultInstrument="Stub"
|
|
50
|
+
render(<Lab instruments={[Stub]} defaultInstrument="Stub" />);
|
|
31
51
|
expect(screen.getAllByLabelText(/^Trial Stub$/)).toHaveLength(1);
|
|
32
52
|
await user.click(screen.getByRole('button', { name: /add trial/i }));
|
|
33
53
|
expect(screen.getAllByLabelText(/^Trial Stub$/)).toHaveLength(2);
|
|
34
54
|
});
|
|
35
55
|
|
|
56
|
+
it('leaves the add-trial control out when the lab says so', () => {
|
|
57
|
+
const { unmount } = render(
|
|
58
|
+
<Lab instruments={[Stub, Other]} defaultInstrument="Stub" addTrial={false} />,
|
|
59
|
+
);
|
|
60
|
+
expect(screen.queryByRole('button', { name: /add trial/i })).toBeNull();
|
|
61
|
+
expect(screen.getByRole('radio', { name: 'Auto' })).toBeInTheDocument();
|
|
62
|
+
unmount();
|
|
63
|
+
render(<Lab instruments={[Stub]} defaultInstrument="Stub" addTrial={false} />);
|
|
64
|
+
expect(screen.queryByRole('button', { name: /add trial/i })).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
36
67
|
it('exposes the color mode as a three-way choice', () => {
|
|
37
|
-
render(<Lab instruments={[Stub]} defaultInstrument="Stub"
|
|
68
|
+
render(<Lab instruments={[Stub]} defaultInstrument="Stub" />);
|
|
38
69
|
for (const label of ['Auto', 'Light', 'Dark']) {
|
|
39
70
|
expect(screen.getByRole('radio', { name: label })).toBeInTheDocument();
|
|
40
71
|
}
|
|
@@ -44,7 +75,7 @@ describe('<LabHeader>', () => {
|
|
|
44
75
|
// The assertion above passes whether the segment holds text or an icon,
|
|
45
76
|
// because both routes produce the same accessible name. This is the one
|
|
46
77
|
// that can tell them apart.
|
|
47
|
-
render(<Lab instruments={[Stub]} defaultInstrument="Stub"
|
|
78
|
+
render(<Lab instruments={[Stub]} defaultInstrument="Stub" />);
|
|
48
79
|
for (const label of ['Auto', 'Light', 'Dark']) {
|
|
49
80
|
const segment = screen.getByRole('radio', { name: label });
|
|
50
81
|
expect(segment.querySelector('svg')).not.toBeNull();
|
package/src/lab/LabHeader.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AddIcon, ModeAutoIcon, ModeDarkIcon, ModeLightIcon } from '@weasel-js/ui';
|
|
2
|
-
import {
|
|
2
|
+
import { MenuButton, ToggleBar } from '../passthrough/weasel-ui';
|
|
3
3
|
import type { LabMode } from '../state/types';
|
|
4
4
|
import { useLabContext } from './LabContext';
|
|
5
5
|
|
|
@@ -21,13 +21,13 @@ const MODE_ITEMS = MODES.map(({ value, label, glyph }) => ({
|
|
|
21
21
|
* mode. Both drive `LabContext`, which carried them with no UI at all — so
|
|
22
22
|
* every consumer rebuilt these two. Rendered before a consumer's own header
|
|
23
23
|
* content, which still lands beside them. */
|
|
24
|
-
export function LabHeader() {
|
|
24
|
+
export function LabHeader({ addTrial = true }: { addTrial?: boolean }) {
|
|
25
25
|
const lab = useLabContext();
|
|
26
26
|
const only = lab.instruments.length === 1 ? lab.instruments[0] : null;
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
29
|
<>
|
|
30
|
-
{only ? (
|
|
30
|
+
{!addTrial ? null : only ? (
|
|
31
31
|
<button
|
|
32
32
|
type="button"
|
|
33
33
|
className="lk-lab-header__add"
|
|
@@ -37,17 +37,11 @@ export function LabHeader() {
|
|
|
37
37
|
<span>Add trial</span>
|
|
38
38
|
</button>
|
|
39
39
|
) : (
|
|
40
|
-
|
|
41
|
-
// reading as the current instrument.
|
|
42
|
-
<Select
|
|
43
|
-
className="lk-lab-header__add-select"
|
|
40
|
+
<MenuButton
|
|
44
41
|
aria-label="Add trial"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
onSelectionChange={(name) => {
|
|
49
|
-
if (name != null) lab.addTrial(String(name));
|
|
50
|
-
}}
|
|
42
|
+
label="Add trial…"
|
|
43
|
+
items={lab.instruments.map((i) => ({ value: i.name, label: i.title ?? i.name }))}
|
|
44
|
+
onAction={(name) => lab.addTrial(name)}
|
|
51
45
|
/>
|
|
52
46
|
)}
|
|
53
47
|
|
package/src/lab/LabPalette.tsx
CHANGED
|
@@ -1,34 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { labContributions, useLabChromeContext } from '../chrome/LabChrome';
|
|
2
|
+
import type { LabContribution } from '../chrome/labTypes';
|
|
3
3
|
import { PaletteRegion } from '../chrome/regions/PaletteRegion';
|
|
4
|
-
import type { TrialChromeContext, TrialContribution } from '../chrome/types';
|
|
5
|
-
import { LabStoreContext } from '../state/context';
|
|
6
4
|
import type { TrialTool } from '../tools/types';
|
|
7
5
|
|
|
8
6
|
/** Props for `<LabPalette>`. */
|
|
9
7
|
export interface LabPaletteProps {
|
|
10
|
-
|
|
8
|
+
/** Tools offered lab-wide. Shorthand for one `palette` contribution each. */
|
|
9
|
+
tools?: readonly TrialTool[];
|
|
10
|
+
/** The lab's contributions; the `palette` ones are laid out here. */
|
|
11
|
+
contributions?: readonly LabContribution[];
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
/** The lab's tool
|
|
14
|
-
* instrument declares no tools of its own resolves to.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const contributions: TrialContribution[] = tools.map((t) => ({
|
|
22
|
-
id: t.id,
|
|
23
|
-
region: 'palette',
|
|
24
|
-
group: t.group,
|
|
25
|
-
item: { icon: t.icon, label: t.label, shortcut: t.shortcut },
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
const ctx = {
|
|
29
|
-
activeToolId,
|
|
30
|
-
setActiveTool: setLabTool,
|
|
31
|
-
} as unknown as TrialChromeContext;
|
|
32
|
-
|
|
33
|
-
return <PaletteRegion contributions={contributions} ctx={ctx} />;
|
|
14
|
+
/** The lab's tool rail. Writes the lab's tool slot, which every trial whose
|
|
15
|
+
* instrument declares no tools of its own resolves to. Renders nothing when
|
|
16
|
+
* the lab contributes no tool rail. */
|
|
17
|
+
export function LabPalette({ tools, contributions }: LabPaletteProps) {
|
|
18
|
+
const ctx = useLabChromeContext();
|
|
19
|
+
const all = labContributions(tools, contributions);
|
|
20
|
+
return <PaletteRegion contributions={all.filter((c) => c.region === 'palette')} ctx={ctx} />;
|
|
34
21
|
}
|
package/src/lab/LabShell.less
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
display: flex;
|
|
3
3
|
flex-direction: column;
|
|
4
4
|
width: 100%;
|
|
5
|
+
// Fills a container of definite height. Where the container has none, the
|
|
6
|
+
// percentage drops out and the shell takes the viewport's height instead.
|
|
5
7
|
height: 100vh;
|
|
8
|
+
height: 100dvh;
|
|
9
|
+
max-height: 100%;
|
|
6
10
|
background: var(--wzl-surface);
|
|
7
11
|
}
|
|
8
12
|
|
|
@@ -42,35 +46,72 @@
|
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
.lk-shell-body {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
45
51
|
flex: 1;
|
|
46
52
|
min-height: 0;
|
|
53
|
+
min-width: 0;
|
|
47
54
|
overflow: auto;
|
|
48
55
|
padding: var(--lk-workspace-pad);
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
// `.lk-shell-body` is a block container, so `flex: 1` here resolves to nothing
|
|
52
|
-
// and the workspace's `height: 100%` collapses to zero.
|
|
53
58
|
.lk-lab__body {
|
|
54
59
|
display: flex;
|
|
55
|
-
|
|
60
|
+
flex: 1;
|
|
56
61
|
min-height: 0;
|
|
57
62
|
position: relative;
|
|
58
63
|
|
|
59
|
-
> .lk-workspace
|
|
64
|
+
> .lk-workspace,
|
|
65
|
+
> .lk-workspace-host {
|
|
66
|
+
min-width: 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.lk-lab__panes {
|
|
71
|
+
flex: 1;
|
|
72
|
+
min-width: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.lk-lab__sidebar {
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-direction: column;
|
|
78
|
+
height: 100%;
|
|
79
|
+
overflow-y: auto;
|
|
80
|
+
background: var(--wzl-surface);
|
|
81
|
+
border-right: 1px solid var(--wzl-border);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.lk-lab__main {
|
|
85
|
+
display: flex;
|
|
86
|
+
min-width: 0;
|
|
87
|
+
height: 100%;
|
|
88
|
+
|
|
89
|
+
> .lk-workspace,
|
|
90
|
+
> .lk-workspace-host {
|
|
91
|
+
flex: 1;
|
|
60
92
|
min-width: 0;
|
|
61
93
|
}
|
|
62
94
|
}
|
|
63
95
|
|
|
64
|
-
// The lab's
|
|
65
|
-
// annotate
|
|
66
|
-
//
|
|
96
|
+
// The lab's two shared drawing buffers, stacked around the trials: marks that
|
|
97
|
+
// annotate an instrument go over its DOM, an opaque renderer goes under it so
|
|
98
|
+
// the pane can still hold a label or a control. Both sit under the viewport
|
|
99
|
+
// chrome, whose z-overlay outranks them, and both are inert — every tile takes
|
|
100
|
+
// input from its own box.
|
|
67
101
|
.lk-lab__surface {
|
|
68
102
|
position: absolute;
|
|
69
103
|
inset: 0;
|
|
70
|
-
z-index: 1;
|
|
71
104
|
pointer-events: none;
|
|
72
105
|
}
|
|
73
106
|
|
|
107
|
+
.lk-lab__surface--under {
|
|
108
|
+
z-index: 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.lk-lab__surface--over {
|
|
112
|
+
z-index: 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
74
115
|
.lk-shell-footer {
|
|
75
116
|
border-top: 1px solid var(--wzl-border);
|
|
76
117
|
padding: var(--wzl-space-xs) var(--wzl-space-md);
|
|
@@ -98,10 +139,6 @@
|
|
|
98
139
|
// against a shrink-to-fit header row it swallows the slack, which pushed the
|
|
99
140
|
// consumer's own header content down to min-content and wrapped the row to
|
|
100
141
|
// three lines. See the `.lk-root` prefix convention in theme/base.less.
|
|
101
|
-
.lk-root .lk-lab-header__add-select {
|
|
102
|
-
width: 160px;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
142
|
// The mode toggle is a preference, not a primary action: it sits quiet until
|
|
106
143
|
// the pointer is on it.
|
|
107
144
|
.lk-lab-header__mode {
|
package/src/lab/LabSwitcher.less
CHANGED
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
border: 0;
|
|
22
22
|
padding: 0;
|
|
23
23
|
cursor: pointer;
|
|
24
|
+
// A title is a click target now, and a consumer's header is usually a
|
|
25
|
+
// crowded flex row -- left to wrap, `brick-icons corpus` breaks after the
|
|
26
|
+
// hyphen and the control becomes three ragged lines with a caret adrift.
|
|
27
|
+
white-space: nowrap;
|
|
24
28
|
|
|
25
29
|
&:hover { color: var(--wzl-text-accent, var(--wzl-text)); }
|
|
26
30
|
|
|
@@ -55,7 +59,10 @@
|
|
|
55
59
|
display: flex;
|
|
56
60
|
flex-direction: column;
|
|
57
61
|
padding: var(--wzl-space-xs);
|
|
58
|
-
|
|
62
|
+
// `--wzl-surface-raised` is translucent by design -- it is for panels that
|
|
63
|
+
// blur what sits behind them. A menu has to be read, so it takes the opaque
|
|
64
|
+
// surface, the way `FloatingPanel` does.
|
|
65
|
+
background: var(--wzl-surface);
|
|
59
66
|
border: 1px solid var(--wzl-border);
|
|
60
67
|
border-radius: var(--wzl-radius-md, 6px);
|
|
61
68
|
box-shadow: var(--wzl-shadow-md, 0 6px 20px rgb(0 0 0 / 35%));
|
|
@@ -10,7 +10,8 @@ function fakeHandle(): SurfaceHandle {
|
|
|
10
10
|
invalidateAll: vi.fn(),
|
|
11
11
|
invalidateRects: vi.fn(),
|
|
12
12
|
registerTile: vi.fn(),
|
|
13
|
-
|
|
13
|
+
registerClear: vi.fn(() => () => {}),
|
|
14
|
+
registerPainter: vi.fn(() => () => {}),
|
|
14
15
|
containerRef: vi.fn(),
|
|
15
16
|
getContainer: vi.fn(() => null),
|
|
16
17
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { describeElement, findOvershoot } from './fitCheck';
|
|
3
|
+
|
|
4
|
+
const bound = { top: 0, left: 0, bottom: 600, right: 800 };
|
|
5
|
+
const box = (bottom: number, right: number) => ({ top: 0, left: 0, bottom, right });
|
|
6
|
+
|
|
7
|
+
describe('findOvershoot', () => {
|
|
8
|
+
it('returns null when everything is inside the bound', () => {
|
|
9
|
+
expect(findOvershoot(bound, [{ item: 'a', edges: box(600, 800) }])).toBeNull();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('ignores sub-pixel overshoot within the tolerance', () => {
|
|
13
|
+
expect(findOvershoot(bound, [{ item: 'a', edges: box(600.8, 800) }])).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('names the item reaching furthest past the bound', () => {
|
|
17
|
+
const hit = findOvershoot(bound, [
|
|
18
|
+
{ item: 'header', edges: box(640, 800) },
|
|
19
|
+
{ item: 'tile', edges: box(900, 800) },
|
|
20
|
+
{ item: 'footer', edges: box(700, 800) },
|
|
21
|
+
]);
|
|
22
|
+
expect(hit).toEqual({ item: 'tile', by: 300, axis: 'y' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('compares both axes and reports the worse one', () => {
|
|
26
|
+
const hit = findOvershoot(bound, [
|
|
27
|
+
{ item: 'tall', edges: box(650, 800) },
|
|
28
|
+
{ item: 'wide', edges: box(600, 1000) },
|
|
29
|
+
]);
|
|
30
|
+
expect(hit).toEqual({ item: 'wide', by: 200, axis: 'x' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('keeps the first item on a tie, which is the outermost in document order', () => {
|
|
34
|
+
const hit = findOvershoot(bound, [
|
|
35
|
+
{ item: 'shell', edges: box(616, 800) },
|
|
36
|
+
{ item: 'footer', edges: box(616, 800) },
|
|
37
|
+
]);
|
|
38
|
+
expect(hit?.item).toBe('shell');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('describeElement', () => {
|
|
43
|
+
it('writes the tag and every class', () => {
|
|
44
|
+
const el = document.createElement('DIV');
|
|
45
|
+
el.className = 'lk-shell-body extra';
|
|
46
|
+
expect(describeElement(el)).toBe('div.lk-shell-body.extra');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('places a classless element by its nearest classed ancestor', () => {
|
|
50
|
+
const outer = document.createElement('div');
|
|
51
|
+
outer.className = 'lk-trial';
|
|
52
|
+
const mid = document.createElement('div');
|
|
53
|
+
const leaf = document.createElement('canvas');
|
|
54
|
+
outer.appendChild(mid).appendChild(leaf);
|
|
55
|
+
expect(describeElement(leaf)).toBe('canvas in div.lk-trial');
|
|
56
|
+
});
|
|
57
|
+
});
|