@pixel-point/toolcraft 0.0.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/LICENSE.md +98 -0
- package/README.md +41 -0
- package/bin/create-toolcraft-app.mjs +8 -0
- package/bin/toolcraft.mjs +8 -0
- package/package.json +24 -0
- package/scripts/prepare-pack.mjs +29 -0
- package/src/cli.mjs +392 -0
- package/src/cli.test.mjs +284 -0
- package/src/copy-recursive.mjs +86 -0
- package/src/generate.mjs +212 -0
- package/src/generate.test.mjs +322 -0
- package/src/import-map.mjs +14 -0
- package/src/package-json.mjs +80 -0
- package/src/package-json.test.mjs +67 -0
- package/src/rewrite-imports.mjs +85 -0
- package/src/rewrite-imports.test.mjs +58 -0
- package/templates/runtime/contracts/component-contracts.test.ts +1165 -0
- package/templates/runtime/contracts/component-contracts.ts +1340 -0
- package/templates/runtime/contracts/decision-contracts.test.ts +206 -0
- package/templates/runtime/contracts/decision-contracts.ts +283 -0
- package/templates/runtime/contracts/index.test.ts +14 -0
- package/templates/runtime/contracts/index.ts +3 -0
- package/templates/runtime/contracts/types.ts +56 -0
- package/templates/runtime/export/export.test.ts +203 -0
- package/templates/runtime/export/export.ts +132 -0
- package/templates/runtime/export/index.ts +1 -0
- package/templates/runtime/index.ts +14 -0
- package/templates/runtime/react/canvas-shell.test.tsx +424 -0
- package/templates/runtime/react/canvas-shell.tsx +408 -0
- package/templates/runtime/react/control-renderers.ts +31 -0
- package/templates/runtime/react/controls-panel.test.tsx +3736 -0
- package/templates/runtime/react/controls-panel.tsx +2327 -0
- package/templates/runtime/react/curve-geometry.test.ts +70 -0
- package/templates/runtime/react/index.ts +15 -0
- package/templates/runtime/react/layer-tree.ts +96 -0
- package/templates/runtime/react/layers-panel.test.tsx +487 -0
- package/templates/runtime/react/layers-panel.tsx +1348 -0
- package/templates/runtime/react/media-file.ts +82 -0
- package/templates/runtime/react/panel-host-config.ts +80 -0
- package/templates/runtime/react/panel-host-geometry.test.ts +66 -0
- package/templates/runtime/react/panel-host-geometry.ts +109 -0
- package/templates/runtime/react/panel-host-types.ts +74 -0
- package/templates/runtime/react/panel-host.test.tsx +102 -0
- package/templates/runtime/react/panel-host.tsx +353 -0
- package/templates/runtime/react/runtime-public-api.test.tsx +132 -0
- package/templates/runtime/react/settings-transfer.test.ts +150 -0
- package/templates/runtime/react/settings-transfer.ts +279 -0
- package/templates/runtime/react/storage-key-migration.ts +48 -0
- package/templates/runtime/react/theme-runtime.tsx +177 -0
- package/templates/runtime/react/timeline-panel.test.tsx +668 -0
- package/templates/runtime/react/timeline-panel.tsx +2953 -0
- package/templates/runtime/react/toolbar-panel.test.tsx +212 -0
- package/templates/runtime/react/toolbar-panel.tsx +205 -0
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +350 -0
- package/templates/runtime/react/toolcraft-app.test.tsx +339 -0
- package/templates/runtime/react/toolcraft-app.tsx +81 -0
- package/templates/runtime/react/toolcraft-root.test.tsx +347 -0
- package/templates/runtime/react/toolcraft-root.tsx +203 -0
- package/templates/runtime/react/use-toolcraft.ts +41 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +1524 -0
- package/templates/runtime/schema/define-toolcraft.ts +1442 -0
- package/templates/runtime/schema/keyframe-capability.test.ts +90 -0
- package/templates/runtime/schema/keyframe-capability.ts +51 -0
- package/templates/runtime/schema/runtime-targets.ts +40 -0
- package/templates/runtime/schema/types.ts +370 -0
- package/templates/runtime/state/canvas-zoom.ts +8 -0
- package/templates/runtime/state/create-template-state.test.ts +242 -0
- package/templates/runtime/state/create-template-state.ts +95 -0
- package/templates/runtime/state/keyframe-evaluation.test.ts +141 -0
- package/templates/runtime/state/keyframe-evaluation.ts +203 -0
- package/templates/runtime/state/persistence.test.ts +217 -0
- package/templates/runtime/state/persistence.ts +511 -0
- package/templates/runtime/state/reducer.test.ts +937 -0
- package/templates/runtime/state/reducer.ts +1212 -0
- package/templates/runtime/state/timeline-readiness.ts +43 -0
- package/templates/runtime/state/types.ts +242 -0
- package/templates/runtime/styles.css +125 -0
- package/templates/runtime/testing/performance.test.ts +1058 -0
- package/templates/runtime/testing/performance.ts +1078 -0
- package/templates/starter/AGENTS.md +186 -0
- package/templates/starter/LICENSE.md +98 -0
- package/templates/starter/NOTICE.md +8 -0
- package/templates/starter/docs/toolcraft/README.md +41 -0
- package/templates/starter/docs/toolcraft/acceptance-testing.md +205 -0
- package/templates/starter/docs/toolcraft/agent-worklog.md +81 -0
- package/templates/starter/docs/toolcraft/assembly-workflow.md +206 -0
- package/templates/starter/docs/toolcraft/component-rules.md +299 -0
- package/templates/starter/docs/toolcraft/custom-controls.md +71 -0
- package/templates/starter/docs/toolcraft/decision-contract.md +71 -0
- package/templates/starter/docs/toolcraft/performance.md +112 -0
- package/templates/starter/docs/toolcraft/renderer-technique.md +48 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +265 -0
- package/templates/starter/docs/toolcraft/workflow.md +87 -0
- package/templates/starter/e2e/app-browser-acceptance.spec.ts +785 -0
- package/templates/starter/e2e/app-controls.spec.ts +41 -0
- package/templates/starter/e2e/app-performance.spec.ts +326 -0
- package/templates/starter/e2e/canvas-handle-helpers.ts +244 -0
- package/templates/starter/e2e/performance-helpers.ts +612 -0
- package/templates/starter/e2e/product-observable-helpers.ts +170 -0
- package/templates/starter/index.html +12 -0
- package/templates/starter/package.json +52 -0
- package/templates/starter/playwright.config.ts +43 -0
- package/templates/starter/scripts/check-ai-skills.mjs +95 -0
- package/templates/starter/scripts/check-toolcraft-docs.mjs +159 -0
- package/templates/starter/scripts/check-toolcraft-integrity.mjs +232 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +48 -0
- package/templates/starter/scripts/toolcraft-port.mjs +54 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +73 -0
- package/templates/starter/src/app/starter-acceptance.test.ts +5959 -0
- package/templates/starter/src/app/starter-acceptance.ts +2646 -0
- package/templates/starter/src/app/starter-performance.test.ts +1390 -0
- package/templates/starter/src/app/starter-performance.ts +12 -0
- package/templates/starter/src/app/starter-schema.test.ts +70 -0
- package/templates/starter/src/app/starter-schema.ts +15 -0
- package/templates/starter/src/main.tsx +18 -0
- package/templates/starter/src/router.tsx +16 -0
- package/templates/starter/src/routes/index.tsx +7 -0
- package/templates/starter/src/routes/root.tsx +19 -0
- package/templates/starter/src/styles.css +120 -0
- package/templates/starter/tsconfig.json +11 -0
- package/templates/starter/vite.config.ts +13 -0
- package/templates/ui/components/composites/accordion.tsx +73 -0
- package/templates/ui/components/composites/alert-dialog.tsx +190 -0
- package/templates/ui/components/composites/alert.tsx +74 -0
- package/templates/ui/components/composites/aspect-ratio.tsx +22 -0
- package/templates/ui/components/composites/avatar.tsx +98 -0
- package/templates/ui/components/composites/badge.tsx +69 -0
- package/templates/ui/components/composites/breadcrumb.tsx +106 -0
- package/templates/ui/components/composites/card.tsx +91 -0
- package/templates/ui/components/composites/combobox.tsx +486 -0
- package/templates/ui/components/composites/command.tsx +296 -0
- package/templates/ui/components/composites/context-menu.tsx +247 -0
- package/templates/ui/components/composites/dialog.tsx +282 -0
- package/templates/ui/components/composites/dropdown-menu.tsx +299 -0
- package/templates/ui/components/composites/empty.tsx +110 -0
- package/templates/ui/components/composites/hover-card.tsx +44 -0
- package/templates/ui/components/composites/index.ts +30 -0
- package/templates/ui/components/composites/menubar.tsx +214 -0
- package/templates/ui/components/composites/navigation-menu.tsx +167 -0
- package/templates/ui/components/composites/pagination.tsx +131 -0
- package/templates/ui/components/composites/progress.tsx +72 -0
- package/templates/ui/components/composites/radio-group.tsx +84 -0
- package/templates/ui/components/composites/resizable.tsx +42 -0
- package/templates/ui/components/composites/sheet.tsx +153 -0
- package/templates/ui/components/composites/sidebar-structural.tsx +310 -0
- package/templates/ui/components/composites/sidebar.tsx +431 -0
- package/templates/ui/components/composites/sonner.tsx +35 -0
- package/templates/ui/components/composites/spinner.tsx +43 -0
- package/templates/ui/components/composites/table.tsx +108 -0
- package/templates/ui/components/composites/tabs.tsx +83 -0
- package/templates/ui/components/control-layout/index.tsx +437 -0
- package/templates/ui/components/controls/actions/actions-control.tsx +139 -0
- package/templates/ui/components/controls/actions/index.ts +9 -0
- package/templates/ui/components/controls/anchor-grid/anchor-grid-control.tsx +107 -0
- package/templates/ui/components/controls/anchor-grid/index.ts +4 -0
- package/templates/ui/components/controls/boolean/boolean-controls.tsx +79 -0
- package/templates/ui/components/controls/boolean/index.ts +4 -0
- package/templates/ui/components/controls/channel-mixer/channel-mixer-control.tsx +95 -0
- package/templates/ui/components/controls/channel-mixer/index.ts +4 -0
- package/templates/ui/components/controls/channel-tabs/channel-tabs.tsx +42 -0
- package/templates/ui/components/controls/channel-tabs/index.ts +6 -0
- package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +90 -0
- package/templates/ui/components/controls/code-textarea/index.ts +4 -0
- package/templates/ui/components/controls/color/color-control.tsx +571 -0
- package/templates/ui/components/controls/color/color-picker-popover.tsx +104 -0
- package/templates/ui/components/controls/color/index.ts +41 -0
- package/templates/ui/components/controls/color/palette-control-data.ts +436 -0
- package/templates/ui/components/controls/color/palette-control.tsx +535 -0
- package/templates/ui/components/controls/color/style-guide-color-picker-channel-utils.ts +162 -0
- package/templates/ui/components/controls/color/style-guide-color-picker-interactions.ts +190 -0
- package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +485 -0
- package/templates/ui/components/controls/color/style-guide-color-picker-parts.tsx +710 -0
- package/templates/ui/components/controls/color/style-guide-color-picker.tsx +503 -0
- package/templates/ui/components/controls/control-types.ts +43 -0
- package/templates/ui/components/controls/curves/curve-geometry.ts +355 -0
- package/templates/ui/components/controls/curves/curve-graph.tsx +390 -0
- package/templates/ui/components/controls/curves/curves-control.tsx +445 -0
- package/templates/ui/components/controls/curves/index.ts +6 -0
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +191 -0
- package/templates/ui/components/controls/file-drop/index.ts +5 -0
- package/templates/ui/components/controls/font-picker/font-catalog.json +15360 -0
- package/templates/ui/components/controls/font-picker/font-catalog.ts +116 -0
- package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1202 -0
- package/templates/ui/components/controls/font-picker/font-preview-loader.ts +336 -0
- package/templates/ui/components/controls/font-picker/index.ts +24 -0
- package/templates/ui/components/controls/font-picker/use-hover-intent.ts +46 -0
- package/templates/ui/components/controls/gradient/gradient-control-utils.ts +190 -0
- package/templates/ui/components/controls/gradient/gradient-control.tsx +612 -0
- package/templates/ui/components/controls/gradient/gradient-stop-list.tsx +400 -0
- package/templates/ui/components/controls/gradient/gradient-toolbar.tsx +152 -0
- package/templates/ui/components/controls/gradient/index.ts +4 -0
- package/templates/ui/components/controls/image-picker/image-picker-control.tsx +139 -0
- package/templates/ui/components/controls/image-picker/index.ts +7 -0
- package/templates/ui/components/controls/index.ts +192 -0
- package/templates/ui/components/controls/range-input/index.ts +4 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +173 -0
- package/templates/ui/components/controls/range-slider/index.ts +4 -0
- package/templates/ui/components/controls/range-slider/range-slider-control.tsx +122 -0
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +61 -0
- package/templates/ui/components/controls/segmented/index.ts +8 -0
- package/templates/ui/components/controls/segmented/segmented-control.tsx +94 -0
- package/templates/ui/components/controls/select/index.ts +4 -0
- package/templates/ui/components/controls/select/select-control.tsx +223 -0
- package/templates/ui/components/controls/slider/index.ts +4 -0
- package/templates/ui/components/controls/slider/slider-control.tsx +150 -0
- package/templates/ui/components/controls/slider/slider-value.ts +56 -0
- package/templates/ui/components/controls/text-input/index.ts +4 -0
- package/templates/ui/components/controls/text-input/text-input-control.tsx +158 -0
- package/templates/ui/components/controls/use-measured-element-width.ts +42 -0
- package/templates/ui/components/controls/vector/index.ts +8 -0
- package/templates/ui/components/controls/vector/vector-control.tsx +401 -0
- package/templates/ui/components/panel/index.ts +19 -0
- package/templates/ui/components/panel/panel-actions.tsx +165 -0
- package/templates/ui/components/panel/panel-header.tsx +61 -0
- package/templates/ui/components/panel/panel-icon-button.tsx +96 -0
- package/templates/ui/components/panel/panel-section.tsx +168 -0
- package/templates/ui/components/panel/panel-surface.tsx +206 -0
- package/templates/ui/components/panel/panel.tsx +210 -0
- package/templates/ui/components/primitives/animated-loader.tsx +61 -0
- package/templates/ui/components/primitives/button-group.tsx +134 -0
- package/templates/ui/components/primitives/button.tsx +429 -0
- package/templates/ui/components/primitives/checkbox.tsx +62 -0
- package/templates/ui/components/primitives/editable-slider-value-label.tsx +337 -0
- package/templates/ui/components/primitives/field.tsx +225 -0
- package/templates/ui/components/primitives/index.ts +82 -0
- package/templates/ui/components/primitives/input-group.tsx +298 -0
- package/templates/ui/components/primitives/input.tsx +61 -0
- package/templates/ui/components/primitives/internal/button-loading.tsx +178 -0
- package/templates/ui/components/primitives/label.tsx +16 -0
- package/templates/ui/components/primitives/popover.tsx +126 -0
- package/templates/ui/components/primitives/portal-layer-context.tsx +33 -0
- package/templates/ui/components/primitives/primitive-arrow-icon.tsx +38 -0
- package/templates/ui/components/primitives/scroll-fade-logic.ts +441 -0
- package/templates/ui/components/primitives/scroll-fade-render.tsx +75 -0
- package/templates/ui/components/primitives/scroll-fade-types.ts +41 -0
- package/templates/ui/components/primitives/scroll-fade.tsx +72 -0
- package/templates/ui/components/primitives/select.tsx +408 -0
- package/templates/ui/components/primitives/selection-state.ts +31 -0
- package/templates/ui/components/primitives/separator.tsx +21 -0
- package/templates/ui/components/primitives/slider/index.ts +4 -0
- package/templates/ui/components/primitives/slider/slider-interaction.tsx +96 -0
- package/templates/ui/components/primitives/slider/slider-parts.tsx +303 -0
- package/templates/ui/components/primitives/slider/slider-reset.ts +152 -0
- package/templates/ui/components/primitives/slider/slider-value.ts +114 -0
- package/templates/ui/components/primitives/slider/slider.tsx +511 -0
- package/templates/ui/components/primitives/switch.tsx +35 -0
- package/templates/ui/components/primitives/textarea.tsx +49 -0
- package/templates/ui/components/primitives/toggle-group.tsx +114 -0
- package/templates/ui/components/primitives/toggle.tsx +46 -0
- package/templates/ui/components/primitives/tooltip.tsx +100 -0
- package/templates/ui/hooks/use-mobile.ts +21 -0
- package/templates/ui/index.ts +31 -0
- package/templates/ui/lib/control-outline.ts +3 -0
- package/templates/ui/lib/input-control-style.ts +131 -0
- package/templates/ui/lib/style-guide-color-utils.ts +111 -0
- package/templates/ui/lib/utils.ts +6 -0
- package/templates/ui/styles.css +291 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import { expect, type Locator, type Page } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ToolcraftPerformanceBudget,
|
|
5
|
+
ToolcraftPerformanceConfig,
|
|
6
|
+
} from "@repo/toolcraft-runtime";
|
|
7
|
+
|
|
8
|
+
export type ToolcraftFrameProbeResult = {
|
|
9
|
+
longTaskCount: number;
|
|
10
|
+
longTaskMaxMs: number;
|
|
11
|
+
maxFrameGapMs: number;
|
|
12
|
+
sampleCount: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ToolcraftInteractionResult = ToolcraftFrameProbeResult & {
|
|
16
|
+
durationMs: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ToolcraftInteractionOptions = {
|
|
20
|
+
settleFrames?: number;
|
|
21
|
+
settleMs?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function startToolcraftFrameProbe(
|
|
25
|
+
page: Page,
|
|
26
|
+
): Promise<() => Promise<ToolcraftFrameProbeResult>> {
|
|
27
|
+
await page.evaluate(() => {
|
|
28
|
+
const win = window as Window & {
|
|
29
|
+
__toolcraftFrameProbe?: {
|
|
30
|
+
active: boolean;
|
|
31
|
+
longTaskCount: number;
|
|
32
|
+
longTaskMaxMs: number;
|
|
33
|
+
observer?: PerformanceObserver;
|
|
34
|
+
maxFrameGapMs: number;
|
|
35
|
+
rafId: number;
|
|
36
|
+
sampleCount: number;
|
|
37
|
+
};
|
|
38
|
+
__toolcraftStopFrameProbe?: () => ToolcraftFrameProbeResult;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (win.__toolcraftFrameProbe?.active) {
|
|
42
|
+
cancelAnimationFrame(win.__toolcraftFrameProbe.rafId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let lastFrame = performance.now();
|
|
46
|
+
win.__toolcraftFrameProbe = {
|
|
47
|
+
active: true,
|
|
48
|
+
longTaskCount: 0,
|
|
49
|
+
longTaskMaxMs: 0,
|
|
50
|
+
maxFrameGapMs: 0,
|
|
51
|
+
rafId: 0,
|
|
52
|
+
sampleCount: 0,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
win.__toolcraftFrameProbe.observer = new PerformanceObserver((list) => {
|
|
57
|
+
const probe = win.__toolcraftFrameProbe;
|
|
58
|
+
if (!probe?.active) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const entry of list.getEntries()) {
|
|
63
|
+
probe.longTaskCount += 1;
|
|
64
|
+
probe.longTaskMaxMs = Math.max(probe.longTaskMaxMs, entry.duration);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
win.__toolcraftFrameProbe.observer.observe({ entryTypes: ["longtask"] });
|
|
68
|
+
} catch {
|
|
69
|
+
// Some browser contexts do not expose longtask entries. Frame gaps still catch jank.
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const tick = (now: number) => {
|
|
73
|
+
const probe = win.__toolcraftFrameProbe;
|
|
74
|
+
if (!probe?.active) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
probe.maxFrameGapMs = Math.max(probe.maxFrameGapMs, now - lastFrame);
|
|
79
|
+
probe.sampleCount += 1;
|
|
80
|
+
lastFrame = now;
|
|
81
|
+
probe.rafId = requestAnimationFrame(tick);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
win.__toolcraftFrameProbe.rafId = requestAnimationFrame(tick);
|
|
85
|
+
win.__toolcraftStopFrameProbe = () => {
|
|
86
|
+
const probe = win.__toolcraftFrameProbe ?? {
|
|
87
|
+
active: false,
|
|
88
|
+
longTaskCount: 0,
|
|
89
|
+
longTaskMaxMs: 0,
|
|
90
|
+
maxFrameGapMs: 0,
|
|
91
|
+
rafId: 0,
|
|
92
|
+
sampleCount: 0,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
probe.active = false;
|
|
96
|
+
cancelAnimationFrame(probe.rafId);
|
|
97
|
+
probe.observer?.disconnect();
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
longTaskCount: probe.longTaskCount,
|
|
101
|
+
longTaskMaxMs: probe.longTaskMaxMs,
|
|
102
|
+
maxFrameGapMs: probe.maxFrameGapMs,
|
|
103
|
+
sampleCount: probe.sampleCount,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return async () =>
|
|
109
|
+
page.evaluate(() => {
|
|
110
|
+
const win = window as Window & {
|
|
111
|
+
__toolcraftStopFrameProbe?: () => ToolcraftFrameProbeResult;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
win.__toolcraftStopFrameProbe?.() ?? {
|
|
116
|
+
longTaskCount: 0,
|
|
117
|
+
longTaskMaxMs: 0,
|
|
118
|
+
maxFrameGapMs: 0,
|
|
119
|
+
sampleCount: 0,
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function measureToolcraftInteraction(
|
|
126
|
+
page: Page,
|
|
127
|
+
action: () => Promise<void>,
|
|
128
|
+
options: ToolcraftInteractionOptions = {},
|
|
129
|
+
): Promise<ToolcraftInteractionResult> {
|
|
130
|
+
const stopProbe = await startToolcraftFrameProbe(page);
|
|
131
|
+
const startedAt = await page.evaluate(() => performance.now());
|
|
132
|
+
|
|
133
|
+
await action();
|
|
134
|
+
|
|
135
|
+
const endedAt = await page.evaluate(() => performance.now());
|
|
136
|
+
await waitForToolcraftAnimationFrames(page, options.settleFrames ?? 3);
|
|
137
|
+
|
|
138
|
+
if (options.settleMs && options.settleMs > 0) {
|
|
139
|
+
await page.waitForTimeout(options.settleMs);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const frameProbe = await stopProbe();
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
durationMs: endedAt - startedAt,
|
|
146
|
+
longTaskCount: frameProbe.longTaskCount,
|
|
147
|
+
longTaskMaxMs: frameProbe.longTaskMaxMs,
|
|
148
|
+
maxFrameGapMs: frameProbe.maxFrameGapMs,
|
|
149
|
+
sampleCount: frameProbe.sampleCount,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function measureToolcraftAnimationFrames(
|
|
154
|
+
page: Page,
|
|
155
|
+
frameCount = 120,
|
|
156
|
+
options: ToolcraftInteractionOptions = {},
|
|
157
|
+
): Promise<ToolcraftInteractionResult> {
|
|
158
|
+
if (frameCount < 120) {
|
|
159
|
+
throw new Error("Animation performance probes must sample at least 120 frames.");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const stopProbe = await startToolcraftFrameProbe(page);
|
|
163
|
+
const startedAt = await page.evaluate(() => performance.now());
|
|
164
|
+
|
|
165
|
+
await waitForToolcraftAnimationFrames(page, frameCount);
|
|
166
|
+
|
|
167
|
+
if (options.settleFrames && options.settleFrames > 0) {
|
|
168
|
+
await waitForToolcraftAnimationFrames(page, options.settleFrames);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (options.settleMs && options.settleMs > 0) {
|
|
172
|
+
await page.waitForTimeout(options.settleMs);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const endedAt = await page.evaluate(() => performance.now());
|
|
176
|
+
const frameProbe = await stopProbe();
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
durationMs: endedAt - startedAt,
|
|
180
|
+
longTaskCount: frameProbe.longTaskCount,
|
|
181
|
+
longTaskMaxMs: frameProbe.longTaskMaxMs,
|
|
182
|
+
maxFrameGapMs: frameProbe.maxFrameGapMs,
|
|
183
|
+
sampleCount: frameProbe.sampleCount,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function waitForToolcraftAnimationFrames(page: Page, count: number): Promise<void> {
|
|
188
|
+
if (count <= 0) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await page.evaluate(
|
|
193
|
+
(frameCount) =>
|
|
194
|
+
new Promise<void>((resolve) => {
|
|
195
|
+
let remainingFrames = frameCount;
|
|
196
|
+
|
|
197
|
+
const tick = () => {
|
|
198
|
+
remainingFrames -= 1;
|
|
199
|
+
|
|
200
|
+
if (remainingFrames <= 0) {
|
|
201
|
+
resolve();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
requestAnimationFrame(tick);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
requestAnimationFrame(tick);
|
|
209
|
+
}),
|
|
210
|
+
count,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function getToolcraftFieldByLabel(page: Page, label: string): Promise<Locator> {
|
|
215
|
+
const field = page.locator('[data-slot="field"]').filter({ hasText: new RegExp(`^${label}`) });
|
|
216
|
+
await expect(field, `Toolcraft field "${label}" should be visible`).toBeVisible();
|
|
217
|
+
return field;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function expectToolcraftSegmentedControlCellsPreservePadding(
|
|
221
|
+
page: Page,
|
|
222
|
+
label: string,
|
|
223
|
+
options: {
|
|
224
|
+
minHorizontalPaddingPx?: number;
|
|
225
|
+
} = {},
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
const minHorizontalPaddingPx = options.minHorizontalPaddingPx ?? 6;
|
|
228
|
+
const field = await getToolcraftFieldByLabel(page, label);
|
|
229
|
+
const segmentedGroup = field.locator('[data-slot="toggle-group"]').first();
|
|
230
|
+
|
|
231
|
+
await expect(
|
|
232
|
+
segmentedGroup,
|
|
233
|
+
`Toolcraft segmented control "${label}" should render a toggle group.`,
|
|
234
|
+
).toBeVisible();
|
|
235
|
+
|
|
236
|
+
const issues = await segmentedGroup.evaluate(
|
|
237
|
+
(group, minPadding) => {
|
|
238
|
+
type LayoutIssue = {
|
|
239
|
+
label: string;
|
|
240
|
+
reason: string;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
function getTextRect(element: Element): DOMRect | null {
|
|
244
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
|
|
245
|
+
acceptNode(node) {
|
|
246
|
+
return node.textContent?.trim()
|
|
247
|
+
? NodeFilter.FILTER_ACCEPT
|
|
248
|
+
: NodeFilter.FILTER_REJECT;
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
const textNodes: Text[] = [];
|
|
252
|
+
let currentNode = walker.nextNode();
|
|
253
|
+
|
|
254
|
+
while (currentNode) {
|
|
255
|
+
textNodes.push(currentNode as Text);
|
|
256
|
+
currentNode = walker.nextNode();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (textNodes.length === 0) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const range = document.createRange();
|
|
264
|
+
range.setStartBefore(textNodes[0]!);
|
|
265
|
+
range.setEndAfter(textNodes[textNodes.length - 1]!);
|
|
266
|
+
|
|
267
|
+
return range.getBoundingClientRect();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const items = Array.from(
|
|
271
|
+
group.querySelectorAll<HTMLElement>('[data-slot="toggle-group-item"]'),
|
|
272
|
+
);
|
|
273
|
+
const issues: LayoutIssue[] = [];
|
|
274
|
+
const canvas = document.createElement("canvas");
|
|
275
|
+
const context = canvas.getContext("2d");
|
|
276
|
+
|
|
277
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
278
|
+
const item = items[index]!;
|
|
279
|
+
const itemRect = item.getBoundingClientRect();
|
|
280
|
+
const textRect = getTextRect(item);
|
|
281
|
+
const label = item.textContent?.trim() || item.getAttribute("aria-label") || `#${index + 1}`;
|
|
282
|
+
const nextItem = items[index + 1];
|
|
283
|
+
const computedStyle = window.getComputedStyle(item);
|
|
284
|
+
|
|
285
|
+
if (context && label.trim()) {
|
|
286
|
+
context.font = computedStyle.font;
|
|
287
|
+
const measuredTextWidth = context.measureText(label).width;
|
|
288
|
+
const requiredWidth = measuredTextWidth + minPadding * 2;
|
|
289
|
+
|
|
290
|
+
if (requiredWidth > itemRect.width + 0.5) {
|
|
291
|
+
issues.push({
|
|
292
|
+
label,
|
|
293
|
+
reason: `label requires ${requiredWidth.toFixed(2)}px including padding but cell width is ${itemRect.width.toFixed(2)}px`,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (nextItem) {
|
|
299
|
+
const nextRect = nextItem.getBoundingClientRect();
|
|
300
|
+
|
|
301
|
+
if (itemRect.right > nextRect.left + 0.5) {
|
|
302
|
+
issues.push({
|
|
303
|
+
label,
|
|
304
|
+
reason: `cell overlaps next cell by ${(itemRect.right - nextRect.left).toFixed(2)}px`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (item.scrollWidth > item.clientWidth + 1) {
|
|
310
|
+
issues.push({
|
|
311
|
+
label,
|
|
312
|
+
reason: `cell scrollWidth ${item.scrollWidth}px exceeds clientWidth ${item.clientWidth}px`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!textRect) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const leftPadding = textRect.left - itemRect.left;
|
|
321
|
+
const rightPadding = itemRect.right - textRect.right;
|
|
322
|
+
|
|
323
|
+
if (leftPadding < minPadding) {
|
|
324
|
+
issues.push({
|
|
325
|
+
label,
|
|
326
|
+
reason: `left text padding ${leftPadding.toFixed(2)}px is below ${minPadding}px`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (rightPadding < minPadding) {
|
|
331
|
+
issues.push({
|
|
332
|
+
label,
|
|
333
|
+
reason: `right text padding ${rightPadding.toFixed(2)}px is below ${minPadding}px`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return issues;
|
|
339
|
+
},
|
|
340
|
+
minHorizontalPaddingPx,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
expect(
|
|
344
|
+
issues,
|
|
345
|
+
`Toolcraft segmented control "${label}" must preserve cell padding and avoid label collisions.`,
|
|
346
|
+
).toEqual([]);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function dragToolcraftSliderByLabel(
|
|
350
|
+
page: Page,
|
|
351
|
+
label: string,
|
|
352
|
+
targetRatio: number,
|
|
353
|
+
): Promise<void> {
|
|
354
|
+
const field = await getToolcraftFieldByLabel(page, label);
|
|
355
|
+
const slider = field.locator('[data-slot="slider"], [role="slider"]').first();
|
|
356
|
+
|
|
357
|
+
await expect(slider, `Toolcraft slider "${label}" should be visible`).toBeVisible();
|
|
358
|
+
|
|
359
|
+
const box = await slider.boundingBox();
|
|
360
|
+
if (!box) {
|
|
361
|
+
throw new Error(`Could not measure slider "${label}".`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const startX = box.x + box.width * 0.15;
|
|
365
|
+
const endX = box.x + box.width * targetRatio;
|
|
366
|
+
const y = box.y + box.height / 2;
|
|
367
|
+
|
|
368
|
+
await page.mouse.move(startX, y);
|
|
369
|
+
await page.mouse.down();
|
|
370
|
+
await page.mouse.move(endX, y, { steps: 12 });
|
|
371
|
+
await page.mouse.up();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export async function dragToolcraftCanvasViewport(
|
|
375
|
+
page: Page,
|
|
376
|
+
delta: { x: number; y: number } = { x: 96, y: -64 },
|
|
377
|
+
): Promise<void> {
|
|
378
|
+
const viewport = page.getByRole("application", { name: "Canvas viewport" });
|
|
379
|
+
await expect(viewport, "Toolcraft canvas viewport should be visible").toBeVisible();
|
|
380
|
+
|
|
381
|
+
const box = await viewport.boundingBox();
|
|
382
|
+
if (!box) {
|
|
383
|
+
throw new Error("Could not measure Toolcraft canvas viewport.");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const startX = box.x + box.width * 0.5;
|
|
387
|
+
const startY = box.y + box.height * 0.5;
|
|
388
|
+
|
|
389
|
+
await page.mouse.move(startX, startY);
|
|
390
|
+
await page.mouse.down();
|
|
391
|
+
await page.mouse.move(startX + delta.x, startY + delta.y, { steps: 16 });
|
|
392
|
+
await page.mouse.up();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function zoomToolcraftCanvasViewport(
|
|
396
|
+
page: Page,
|
|
397
|
+
repetitions = 2,
|
|
398
|
+
): Promise<void> {
|
|
399
|
+
const zoomIn = page.getByRole("button", { name: "Zoom in" });
|
|
400
|
+
const zoomOut = page.getByRole("button", { name: "Zoom out" });
|
|
401
|
+
|
|
402
|
+
await expect(zoomIn, "Toolcraft zoom-in control should be visible").toBeVisible();
|
|
403
|
+
await expect(zoomOut, "Toolcraft zoom-out control should be visible").toBeVisible();
|
|
404
|
+
|
|
405
|
+
for (let index = 0; index < repetitions; index += 1) {
|
|
406
|
+
await zoomIn.click();
|
|
407
|
+
await waitForToolcraftAnimationFrames(page, 2);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
for (let index = 0; index < repetitions; index += 1) {
|
|
411
|
+
await zoomOut.click();
|
|
412
|
+
await waitForToolcraftAnimationFrames(page, 2);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export async function expectToolcraftDiscreteSliderDragSmoothness(
|
|
417
|
+
page: Page,
|
|
418
|
+
label: string,
|
|
419
|
+
options: ToolcraftInteractionOptions & {
|
|
420
|
+
expectMarkers?: boolean;
|
|
421
|
+
maxFrameGapMs?: number;
|
|
422
|
+
maxInteractionMs?: number;
|
|
423
|
+
} = {},
|
|
424
|
+
): Promise<ToolcraftInteractionResult> {
|
|
425
|
+
const field = await getToolcraftFieldByLabel(page, label);
|
|
426
|
+
const slider = field.locator('[data-slot="slider"][data-variant="discrete"]').first();
|
|
427
|
+
|
|
428
|
+
await expect(
|
|
429
|
+
slider,
|
|
430
|
+
`Toolcraft discrete slider "${label}" should render the discrete variant.`,
|
|
431
|
+
).toBeVisible();
|
|
432
|
+
|
|
433
|
+
const markers = field.locator('[data-slot="slider-marker"]');
|
|
434
|
+
if (options.expectMarkers === false) {
|
|
435
|
+
await expect(
|
|
436
|
+
markers,
|
|
437
|
+
`Toolcraft half-width discrete slider "${label}" should hide over-budget tick markers.`,
|
|
438
|
+
).toHaveCount(0);
|
|
439
|
+
} else {
|
|
440
|
+
await expect(
|
|
441
|
+
markers.first(),
|
|
442
|
+
`Toolcraft discrete slider "${label}" should render tick markers.`,
|
|
443
|
+
).toBeVisible();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const result = await measureToolcraftInteraction(
|
|
447
|
+
page,
|
|
448
|
+
async () => {
|
|
449
|
+
await dragToolcraftSliderByLabel(page, label, 0.85);
|
|
450
|
+
},
|
|
451
|
+
options,
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
expectToolcraftPerformanceBudget(result, {
|
|
455
|
+
maxFrameGapMs: options.maxFrameGapMs ?? 80,
|
|
456
|
+
maxInteractionMs: options.maxInteractionMs ?? 500,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
return result;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export async function readToolcraftCanvasViewport(page: Page): Promise<{
|
|
463
|
+
offsetX: number;
|
|
464
|
+
offsetY: number;
|
|
465
|
+
zoom: number;
|
|
466
|
+
}> {
|
|
467
|
+
return page.evaluate(() => {
|
|
468
|
+
const canvas = document.querySelector("[data-toolcraft-editable-canvas]");
|
|
469
|
+
const style = canvas ? window.getComputedStyle(canvas) : null;
|
|
470
|
+
const zoomText =
|
|
471
|
+
canvas?.getAttribute("data-canvas-zoom") ??
|
|
472
|
+
style?.getPropertyValue("--canvas-zoom") ??
|
|
473
|
+
"1";
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
offsetX: Number(canvas?.getAttribute("data-canvas-offset-x") ?? 0),
|
|
477
|
+
offsetY: Number(canvas?.getAttribute("data-canvas-offset-y") ?? 0),
|
|
478
|
+
zoom: Number.parseFloat(zoomText) || 1,
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export async function expectToolcraftCanvasViewportStable(
|
|
484
|
+
page: Page,
|
|
485
|
+
action: () => Promise<void>,
|
|
486
|
+
options: ToolcraftInteractionOptions & {
|
|
487
|
+
maxOffsetDelta?: number;
|
|
488
|
+
maxZoomDelta?: number;
|
|
489
|
+
} = {},
|
|
490
|
+
): Promise<ToolcraftInteractionResult> {
|
|
491
|
+
const before = await readToolcraftCanvasViewport(page);
|
|
492
|
+
const result = await measureToolcraftInteraction(page, action, options);
|
|
493
|
+
const after = await readToolcraftCanvasViewport(page);
|
|
494
|
+
const maxOffsetDelta = options.maxOffsetDelta ?? 0.5;
|
|
495
|
+
const maxZoomDelta = options.maxZoomDelta ?? 0.001;
|
|
496
|
+
|
|
497
|
+
expect(
|
|
498
|
+
Math.abs(after.offsetX - before.offsetX),
|
|
499
|
+
`Expected canvas offsetX to stay stable within ${maxOffsetDelta}px.`,
|
|
500
|
+
).toBeLessThanOrEqual(maxOffsetDelta);
|
|
501
|
+
expect(
|
|
502
|
+
Math.abs(after.offsetY - before.offsetY),
|
|
503
|
+
`Expected canvas offsetY to stay stable within ${maxOffsetDelta}px.`,
|
|
504
|
+
).toBeLessThanOrEqual(maxOffsetDelta);
|
|
505
|
+
expect(
|
|
506
|
+
Math.abs(after.zoom - before.zoom),
|
|
507
|
+
`Expected canvas zoom to stay stable within ${maxZoomDelta}.`,
|
|
508
|
+
).toBeLessThanOrEqual(maxZoomDelta);
|
|
509
|
+
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
type ToolcraftPerformanceBudgetResult = Partial<ToolcraftInteractionResult> & {
|
|
514
|
+
durationMs?: number;
|
|
515
|
+
exportMs?: number;
|
|
516
|
+
frameGapMs?: number;
|
|
517
|
+
previewMs?: number;
|
|
518
|
+
renderMs?: number;
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
export function getToolcraftPerformanceScenarioBudget(
|
|
522
|
+
config: ToolcraftPerformanceConfig,
|
|
523
|
+
scenarioId: string,
|
|
524
|
+
): ToolcraftPerformanceBudget {
|
|
525
|
+
const scenario = config.scenarios.find((item) => item.id === scenarioId);
|
|
526
|
+
|
|
527
|
+
if (!scenario) {
|
|
528
|
+
throw new Error(`Toolcraft performance scenario "${scenarioId}" was not found.`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return scenario.budget;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function getToolcraftPerformanceStressValue<TValue = unknown>(
|
|
535
|
+
config: ToolcraftPerformanceConfig,
|
|
536
|
+
scenarioId: string,
|
|
537
|
+
): TValue {
|
|
538
|
+
const scenario = config.scenarios.find((item) => item.id === scenarioId);
|
|
539
|
+
|
|
540
|
+
if (!scenario) {
|
|
541
|
+
throw new Error(`Toolcraft performance scenario "${scenarioId}" was not found.`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (
|
|
545
|
+
!scenario.stressFixture ||
|
|
546
|
+
!Object.prototype.hasOwnProperty.call(scenario.stressFixture, "value")
|
|
547
|
+
) {
|
|
548
|
+
throw new Error(
|
|
549
|
+
`Toolcraft performance scenario "${scenarioId}" does not declare stressFixture.value.`,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return scenario.stressFixture.value as TValue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function expectToolcraftScenarioPerformanceBudget(
|
|
557
|
+
result: ToolcraftPerformanceBudgetResult,
|
|
558
|
+
config: ToolcraftPerformanceConfig,
|
|
559
|
+
scenarioId: string,
|
|
560
|
+
): void {
|
|
561
|
+
expectToolcraftPerformanceBudget(
|
|
562
|
+
result,
|
|
563
|
+
getToolcraftPerformanceScenarioBudget(config, scenarioId),
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function expectToolcraftPerformanceBudget(
|
|
568
|
+
result: ToolcraftPerformanceBudgetResult,
|
|
569
|
+
budget: ToolcraftPerformanceBudget,
|
|
570
|
+
): void {
|
|
571
|
+
if (typeof budget.maxInteractionMs === "number") {
|
|
572
|
+
expect(
|
|
573
|
+
result.durationMs,
|
|
574
|
+
`Expected interaction duration to stay within ${budget.maxInteractionMs}ms.`,
|
|
575
|
+
).toBeLessThanOrEqual(budget.maxInteractionMs);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (typeof budget.maxFrameGapMs === "number") {
|
|
579
|
+
expect(
|
|
580
|
+
result.maxFrameGapMs ?? result.frameGapMs,
|
|
581
|
+
`Expected frame gaps to stay within ${budget.maxFrameGapMs}ms.`,
|
|
582
|
+
).toBeLessThanOrEqual(budget.maxFrameGapMs);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (typeof budget.maxLongTaskMs === "number") {
|
|
586
|
+
expect(
|
|
587
|
+
result.longTaskMaxMs,
|
|
588
|
+
`Expected long tasks to stay within ${budget.maxLongTaskMs}ms.`,
|
|
589
|
+
).toBeLessThanOrEqual(budget.maxLongTaskMs);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (typeof budget.maxExportMs === "number") {
|
|
593
|
+
expect(
|
|
594
|
+
result.exportMs ?? result.durationMs,
|
|
595
|
+
`Expected export/copy duration to stay within ${budget.maxExportMs}ms.`,
|
|
596
|
+
).toBeLessThanOrEqual(budget.maxExportMs);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (typeof budget.maxPreviewMs === "number") {
|
|
600
|
+
expect(
|
|
601
|
+
result.previewMs ?? result.durationMs,
|
|
602
|
+
`Expected preview duration to stay within ${budget.maxPreviewMs}ms.`,
|
|
603
|
+
).toBeLessThanOrEqual(budget.maxPreviewMs);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (typeof budget.maxRenderMs === "number") {
|
|
607
|
+
expect(
|
|
608
|
+
result.renderMs ?? result.durationMs,
|
|
609
|
+
`Expected render duration to stay within ${budget.maxRenderMs}ms.`,
|
|
610
|
+
).toBeLessThanOrEqual(budget.maxRenderMs);
|
|
611
|
+
}
|
|
612
|
+
}
|