@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,41 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test("browser: starter opens as a neutral Toolcraft shell", async ({ page }) => {
|
|
4
|
+
await page.goto("/");
|
|
5
|
+
|
|
6
|
+
await expect(page.locator('[data-slot="toolcraft-runtime-app"]')).toBeVisible();
|
|
7
|
+
await expect(page.getByRole("application", { name: "Canvas viewport" })).toBeVisible();
|
|
8
|
+
|
|
9
|
+
await expect(page.getByText("Toolcraft App Template Controls")).toHaveCount(0);
|
|
10
|
+
await expect(page.getByText("Generation")).toHaveCount(0);
|
|
11
|
+
await expect(page.getByText("Prompt")).toHaveCount(0);
|
|
12
|
+
await expect(page.getByText("Dur:")).toHaveCount(0);
|
|
13
|
+
await expect(page.getByRole("button", { name: /Play playback|Pause playback/ })).toHaveCount(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("browser: starter canvas accepts media upload without product controls", async ({
|
|
17
|
+
page,
|
|
18
|
+
}) => {
|
|
19
|
+
await page.goto("/");
|
|
20
|
+
|
|
21
|
+
const upload = await page.evaluateHandle(() => {
|
|
22
|
+
const dataTransfer = new DataTransfer();
|
|
23
|
+
const file = new File(
|
|
24
|
+
[
|
|
25
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="128" height="96"><rect width="128" height="96" fill="#888"/></svg>',
|
|
26
|
+
],
|
|
27
|
+
"starter-fixture.svg",
|
|
28
|
+
{ type: "image/svg+xml" },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
dataTransfer.items.add(file);
|
|
32
|
+
return dataTransfer;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await page
|
|
36
|
+
.getByRole("application", { name: "Canvas viewport" })
|
|
37
|
+
.dispatchEvent("drop", { dataTransfer: upload });
|
|
38
|
+
|
|
39
|
+
await expect(page.getByRole("img", { name: "starter-fixture.svg" })).toBeVisible();
|
|
40
|
+
await expect(page.getByText("Prompt")).toHaveCount(0);
|
|
41
|
+
});
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { expect, test } from "@playwright/test";
|
|
6
|
+
|
|
7
|
+
import { starterPerformance } from "../src/app/starter-performance";
|
|
8
|
+
import { starterSchema } from "../src/app/starter-schema";
|
|
9
|
+
|
|
10
|
+
const currentFileName = basename(fileURLToPath(import.meta.url));
|
|
11
|
+
const e2eDir = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
type BrowserTestSource = {
|
|
14
|
+
fileName: string;
|
|
15
|
+
source: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function escapeRegExp(value: string): string {
|
|
19
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stripJsComments(source: string): string {
|
|
23
|
+
return source
|
|
24
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
25
|
+
.replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hasRealLayerRowInteraction(source: string): boolean {
|
|
29
|
+
return /data-layer-id|data-template-layer-name|selectLayerByName\s*\(|layerRowByName\s*\(|getByRole\s*\(\s*(["'`])option\1/i.test(
|
|
30
|
+
source,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasRealLayerVisibilityInteraction(source: string): boolean {
|
|
35
|
+
return /toggleLayerVisibilityByName\s*\(|getByRole\s*\([\s\S]*?(Hide|Show|Disable|Enable).*layer|aria-label[\s\S]*?(Hide|Show|Disable|Enable)/i.test(
|
|
36
|
+
source,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hasRealLayerDragInteraction(source: string): boolean {
|
|
41
|
+
return /\.dragTo\s*\(|page\.mouse\.(?:down|move|up)\s*\(|dragLayer(?:Before|After|ToGroup|ByName)?\s*\(/i.test(
|
|
42
|
+
source,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readSiblingBrowserTestSources(): BrowserTestSource[] {
|
|
47
|
+
return readdirSync(e2eDir)
|
|
48
|
+
.filter((fileName) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(fileName))
|
|
49
|
+
.filter((fileName) => fileName !== currentFileName)
|
|
50
|
+
.map((fileName) => ({
|
|
51
|
+
fileName,
|
|
52
|
+
source: readFileSync(join(e2eDir, fileName), "utf8"),
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function findNamedBrowserTestSource(
|
|
57
|
+
sources: readonly BrowserTestSource[],
|
|
58
|
+
testName: string,
|
|
59
|
+
): string | undefined {
|
|
60
|
+
const testStartPattern = new RegExp(
|
|
61
|
+
`(?:test|it)(?:\\.[\\w]+)?\\(\\s*(["'\`])${escapeRegExp(testName)}\\1`,
|
|
62
|
+
);
|
|
63
|
+
const nextTestPattern = /\n\s*(?:test|it)(?:\.[\w]+)?\(\s*["'`]/;
|
|
64
|
+
|
|
65
|
+
for (const { source } of sources) {
|
|
66
|
+
const cleanSource = stripJsComments(source);
|
|
67
|
+
const match = testStartPattern.exec(cleanSource);
|
|
68
|
+
if (!match) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const startIndex = match.index;
|
|
73
|
+
const afterStart = cleanSource.slice(startIndex + 1);
|
|
74
|
+
const nextMatchIndex = afterStart.search(nextTestPattern);
|
|
75
|
+
|
|
76
|
+
return cleanSource.slice(
|
|
77
|
+
startIndex,
|
|
78
|
+
nextMatchIndex === -1 ? undefined : startIndex + 1 + nextMatchIndex,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getDiscreteControlLabels(): Set<string> {
|
|
86
|
+
const labels = new Set<string>();
|
|
87
|
+
|
|
88
|
+
for (const section of starterSchema.panels.controls?.sections ?? []) {
|
|
89
|
+
for (const control of Object.values(section.controls)) {
|
|
90
|
+
if (
|
|
91
|
+
(control.type === "slider" || control.type === "rangeSlider") &&
|
|
92
|
+
control.variant === "discrete" &&
|
|
93
|
+
typeof control.label === "string"
|
|
94
|
+
) {
|
|
95
|
+
labels.add(control.label);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return labels;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getScenarioBudgetAssertionPattern(scenarioId: string): RegExp {
|
|
104
|
+
return new RegExp(
|
|
105
|
+
`expectToolcraftScenarioPerformanceBudget\\s*\\([\\s\\S]*?,\\s*(?:starterPerformance|appPerformance)\\s*,\\s*(["'\`])${escapeRegExp(
|
|
106
|
+
scenarioId,
|
|
107
|
+
)}\\1`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getScenarioStressFixturePattern(scenarioId: string): RegExp {
|
|
112
|
+
return new RegExp(
|
|
113
|
+
`getToolcraftPerformanceStressValue(?:\\s*<[^>]+>)?\\s*\\(\\s*(?:starterPerformance|appPerformance)\\s*,\\s*(["'\`])${escapeRegExp(
|
|
114
|
+
scenarioId,
|
|
115
|
+
)}\\1`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
test("browser performance matrix points at real Playwright tests", () => {
|
|
120
|
+
const browserTestSources = readSiblingBrowserTestSources();
|
|
121
|
+
|
|
122
|
+
for (const scenario of starterPerformance.scenarios) {
|
|
123
|
+
if (!scenario.browser) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
expect(
|
|
128
|
+
Boolean(findNamedBrowserTestSource(browserTestSources, scenario.browserTestName)),
|
|
129
|
+
`${scenario.id} must be backed by a browser performance test named "${scenario.browserTestName}".`,
|
|
130
|
+
).toBe(true);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("browser performance tests use real Toolcraft interactions", () => {
|
|
135
|
+
if (starterPerformance.scenarios.length === 0) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const browserTestSources = readSiblingBrowserTestSources();
|
|
140
|
+
const discreteControlLabels = getDiscreteControlLabels();
|
|
141
|
+
|
|
142
|
+
for (const scenario of starterPerformance.scenarios) {
|
|
143
|
+
const browserTestSource = findNamedBrowserTestSource(
|
|
144
|
+
browserTestSources,
|
|
145
|
+
scenario.browserTestName,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(
|
|
149
|
+
browserTestSource,
|
|
150
|
+
`${scenario.id} must be backed by a browser performance test named "${scenario.browserTestName}".`,
|
|
151
|
+
).toBeDefined();
|
|
152
|
+
|
|
153
|
+
if (!browserTestSource) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (scenario.workload) {
|
|
158
|
+
expect(
|
|
159
|
+
scenario.stressFixture,
|
|
160
|
+
`${scenario.id} workload scenario must declare stressFixture so browser tests cannot use toy inputs.`,
|
|
161
|
+
).toBeDefined();
|
|
162
|
+
expect(
|
|
163
|
+
browserTestSource,
|
|
164
|
+
`${scenario.id} must read the declared stress fixture with getToolcraftPerformanceStressValue(appPerformance, "${scenario.id}") before measuring performance.`,
|
|
165
|
+
).toMatch(getScenarioStressFixturePattern(scenario.id));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (scenario.interaction === "control-drag") {
|
|
169
|
+
expect(
|
|
170
|
+
browserTestSource,
|
|
171
|
+
`${scenario.id} must measure a real Toolcraft pointer drag instead of filling a hidden/numeric editor.`,
|
|
172
|
+
).toMatch(/dragToolcraftSliderByLabel\s*\(/);
|
|
173
|
+
|
|
174
|
+
if (scenario.controlLabel) {
|
|
175
|
+
expect(
|
|
176
|
+
browserTestSource,
|
|
177
|
+
`${scenario.id} must drag the declared control label "${scenario.controlLabel}".`,
|
|
178
|
+
).toMatch(
|
|
179
|
+
new RegExp(
|
|
180
|
+
`dragToolcraftSliderByLabel\\s*\\([\\s\\S]*?(["'\`])${escapeRegExp(
|
|
181
|
+
scenario.controlLabel,
|
|
182
|
+
)}\\1`,
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (scenario.controlLabel && discreteControlLabels.has(scenario.controlLabel)) {
|
|
188
|
+
expect(
|
|
189
|
+
browserTestSource,
|
|
190
|
+
`${scenario.id} drags discrete slider "${scenario.controlLabel}" and must verify Toolcraft marker-budget behavior plus smooth drag.`,
|
|
191
|
+
).toMatch(/expectToolcraftDiscreteSliderDragSmoothness\s*\(/);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (scenario.interaction === "control-change") {
|
|
196
|
+
expect(
|
|
197
|
+
browserTestSource,
|
|
198
|
+
`${scenario.id} must change the declared control through the browser UI.`,
|
|
199
|
+
).toMatch(/\.fill\s*\(|\.pressSequentially\s*\(|\.selectOption\s*\(|getByRole\s*\([\s\S]*?\.click\s*\(/);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (scenario.uiSelector) {
|
|
203
|
+
expect(
|
|
204
|
+
browserTestSource,
|
|
205
|
+
`${scenario.id} must interact with the declared UI selector "${scenario.uiSelector}".`,
|
|
206
|
+
).toContain(scenario.uiSelector);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
expect(
|
|
210
|
+
browserTestSource,
|
|
211
|
+
`${scenario.id} must measure browser responsiveness with measureToolcraftInteraction.`,
|
|
212
|
+
).toMatch(
|
|
213
|
+
/measureToolcraftInteraction\s*\(|measureToolcraftAnimationFrames\s*\(|expectToolcraftCanvasViewportStable\s*\(/,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (scenario.interaction === "viewport-stability") {
|
|
217
|
+
expect(
|
|
218
|
+
browserTestSource,
|
|
219
|
+
`${scenario.id} must use expectToolcraftCanvasViewportStable to catch canvas zoom/offset jumps.`,
|
|
220
|
+
).toMatch(/expectToolcraftCanvasViewportStable\s*\(/);
|
|
221
|
+
|
|
222
|
+
if (scenario.target === "timeline.keyframes") {
|
|
223
|
+
expect(
|
|
224
|
+
browserTestSource,
|
|
225
|
+
`${scenario.id} must exercise toolbar zoom or radar before checking keyframe viewport stability.`,
|
|
226
|
+
).toMatch(/Zoom in|Zoom out|Center canvas|canvas\.zoom|canvas\.center/);
|
|
227
|
+
expect(
|
|
228
|
+
browserTestSource,
|
|
229
|
+
`${scenario.id} must open the expanded keyframe editor while checking viewport stability.`,
|
|
230
|
+
).toMatch(/Expand timeline panel|timeline\.setExpanded|timeline-expanded/);
|
|
231
|
+
expect(
|
|
232
|
+
browserTestSource,
|
|
233
|
+
`${scenario.id} must create or update at least one keyframe while checking viewport stability.`,
|
|
234
|
+
).toMatch(/Add .* keyframe|Disable .* keyframes|timeline-keyframe-row/);
|
|
235
|
+
expect(
|
|
236
|
+
browserTestSource,
|
|
237
|
+
`${scenario.id} must scrub or play the timeline while checking viewport stability.`,
|
|
238
|
+
).toMatch(/Playback position|Play playback|Pause playback|timeline\.setCurrentTime/);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (scenario.target === "layers.interactions") {
|
|
242
|
+
expect(
|
|
243
|
+
browserTestSource,
|
|
244
|
+
`${scenario.id} must exercise toolbar zoom or radar before checking layer viewport stability.`,
|
|
245
|
+
).toMatch(/Zoom in|Zoom out|Center canvas|canvas\.zoom|canvas\.center/);
|
|
246
|
+
expect(
|
|
247
|
+
hasRealLayerRowInteraction(browserTestSource),
|
|
248
|
+
`${scenario.id} must select real LayersPanel rows while checking viewport stability.`,
|
|
249
|
+
).toBe(true);
|
|
250
|
+
expect(
|
|
251
|
+
hasRealLayerVisibilityInteraction(browserTestSource),
|
|
252
|
+
`${scenario.id} must toggle real layer visibility while checking viewport stability.`,
|
|
253
|
+
).toBe(true);
|
|
254
|
+
expect(
|
|
255
|
+
hasRealLayerDragInteraction(browserTestSource),
|
|
256
|
+
`${scenario.id} must drag real layer rows for reorder or grouping while checking viewport stability.`,
|
|
257
|
+
).toBe(true);
|
|
258
|
+
expect(
|
|
259
|
+
browserTestSource,
|
|
260
|
+
`${scenario.id} must assert selected-layer or product output after layer interactions.`,
|
|
261
|
+
).toMatch(/selected layer|selectedLayer|product-output|rendered-pixels|canvas|export|output/i);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (scenario.interaction === "animation-frame") {
|
|
266
|
+
expect(
|
|
267
|
+
browserTestSource,
|
|
268
|
+
`${scenario.id} must use measureToolcraftAnimationFrames so animated renderers are sampled for at least 120 frames.`,
|
|
269
|
+
).toMatch(/measureToolcraftAnimationFrames\s*\(/);
|
|
270
|
+
expect(
|
|
271
|
+
browserTestSource,
|
|
272
|
+
`${scenario.id} must not satisfy animation coverage with a short waitForToolcraftAnimationFrames probe.`,
|
|
273
|
+
).not.toMatch(/waitForToolcraftAnimationFrames\s*\(\s*page\s*,\s*(?:[1-9]|[1-9]\d|1[01]\d)\s*\)/);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (scenario.interaction === "animation-viewport-drag") {
|
|
277
|
+
expect(
|
|
278
|
+
browserTestSource,
|
|
279
|
+
`${scenario.id} must measure a real canvas viewport drag while the renderer is animated.`,
|
|
280
|
+
).toMatch(/measureToolcraftInteraction\s*\([\s\S]*dragToolcraftCanvasViewport\s*\(/);
|
|
281
|
+
expect(
|
|
282
|
+
browserTestSource,
|
|
283
|
+
`${scenario.id} must not fake animated viewport coverage with toolbar zoom or direct canvas commands.`,
|
|
284
|
+
).not.toMatch(/canvas\.setOffset|canvas\.panBy|canvas\.zoom|Zoom in|Zoom out|Center canvas/);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (scenario.interaction === "viewport-zoom-stress") {
|
|
288
|
+
expect(
|
|
289
|
+
browserTestSource,
|
|
290
|
+
`${scenario.id} must measure real toolbar zoom while sampling frame gaps.`,
|
|
291
|
+
).toMatch(/measureToolcraftInteraction\s*\([\s\S]*zoomToolcraftCanvasViewport\s*\(/);
|
|
292
|
+
expect(
|
|
293
|
+
browserTestSource,
|
|
294
|
+
`${scenario.id} must not fake zoom stress with direct runtime canvas commands.`,
|
|
295
|
+
).not.toMatch(/canvas\.setZoom|canvas\.zoom|canvas\.setScale|canvas\.setOffset|canvas\.panBy/);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
expect(
|
|
299
|
+
browserTestSource,
|
|
300
|
+
`${scenario.id} must assert product or UI output after the measured interaction so a no-op control cannot pass.`,
|
|
301
|
+
).toMatch(/await\s+expect\s*\(|expect\s*\(/);
|
|
302
|
+
|
|
303
|
+
expect(
|
|
304
|
+
browserTestSource,
|
|
305
|
+
`${scenario.id} must assert its typed budget through expectToolcraftScenarioPerformanceBudget(..., appPerformance, "${scenario.id}") so browser thresholds cannot drift from app-performance.ts.`,
|
|
306
|
+
).toMatch(getScenarioBudgetAssertionPattern(scenario.id));
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("declared renderer layer selectors are present", async ({ page }) => {
|
|
311
|
+
if (!starterPerformance.usesCustomRenderer) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const visibleLayers =
|
|
316
|
+
starterPerformance.rendererTechnique?.layers?.filter((layer) => layer.uiSelector) ?? [];
|
|
317
|
+
|
|
318
|
+
await page.goto("/");
|
|
319
|
+
|
|
320
|
+
for (const layer of visibleLayers) {
|
|
321
|
+
await expect(
|
|
322
|
+
page.locator(layer.uiSelector!).first(),
|
|
323
|
+
`renderer layer "${layer.id}" should exist at ${layer.uiSelector}`,
|
|
324
|
+
).toBeVisible();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { expect, type Locator, type Page } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
export function getCanvasHandle(page: Page, testId: string): Locator {
|
|
4
|
+
return page.locator(`[data-toolcraft-canvas-handle][data-testid="${testId}"]`);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function dragCanvasHandle(
|
|
8
|
+
page: Page,
|
|
9
|
+
testId: string,
|
|
10
|
+
delta: { x: number; y: number },
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
const handle = getCanvasHandle(page, testId);
|
|
13
|
+
await expect(handle, `Canvas handle "${testId}" should be visible`).toBeVisible();
|
|
14
|
+
|
|
15
|
+
const box = await handle.boundingBox();
|
|
16
|
+
if (!box) {
|
|
17
|
+
throw new Error(`Could not measure canvas handle "${testId}".`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const startX = box.x + box.width / 2;
|
|
21
|
+
const startY = box.y + box.height / 2;
|
|
22
|
+
|
|
23
|
+
await page.mouse.move(startX, startY);
|
|
24
|
+
await page.mouse.down();
|
|
25
|
+
await page.mouse.move(startX + delta.x, startY + delta.y, { steps: 8 });
|
|
26
|
+
await page.mouse.up();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const defaultForbiddenCanvasCopyPatterns = [
|
|
30
|
+
/apply/i,
|
|
31
|
+
/choose file/i,
|
|
32
|
+
/click to upload/i,
|
|
33
|
+
/copy png/i,
|
|
34
|
+
/drag it onto the canvas/i,
|
|
35
|
+
/export png/i,
|
|
36
|
+
/generate/i,
|
|
37
|
+
/reset/i,
|
|
38
|
+
/settings/i,
|
|
39
|
+
/upload an image/i,
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
const productCanvasTextSelector =
|
|
43
|
+
"[data-toolcraft-product-output], [data-toolcraft-product-text]";
|
|
44
|
+
|
|
45
|
+
export type ToolcraftCanvasUiCheckOptions = {
|
|
46
|
+
allowedProductText?: readonly RegExp[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export async function expectNoForbiddenCanvasUi(
|
|
50
|
+
page: Page,
|
|
51
|
+
options: ToolcraftCanvasUiCheckOptions = {},
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
const canvasWorld = page.locator(
|
|
54
|
+
'[data-toolcraft-canvas-world], [data-toolcraft-editable-canvas]',
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
await expect(
|
|
58
|
+
canvasWorld.locator(
|
|
59
|
+
[
|
|
60
|
+
'button:not([data-toolcraft-canvas-handle])',
|
|
61
|
+
"input",
|
|
62
|
+
"textarea",
|
|
63
|
+
"select",
|
|
64
|
+
'[role="button"]:not([data-toolcraft-canvas-handle])',
|
|
65
|
+
'[role="menu"]',
|
|
66
|
+
'[role="dialog"]',
|
|
67
|
+
'[data-slot="button"]',
|
|
68
|
+
'[data-slot="input"]',
|
|
69
|
+
].join(", "),
|
|
70
|
+
),
|
|
71
|
+
"Canvas must not contain app UI controls; only product output and data-toolcraft-canvas-handle overlays are allowed.",
|
|
72
|
+
).toHaveCount(0);
|
|
73
|
+
|
|
74
|
+
const canvasTextMatches = await canvasWorld.evaluateAll(
|
|
75
|
+
(roots, { allowedSources, forbiddenSources, productTextSelector }) => {
|
|
76
|
+
const allowedPatterns = allowedSources.map((source) => new RegExp(source, "i"));
|
|
77
|
+
const forbiddenPatterns = forbiddenSources.map((source) => new RegExp(source, "i"));
|
|
78
|
+
const matches: { kind: "forbidden-copy" | "unclassified-text"; text: string }[] = [];
|
|
79
|
+
|
|
80
|
+
const getDirectText = (element: Element) =>
|
|
81
|
+
Array.from(element.childNodes)
|
|
82
|
+
.filter((node) => node.nodeType === Node.TEXT_NODE)
|
|
83
|
+
.map((node) => node.textContent?.trim() ?? "")
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.join(" ")
|
|
86
|
+
.trim();
|
|
87
|
+
|
|
88
|
+
for (const root of roots) {
|
|
89
|
+
const elements = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
90
|
+
|
|
91
|
+
for (const element of elements) {
|
|
92
|
+
if (element.closest("[data-toolcraft-canvas-handle]")) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const text = getDirectText(element);
|
|
97
|
+
|
|
98
|
+
if (!text) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (allowedPatterns.some((pattern) => pattern.test(text))) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (forbiddenPatterns.some((pattern) => pattern.test(text))) {
|
|
107
|
+
matches.push({ kind: "forbidden-copy", text });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!element.closest(productTextSelector)) {
|
|
112
|
+
matches.push({ kind: "unclassified-text", text });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return matches;
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
allowedSources: (options.allowedProductText ?? []).map((pattern) => pattern.source),
|
|
121
|
+
forbiddenSources: defaultForbiddenCanvasCopyPatterns.map((pattern) => pattern.source),
|
|
122
|
+
productTextSelector: productCanvasTextSelector,
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
expect(
|
|
127
|
+
canvasTextMatches,
|
|
128
|
+
"Canvas text must be product output marked with data-toolcraft-product-output/data-toolcraft-product-text; app UI copy, CTAs, helper text, upload prompts, export/copy/reset text, and settings labels are forbidden.",
|
|
129
|
+
).toEqual([]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function expectExportExcludesCanvasHandles(
|
|
133
|
+
page: Page,
|
|
134
|
+
exportAction: () => Promise<unknown>,
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
await exportAction();
|
|
137
|
+
|
|
138
|
+
const handles = page.locator("[data-toolcraft-canvas-handle]");
|
|
139
|
+
const count = await handles.count();
|
|
140
|
+
|
|
141
|
+
for (let index = 0; index < count; index += 1) {
|
|
142
|
+
const handle = handles.nth(index);
|
|
143
|
+
await expect(
|
|
144
|
+
handle,
|
|
145
|
+
"Canvas handles should remain editor overlays after export/copy and must not be duplicated into product output.",
|
|
146
|
+
).toHaveAttribute("data-toolcraft-canvas-handle");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function expectCanvasHandlesUseToolcraftVisualLanguage(page: Page): Promise<void> {
|
|
151
|
+
const handles = page.locator("[data-toolcraft-canvas-handle]");
|
|
152
|
+
const count = await handles.count();
|
|
153
|
+
|
|
154
|
+
for (let index = 0; index < count; index += 1) {
|
|
155
|
+
const handle = handles.nth(index);
|
|
156
|
+
|
|
157
|
+
await expect(handle, `Canvas handle ${index + 1} should be visible`).toBeVisible();
|
|
158
|
+
await expect(
|
|
159
|
+
handle.locator(
|
|
160
|
+
[
|
|
161
|
+
"button",
|
|
162
|
+
"input",
|
|
163
|
+
"textarea",
|
|
164
|
+
"select",
|
|
165
|
+
'[role="button"]',
|
|
166
|
+
'[role="menu"]',
|
|
167
|
+
'[role="dialog"]',
|
|
168
|
+
'[data-slot="button"]',
|
|
169
|
+
'[data-slot="input"]',
|
|
170
|
+
].join(", "),
|
|
171
|
+
),
|
|
172
|
+
"Canvas handles must not contain nested app UI controls.",
|
|
173
|
+
).toHaveCount(0);
|
|
174
|
+
|
|
175
|
+
const style = await handle.evaluate((node) => {
|
|
176
|
+
const element = node as HTMLElement;
|
|
177
|
+
const elements = [element, ...Array.from(element.querySelectorAll<HTMLElement>("*"))];
|
|
178
|
+
const toNumber = (value: string) => Number.parseFloat(value) || 0;
|
|
179
|
+
const svgStrokeWidths = Array.from(
|
|
180
|
+
element.querySelectorAll<SVGElement>(
|
|
181
|
+
"svg [stroke-width], svg line, svg path, svg circle, svg rect, svg polyline, svg polygon",
|
|
182
|
+
),
|
|
183
|
+
).map((svgElement) => {
|
|
184
|
+
const attributeValue = svgElement.getAttribute("stroke-width");
|
|
185
|
+
const computedValue = window.getComputedStyle(svgElement).strokeWidth;
|
|
186
|
+
return toNumber(attributeValue ?? computedValue);
|
|
187
|
+
});
|
|
188
|
+
const getMaxStrokeWidth = (property: "border" | "outline") =>
|
|
189
|
+
Math.max(
|
|
190
|
+
...elements.map((currentElement) => {
|
|
191
|
+
const computed = window.getComputedStyle(currentElement);
|
|
192
|
+
if (property === "outline") {
|
|
193
|
+
return toNumber(computed.outlineWidth);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return Math.max(
|
|
197
|
+
toNumber(computed.borderTopWidth),
|
|
198
|
+
toNumber(computed.borderRightWidth),
|
|
199
|
+
toNumber(computed.borderBottomWidth),
|
|
200
|
+
toNumber(computed.borderLeftWidth),
|
|
201
|
+
);
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
const rect = element.getBoundingClientRect();
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
borderWidth: getMaxStrokeWidth("border"),
|
|
208
|
+
height: rect.height,
|
|
209
|
+
outlineWidth: getMaxStrokeWidth("outline"),
|
|
210
|
+
role: element.getAttribute("role") ?? "",
|
|
211
|
+
svgStrokeWidth: Math.max(0, ...svgStrokeWidths),
|
|
212
|
+
tagName: element.tagName.toLowerCase(),
|
|
213
|
+
text: element.textContent?.trim() ?? "",
|
|
214
|
+
width: rect.width,
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(
|
|
219
|
+
["button", "input", "select", "textarea"],
|
|
220
|
+
"Canvas handle roots must be visual elements, not native app controls.",
|
|
221
|
+
).not.toContain(style.tagName);
|
|
222
|
+
expect(
|
|
223
|
+
["button", "menu", "dialog", "textbox", "slider", "spinbutton", "combobox", "listbox"],
|
|
224
|
+
"Canvas handle roots must not expose app-control roles.",
|
|
225
|
+
).not.toContain(style.role);
|
|
226
|
+
expect(style.text, "Canvas handles should be visual pins/lines, not text labels.").toBe("");
|
|
227
|
+
expect(
|
|
228
|
+
style.borderWidth,
|
|
229
|
+
"Canvas handle borders, including child elements, should stay close to Toolcraft control stroke weights.",
|
|
230
|
+
).toBeLessThanOrEqual(2);
|
|
231
|
+
expect(
|
|
232
|
+
style.outlineWidth,
|
|
233
|
+
"Canvas handle outlines, including child elements, should stay close to Toolcraft focus ring weights.",
|
|
234
|
+
).toBeLessThanOrEqual(2);
|
|
235
|
+
expect(
|
|
236
|
+
style.svgStrokeWidth,
|
|
237
|
+
"Canvas handle SVG strokes should stay close to Toolcraft control stroke weights.",
|
|
238
|
+
).toBeLessThanOrEqual(2);
|
|
239
|
+
expect(
|
|
240
|
+
style.width <= 96 || style.height <= 96,
|
|
241
|
+
"Canvas handles should be pins, lines, corners, or light geometry, not panel-sized controls.",
|
|
242
|
+
).toBe(true);
|
|
243
|
+
}
|
|
244
|
+
}
|