@pixel-point/toolcraft 0.0.9 → 0.0.11
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 +42 -9
- package/package.json +1 -1
- package/src/generate.mjs +14 -6
- package/src/generate.test.mjs +28 -0
- package/templates/runtime/contracts/component-contracts.test.ts +148 -34
- package/templates/runtime/contracts/component-contracts.ts +79 -38
- package/templates/runtime/contracts/decision-contracts.test.ts +33 -1
- package/templates/runtime/contracts/decision-contracts.ts +19 -6
- package/templates/runtime/export/export.test.ts +63 -0
- package/templates/runtime/export/export.ts +55 -0
- package/templates/runtime/index.ts +1 -0
- package/templates/runtime/react/canvas-shell.test.tsx +58 -4
- package/templates/runtime/react/canvas-shell.tsx +31 -9
- package/templates/runtime/react/controls-panel.test.tsx +474 -27
- package/templates/runtime/react/controls-panel.tsx +71 -26
- package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
- package/templates/runtime/react/settings-transfer.test.ts +4 -0
- package/templates/runtime/react/settings-transfer.ts +6 -1
- package/templates/runtime/react/timeline-panel.test.tsx +14 -0
- package/templates/runtime/react/timeline-panel.tsx +44 -7
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
- package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
- package/templates/runtime/react/toolcraft-app.tsx +56 -37
- package/templates/runtime/schema/define-toolcraft.test.ts +225 -173
- package/templates/runtime/schema/define-toolcraft.ts +117 -247
- package/templates/runtime/schema/runtime-targets.ts +21 -0
- package/templates/runtime/schema/types.ts +41 -0
- package/templates/runtime/state/create-template-state.test.ts +156 -0
- package/templates/runtime/state/create-template-state.ts +38 -8
- package/templates/runtime/state/media-defaults.ts +105 -0
- package/templates/runtime/state/persistence.test.ts +58 -0
- package/templates/runtime/state/persistence.ts +105 -1
- package/templates/runtime/state/reducer.test.ts +280 -4
- package/templates/runtime/state/reducer.ts +195 -9
- package/templates/runtime/state/timeline-loop.test.ts +71 -0
- package/templates/runtime/state/timeline-loop.ts +35 -0
- package/templates/runtime/state/types.ts +27 -0
- package/templates/runtime/testing/performance.test.ts +657 -2
- package/templates/runtime/testing/performance.ts +789 -49
- package/templates/starter/AGENTS.md +22 -16
- package/templates/starter/docs/toolcraft/README.md +8 -4
- package/templates/starter/docs/toolcraft/acceptance-testing.md +38 -7
- package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
- package/templates/starter/docs/toolcraft/assembly-workflow.md +51 -20
- package/templates/starter/docs/toolcraft/component-rules.md +49 -37
- package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
- package/templates/starter/docs/toolcraft/performance.md +30 -10
- package/templates/starter/docs/toolcraft/schema-reference.md +134 -33
- package/templates/starter/docs/toolcraft/workflow.md +5 -2
- package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
- package/templates/starter/e2e/app-performance.spec.ts +55 -2
- package/templates/starter/e2e/performance-helpers.ts +45 -0
- package/templates/starter/index.html +1 -0
- package/templates/starter/package.json +1 -0
- package/templates/starter/playwright.config.ts +1 -1
- package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +82 -16
- package/templates/starter/scripts/toolcraft-port.mjs +178 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +147 -0
- package/templates/starter/src/app/starter-acceptance.test.ts +2573 -313
- package/templates/starter/src/app/starter-acceptance.ts +978 -81
- package/templates/starter/src/app/starter-performance.test.ts +111 -7
- package/templates/starter/src/app/starter-performance.ts +5 -0
- package/templates/starter/src/app/starter-schema.test.ts +32 -7
- package/templates/starter/src/app/starter-schema.ts +6 -2
- package/templates/starter/vite.config.ts +58 -2
- package/templates/ui/components/controls/actions/actions-control.tsx +46 -3
- package/templates/ui/components/controls/color/palette-control.tsx +34 -4
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +186 -14
- package/templates/ui/components/controls/file-drop/index.ts +6 -1
- package/templates/ui/components/controls/index.ts +2 -0
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
- package/templates/ui/components/controls/select/select-control.tsx +4 -26
- package/templates/ui/components/controls/vector/vector-control.tsx +25 -4
- package/templates/ui/components/panel/panel-actions.tsx +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
1
|
+
import { act, cleanup, createEvent, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
2
|
import { FileDrop, Panel } from "@repo/ui";
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_COLOR_FORMAT_MODE,
|
|
@@ -54,18 +54,6 @@ function createSchema() {
|
|
|
54
54
|
sections: [
|
|
55
55
|
{
|
|
56
56
|
controls: {
|
|
57
|
-
canvasHeight: {
|
|
58
|
-
defaultValue: 180,
|
|
59
|
-
label: "Canvas height",
|
|
60
|
-
target: "canvas.size.height",
|
|
61
|
-
type: "text",
|
|
62
|
-
},
|
|
63
|
-
canvasWidth: {
|
|
64
|
-
defaultValue: 320,
|
|
65
|
-
label: "Canvas width",
|
|
66
|
-
target: "canvas.size.width",
|
|
67
|
-
type: "text",
|
|
68
|
-
},
|
|
69
57
|
prompt: {
|
|
70
58
|
defaultValue: "Initial prompt",
|
|
71
59
|
description: "Describe the generated result.",
|
|
@@ -109,13 +97,6 @@ function createSchema() {
|
|
|
109
97
|
type: "switch",
|
|
110
98
|
},
|
|
111
99
|
},
|
|
112
|
-
layoutGroups: [
|
|
113
|
-
{
|
|
114
|
-
columns: 2,
|
|
115
|
-
controls: ["canvasWidth", "canvasHeight"],
|
|
116
|
-
layout: "inline",
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
100
|
title: "Basic",
|
|
120
101
|
},
|
|
121
102
|
{
|
|
@@ -252,7 +233,19 @@ function StateProbe() {
|
|
|
252
233
|
<span data-testid="media-ids">
|
|
253
234
|
{state.mediaAssets.map((asset) => asset.id).join(",")}
|
|
254
235
|
</span>
|
|
236
|
+
<span data-testid="media-sizes">
|
|
237
|
+
{state.mediaAssets
|
|
238
|
+
.map((asset) =>
|
|
239
|
+
asset.size ? `${asset.size.width}x${asset.size.height}` : "none",
|
|
240
|
+
)
|
|
241
|
+
.join(",")}
|
|
242
|
+
</span>
|
|
243
|
+
<span data-testid="media-transforms">
|
|
244
|
+
{JSON.stringify(state.mediaAssets.map((asset) => asset.transform ?? null))}
|
|
245
|
+
</span>
|
|
255
246
|
<span data-testid="timeline-expanded">{String(state.timeline.expanded)}</span>
|
|
247
|
+
<span data-testid="timeline-panel-extended">{String(state.panels.timeline.extended)}</span>
|
|
248
|
+
<span data-testid="timeline-panel-hidden">{String(state.panels.timeline.hidden)}</span>
|
|
256
249
|
<span data-testid="timeline-keyframes">
|
|
257
250
|
{JSON.stringify(state.timeline.keyframeGroups)}
|
|
258
251
|
</span>
|
|
@@ -388,6 +381,42 @@ describe("ControlsPanel", () => {
|
|
|
388
381
|
expect(inlineGroup?.textContent).toContain("Snap Y");
|
|
389
382
|
});
|
|
390
383
|
|
|
384
|
+
it("commits palette family and shade changes to runtime state immediately", () => {
|
|
385
|
+
const schema = defineToolcraft({
|
|
386
|
+
canvas: { enabled: false },
|
|
387
|
+
panels: {
|
|
388
|
+
controls: {
|
|
389
|
+
sections: [
|
|
390
|
+
{
|
|
391
|
+
controls: {
|
|
392
|
+
inkPalette: {
|
|
393
|
+
defaultValue: { family: "Slate", shade: "900" },
|
|
394
|
+
label: "Palette",
|
|
395
|
+
target: "ink.palette",
|
|
396
|
+
type: "palette",
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
title: "Ink",
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
title: "Controls",
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
renderControlsPanelWithSchema(schema);
|
|
408
|
+
|
|
409
|
+
fireEvent.click(screen.getByRole("button", { name: "Primary family Red" }));
|
|
410
|
+
let values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
411
|
+
|
|
412
|
+
expect(values["ink.palette"]).toEqual({ family: "Red", shade: "900" });
|
|
413
|
+
|
|
414
|
+
fireEvent.click(screen.getByRole("button", { name: "Primary shade 500" }));
|
|
415
|
+
values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
416
|
+
|
|
417
|
+
expect(values["ink.palette"]).toEqual({ family: "Red", shade: "500" });
|
|
418
|
+
});
|
|
419
|
+
|
|
391
420
|
it("renders local action buttons as a two-column grid below their label", () => {
|
|
392
421
|
const schema = defineToolcraft({
|
|
393
422
|
canvas: { enabled: false },
|
|
@@ -717,6 +746,44 @@ describe("ControlsPanel", () => {
|
|
|
717
746
|
expect(container.querySelector('[data-slot="toggle-group"]')).toBeTruthy();
|
|
718
747
|
});
|
|
719
748
|
|
|
749
|
+
it("renders standalone select controls stacked full-width instead of compact side-label rows", () => {
|
|
750
|
+
const schema = defineToolcraft({
|
|
751
|
+
canvas: { enabled: false },
|
|
752
|
+
panels: {
|
|
753
|
+
controls: {
|
|
754
|
+
sections: [
|
|
755
|
+
{
|
|
756
|
+
controls: {
|
|
757
|
+
shape: {
|
|
758
|
+
defaultValue: "pill",
|
|
759
|
+
label: "Shape",
|
|
760
|
+
options: [
|
|
761
|
+
{ label: "Pill", value: "pill" },
|
|
762
|
+
{ label: "Circle", value: "circle" },
|
|
763
|
+
],
|
|
764
|
+
target: "glass.shape",
|
|
765
|
+
type: "select",
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
title: "Glass",
|
|
769
|
+
},
|
|
770
|
+
],
|
|
771
|
+
title: "Controls",
|
|
772
|
+
},
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
renderControlsPanelWithSchema(schema);
|
|
777
|
+
const trigger = screen.getByRole("combobox");
|
|
778
|
+
const field = trigger.closest<HTMLElement>('[data-slot="field"]');
|
|
779
|
+
|
|
780
|
+
expect(screen.getByText("Shape")).toBeTruthy();
|
|
781
|
+
expect(trigger.textContent).toContain("Pill");
|
|
782
|
+
expect(field?.dataset.orientation).toBe("vertical");
|
|
783
|
+
expect(field?.className).toContain("gap-2");
|
|
784
|
+
expect(trigger.closest("div")?.className).toContain("w-full");
|
|
785
|
+
});
|
|
786
|
+
|
|
720
787
|
it("renders compact select pairs inline with stacked full-width fields", () => {
|
|
721
788
|
const schema = defineToolcraft({
|
|
722
789
|
canvas: { enabled: false },
|
|
@@ -1691,6 +1758,67 @@ describe("ControlsPanel", () => {
|
|
|
1691
1758
|
expect(values["geometry.position"]).toEqual({ x: "-1.00", y: "-1.00" });
|
|
1692
1759
|
});
|
|
1693
1760
|
|
|
1761
|
+
it("resets a vector pad to its default value on double click", () => {
|
|
1762
|
+
const schema = defineToolcraft({
|
|
1763
|
+
canvas: { enabled: true },
|
|
1764
|
+
panels: {
|
|
1765
|
+
controls: {
|
|
1766
|
+
sections: [
|
|
1767
|
+
{
|
|
1768
|
+
controls: {
|
|
1769
|
+
position: {
|
|
1770
|
+
defaultValue: { x: "0.48", y: "-0.34" },
|
|
1771
|
+
label: "Position",
|
|
1772
|
+
target: "geometry.position",
|
|
1773
|
+
type: "vector",
|
|
1774
|
+
},
|
|
1775
|
+
},
|
|
1776
|
+
title: "Geometry",
|
|
1777
|
+
},
|
|
1778
|
+
],
|
|
1779
|
+
title: "Controls",
|
|
1780
|
+
},
|
|
1781
|
+
},
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
renderControlsPanelWithSchema(schema);
|
|
1785
|
+
const pad = screen.getByRole("button", { name: "Position X/Y pad" });
|
|
1786
|
+
|
|
1787
|
+
vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({
|
|
1788
|
+
bottom: 100,
|
|
1789
|
+
height: 100,
|
|
1790
|
+
left: 0,
|
|
1791
|
+
right: 100,
|
|
1792
|
+
toJSON: () => undefined,
|
|
1793
|
+
top: 0,
|
|
1794
|
+
width: 100,
|
|
1795
|
+
x: 0,
|
|
1796
|
+
y: 0,
|
|
1797
|
+
} as DOMRect);
|
|
1798
|
+
Object.defineProperty(pad, "setPointerCapture", {
|
|
1799
|
+
configurable: true,
|
|
1800
|
+
value: vi.fn(),
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
fireEvent.pointerDown(pad, {
|
|
1804
|
+
buttons: 1,
|
|
1805
|
+
clientX: 0,
|
|
1806
|
+
clientY: 0,
|
|
1807
|
+
pointerId: 1,
|
|
1808
|
+
});
|
|
1809
|
+
expect(
|
|
1810
|
+
JSON.parse(screen.getByTestId("values-json").textContent ?? "{}")[
|
|
1811
|
+
"geometry.position"
|
|
1812
|
+
],
|
|
1813
|
+
).toEqual({ x: "-1.00", y: "-1.00" });
|
|
1814
|
+
|
|
1815
|
+
fireEvent.doubleClick(pad);
|
|
1816
|
+
|
|
1817
|
+
const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
1818
|
+
expect(values["geometry.position"]).toEqual({ x: "0.48", y: "-0.34" });
|
|
1819
|
+
expect(screen.getByText("0.48, -0.34")).toBeTruthy();
|
|
1820
|
+
});
|
|
1821
|
+
|
|
1694
1822
|
it("locks vector pad movement to the dominant axis while shift is held", () => {
|
|
1695
1823
|
const schema = defineToolcraft({
|
|
1696
1824
|
canvas: { enabled: true },
|
|
@@ -1811,6 +1939,93 @@ describe("ControlsPanel", () => {
|
|
|
1811
1939
|
expect(values["geometry.position"]).toEqual({ x: "0.00", y: "0.80" });
|
|
1812
1940
|
});
|
|
1813
1941
|
|
|
1942
|
+
it("renders vector pad values as compact rounded coordinates", () => {
|
|
1943
|
+
const schema = defineToolcraft({
|
|
1944
|
+
canvas: { enabled: true },
|
|
1945
|
+
panels: {
|
|
1946
|
+
controls: {
|
|
1947
|
+
sections: [
|
|
1948
|
+
{
|
|
1949
|
+
controls: {
|
|
1950
|
+
center: {
|
|
1951
|
+
defaultValue: {
|
|
1952
|
+
x: "-0.07070312499999998",
|
|
1953
|
+
y: "-0.00392795138",
|
|
1954
|
+
},
|
|
1955
|
+
label: "Center",
|
|
1956
|
+
target: "geometry.center",
|
|
1957
|
+
type: "vector",
|
|
1958
|
+
},
|
|
1959
|
+
},
|
|
1960
|
+
title: "Geometry",
|
|
1961
|
+
},
|
|
1962
|
+
],
|
|
1963
|
+
title: "Controls",
|
|
1964
|
+
},
|
|
1965
|
+
},
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
renderControlsPanelWithSchema(schema);
|
|
1969
|
+
|
|
1970
|
+
expect(screen.getByText("-0.07, 0.00")).toBeTruthy();
|
|
1971
|
+
expect(screen.queryByText("-0.07070312499999998, -0.00392795138")).toBeNull();
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
it("prevents native text selection while dragging a vector pad", () => {
|
|
1975
|
+
const schema = defineToolcraft({
|
|
1976
|
+
canvas: { enabled: true },
|
|
1977
|
+
panels: {
|
|
1978
|
+
controls: {
|
|
1979
|
+
sections: [
|
|
1980
|
+
{
|
|
1981
|
+
controls: {
|
|
1982
|
+
center: {
|
|
1983
|
+
defaultValue: { x: "0.00", y: "0.00" },
|
|
1984
|
+
label: "Center",
|
|
1985
|
+
target: "geometry.center",
|
|
1986
|
+
type: "vector",
|
|
1987
|
+
},
|
|
1988
|
+
},
|
|
1989
|
+
title: "Geometry",
|
|
1990
|
+
},
|
|
1991
|
+
],
|
|
1992
|
+
title: "Controls",
|
|
1993
|
+
},
|
|
1994
|
+
},
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
renderControlsPanelWithSchema(schema);
|
|
1998
|
+
const pad = screen.getByRole("button", { name: "Center X/Y pad" });
|
|
1999
|
+
|
|
2000
|
+
vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({
|
|
2001
|
+
bottom: 100,
|
|
2002
|
+
height: 100,
|
|
2003
|
+
left: 0,
|
|
2004
|
+
right: 100,
|
|
2005
|
+
toJSON: () => undefined,
|
|
2006
|
+
top: 0,
|
|
2007
|
+
width: 100,
|
|
2008
|
+
x: 0,
|
|
2009
|
+
y: 0,
|
|
2010
|
+
} as DOMRect);
|
|
2011
|
+
Object.defineProperty(pad, "setPointerCapture", {
|
|
2012
|
+
configurable: true,
|
|
2013
|
+
value: vi.fn(),
|
|
2014
|
+
});
|
|
2015
|
+
|
|
2016
|
+
const pointerDown = createEvent.pointerDown(pad, {
|
|
2017
|
+
buttons: 1,
|
|
2018
|
+
clientX: 50,
|
|
2019
|
+
clientY: 50,
|
|
2020
|
+
pointerId: 1,
|
|
2021
|
+
});
|
|
2022
|
+
|
|
2023
|
+
fireEvent(pad, pointerDown);
|
|
2024
|
+
|
|
2025
|
+
expect(pointerDown.defaultPrevented).toBe(true);
|
|
2026
|
+
expect(pad.className).toContain("select-none");
|
|
2027
|
+
});
|
|
2028
|
+
|
|
1814
2029
|
it("allows cartesian vector pads when a product intentionally uses y-up math", () => {
|
|
1815
2030
|
const schema = defineToolcraft({
|
|
1816
2031
|
canvas: { enabled: true },
|
|
@@ -2567,9 +2782,12 @@ describe("ControlsPanel", () => {
|
|
|
2567
2782
|
|
|
2568
2783
|
for (const [draftValue, expectedValue, expectedLabel] of [
|
|
2569
2784
|
["0/1", '"shape.range":[0,1]', "0% – 1%"],
|
|
2785
|
+
["1-6", '"shape.range":[1,6]', "1% – 6%"],
|
|
2570
2786
|
["2-3", '"shape.range":[2,3]', "2% – 3%"],
|
|
2571
2787
|
["4 - 5", '"shape.range":[4,5]', "4% – 5%"],
|
|
2572
2788
|
["6–7", '"shape.range":[6,7]', "6% – 7%"],
|
|
2789
|
+
["7—8", '"shape.range":[7,8]', "7% – 8%"],
|
|
2790
|
+
["8−9", '"shape.range":[8,9]', "8% – 9%"],
|
|
2573
2791
|
] as const) {
|
|
2574
2792
|
fireEvent.click(screen.getByRole("button", { name: "Edit Range value" }));
|
|
2575
2793
|
const editor = screen.getByRole("textbox", { name: "Range value" });
|
|
@@ -2708,7 +2926,7 @@ describe("ControlsPanel", () => {
|
|
|
2708
2926
|
},
|
|
2709
2927
|
});
|
|
2710
2928
|
|
|
2711
|
-
renderControlsPanelWithSchema(schema, undefined, {
|
|
2929
|
+
const { container } = renderControlsPanelWithSchema(schema, undefined, {
|
|
2712
2930
|
layers: [
|
|
2713
2931
|
{
|
|
2714
2932
|
id: "layer-1",
|
|
@@ -2733,9 +2951,66 @@ describe("ControlsPanel", () => {
|
|
|
2733
2951
|
});
|
|
2734
2952
|
|
|
2735
2953
|
expect(screen.getByRole("img", { name: "material.svg" })).toBeTruthy();
|
|
2954
|
+
const transformActions = container.querySelector(
|
|
2955
|
+
'[data-slot="actions-control"][aria-label="Image transforms"]',
|
|
2956
|
+
);
|
|
2957
|
+
const transformActionButtons = transformActions?.querySelector(
|
|
2958
|
+
'[data-slot="actions-control-buttons"]',
|
|
2959
|
+
);
|
|
2960
|
+
|
|
2961
|
+
expect(transformActions).toBeTruthy();
|
|
2962
|
+
expect(transformActions?.parentElement?.style.gap).toBe("6px");
|
|
2963
|
+
expect(transformActionButtons?.className).toContain("grid-cols-3");
|
|
2964
|
+
expect(transformActionButtons?.getAttribute("data-actions-columns")).toBe("3");
|
|
2965
|
+
expect(screen.queryByRole("button", { name: "90° Left" })).toBeNull();
|
|
2966
|
+
|
|
2967
|
+
for (const [name, visibleLabel] of [
|
|
2968
|
+
["90° Right", "90°"],
|
|
2969
|
+
["Flip horizontal", "Flip H"],
|
|
2970
|
+
["Flip vertical", "Flip V"],
|
|
2971
|
+
] as const) {
|
|
2972
|
+
const button = screen.getByRole("button", { name });
|
|
2973
|
+
|
|
2974
|
+
expect(button).toBeTruthy();
|
|
2975
|
+
expect(button.textContent).toContain(visibleLabel);
|
|
2976
|
+
}
|
|
2736
2977
|
expect(
|
|
2737
2978
|
screen.queryByText("Click to upload an image"),
|
|
2738
2979
|
).toBeNull();
|
|
2980
|
+
const initialPreviewFrameStyle = container
|
|
2981
|
+
.querySelector('[data-slot="file-upload-preview-frame"]')
|
|
2982
|
+
?.getAttribute("style");
|
|
2983
|
+
|
|
2984
|
+
expect(initialPreviewFrameStyle).toBe(
|
|
2985
|
+
"aspect-ratio: 96 / 64; max-height: 196px; width: 100%;",
|
|
2986
|
+
);
|
|
2987
|
+
expect(screen.getByRole("img", { name: "material.svg" }).getAttribute("style")).toBe(
|
|
2988
|
+
"transform: translate(-50%, -50%);",
|
|
2989
|
+
);
|
|
2990
|
+
|
|
2991
|
+
fireEvent.click(screen.getByRole("button", { name: "90° Right" }));
|
|
2992
|
+
expect(screen.getByTestId("media-transforms").textContent).toBe(
|
|
2993
|
+
'[{"rotationDeg":90}]',
|
|
2994
|
+
);
|
|
2995
|
+
expect(
|
|
2996
|
+
container.querySelector('[data-slot="file-upload-preview-frame"]')?.getAttribute("style"),
|
|
2997
|
+
).toBe(initialPreviewFrameStyle);
|
|
2998
|
+
expect(screen.getByRole("img", { name: "material.svg" }).getAttribute("style")).toBe(
|
|
2999
|
+
"transform: translate(-50%, -50%) rotate(90deg); width: 66.6667%;",
|
|
3000
|
+
);
|
|
3001
|
+
|
|
3002
|
+
fireEvent.click(screen.getByRole("button", { name: "Flip horizontal" }));
|
|
3003
|
+
expect(screen.getByTestId("media-transforms").textContent).toBe(
|
|
3004
|
+
'[{"rotationDeg":90,"flipHorizontal":true}]',
|
|
3005
|
+
);
|
|
3006
|
+
expect(screen.getByRole("img", { name: "material.svg" }).getAttribute("style")).toBe(
|
|
3007
|
+
"transform: translate(-50%, -50%) rotate(90deg) scale(-1, 1); width: 66.6667%;",
|
|
3008
|
+
);
|
|
3009
|
+
|
|
3010
|
+
fireEvent.click(screen.getByRole("button", { name: "Flip vertical" }));
|
|
3011
|
+
expect(screen.getByTestId("media-transforms").textContent).toBe(
|
|
3012
|
+
'[{"rotationDeg":90,"flipHorizontal":true,"flipVertical":true}]',
|
|
3013
|
+
);
|
|
2739
3014
|
|
|
2740
3015
|
fireEvent.click(screen.getByRole("button", { name: "Remove image" }));
|
|
2741
3016
|
|
|
@@ -2744,6 +3019,113 @@ describe("ControlsPanel", () => {
|
|
|
2744
3019
|
expect(screen.getByText("Click to upload an image")).toBeTruthy();
|
|
2745
3020
|
});
|
|
2746
3021
|
|
|
3022
|
+
it("renders schema default media as a removable fileDrop attachment", () => {
|
|
3023
|
+
const schema = defineToolcraft({
|
|
3024
|
+
canvas: { enabled: true, upload: true },
|
|
3025
|
+
media: {
|
|
3026
|
+
defaultAssets: [
|
|
3027
|
+
{
|
|
3028
|
+
dataUrl:
|
|
3029
|
+
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='64' viewBox='0 0 96 64'%3E%3Crect width='96' height='64' fill='%23777'/%3E%3C/svg%3E",
|
|
3030
|
+
fileName: "default-background.svg",
|
|
3031
|
+
id: "default-background",
|
|
3032
|
+
layerId: "default-background-layer",
|
|
3033
|
+
mimeType: "image/svg+xml",
|
|
3034
|
+
size: { height: 64, unit: "px", width: 96 },
|
|
3035
|
+
sourceTarget: "input.source",
|
|
3036
|
+
},
|
|
3037
|
+
],
|
|
3038
|
+
},
|
|
3039
|
+
panels: {
|
|
3040
|
+
controls: {
|
|
3041
|
+
sections: [
|
|
3042
|
+
{
|
|
3043
|
+
controls: {
|
|
3044
|
+
source: {
|
|
3045
|
+
accept: "PNG, SVG",
|
|
3046
|
+
label: "Image",
|
|
3047
|
+
target: "input.source",
|
|
3048
|
+
type: "fileDrop",
|
|
3049
|
+
},
|
|
3050
|
+
},
|
|
3051
|
+
layout: "standalone",
|
|
3052
|
+
title: "Input",
|
|
3053
|
+
},
|
|
3054
|
+
],
|
|
3055
|
+
title: "Generation Controls",
|
|
3056
|
+
},
|
|
3057
|
+
},
|
|
3058
|
+
});
|
|
3059
|
+
|
|
3060
|
+
renderControlsPanelWithSchema(schema);
|
|
3061
|
+
|
|
3062
|
+
expect(screen.getByRole("img", { name: "default-background.svg" })).toBeTruthy();
|
|
3063
|
+
expect(screen.getByTestId("media-ids").textContent).toBe("default-background");
|
|
3064
|
+
|
|
3065
|
+
fireEvent.click(screen.getByRole("button", { name: "Remove image" }));
|
|
3066
|
+
|
|
3067
|
+
expect(screen.getByTestId("media-count").textContent).toBe("0");
|
|
3068
|
+
expect(screen.queryByRole("img", { name: "default-background.svg" })).toBeNull();
|
|
3069
|
+
expect(screen.getByText("Click to upload an image")).toBeTruthy();
|
|
3070
|
+
|
|
3071
|
+
fireEvent.click(screen.getByRole("button", { name: "Reset Input section" }));
|
|
3072
|
+
|
|
3073
|
+
expect(screen.getByRole("img", { name: "default-background.svg" })).toBeTruthy();
|
|
3074
|
+
expect(screen.getByTestId("media-count").textContent).toBe("1");
|
|
3075
|
+
|
|
3076
|
+
fireEvent.click(screen.getByRole("button", { name: "Remove image" }));
|
|
3077
|
+
fireEvent.click(screen.getByRole("button", { name: "Reset controls" }));
|
|
3078
|
+
|
|
3079
|
+
expect(screen.getByRole("img", { name: "default-background.svg" })).toBeTruthy();
|
|
3080
|
+
expect(screen.getByTestId("media-count").textContent).toBe("1");
|
|
3081
|
+
});
|
|
3082
|
+
|
|
3083
|
+
it("keeps the current editable canvas size when uploading an image through fileDrop", async () => {
|
|
3084
|
+
const schema = defineToolcraft({
|
|
3085
|
+
canvas: {
|
|
3086
|
+
enabled: true,
|
|
3087
|
+
size: { height: 720, unit: "px", width: 1280 },
|
|
3088
|
+
upload: true,
|
|
3089
|
+
},
|
|
3090
|
+
panels: {
|
|
3091
|
+
controls: {
|
|
3092
|
+
sections: [
|
|
3093
|
+
{
|
|
3094
|
+
controls: {
|
|
3095
|
+
source: {
|
|
3096
|
+
label: "Image",
|
|
3097
|
+
target: "input.source",
|
|
3098
|
+
type: "fileDrop",
|
|
3099
|
+
},
|
|
3100
|
+
},
|
|
3101
|
+
layout: "standalone",
|
|
3102
|
+
title: "Input",
|
|
3103
|
+
},
|
|
3104
|
+
],
|
|
3105
|
+
title: "Generation Controls",
|
|
3106
|
+
},
|
|
3107
|
+
},
|
|
3108
|
+
});
|
|
3109
|
+
const { container } = renderControlsPanelWithSchema(schema);
|
|
3110
|
+
const input = container.querySelector<HTMLInputElement>('input[type="file"]');
|
|
3111
|
+
|
|
3112
|
+
expect(input).toBeTruthy();
|
|
3113
|
+
expect(screen.getByTestId("canvas-size").textContent).toBe("1280,720");
|
|
3114
|
+
|
|
3115
|
+
fireEvent.change(input as HTMLInputElement, {
|
|
3116
|
+
target: {
|
|
3117
|
+
files: [new File(["image"], "wide.png", { type: "image/png" })],
|
|
3118
|
+
},
|
|
3119
|
+
});
|
|
3120
|
+
|
|
3121
|
+
await waitFor(() => {
|
|
3122
|
+
expect(screen.getByTestId("media-count").textContent).toBe("1");
|
|
3123
|
+
});
|
|
3124
|
+
|
|
3125
|
+
expect(screen.getByTestId("canvas-size").textContent).toBe("1280,720");
|
|
3126
|
+
expect(screen.getByTestId("media-sizes").textContent).toBe("1280x720");
|
|
3127
|
+
});
|
|
3128
|
+
|
|
2747
3129
|
it("removes single-layer file uploads from the canvas and returns the file control to empty", () => {
|
|
2748
3130
|
const schema = defineToolcraft({
|
|
2749
3131
|
canvas: { enabled: true, upload: true },
|
|
@@ -2878,6 +3260,7 @@ describe("ControlsPanel", () => {
|
|
|
2878
3260
|
expect(screen.getByRole("img", { name: "one.svg" })).toBeTruthy();
|
|
2879
3261
|
expect(screen.getByRole("img", { name: "two.svg" })).toBeTruthy();
|
|
2880
3262
|
expect(screen.getByTestId("media-ids").textContent).toBe("media-1,media-2");
|
|
3263
|
+
expect(screen.queryByRole("button", { name: "90° Left" })).toBeNull();
|
|
2881
3264
|
|
|
2882
3265
|
const firstPreview = screen
|
|
2883
3266
|
.getByRole("img", { name: "one.svg" })
|
|
@@ -2894,6 +3277,15 @@ describe("ControlsPanel", () => {
|
|
|
2894
3277
|
expect(firstPreview.getAttribute("tabindex")).toBe("0");
|
|
2895
3278
|
expect(firstPreview.className).toContain("cursor-grab");
|
|
2896
3279
|
|
|
3280
|
+
fireEvent.click(firstPreview);
|
|
3281
|
+
expect(firstPreview.getAttribute("data-selected")).toBe("true");
|
|
3282
|
+
expect(screen.getByRole("button", { name: "90° Right" })).toBeTruthy();
|
|
3283
|
+
|
|
3284
|
+
fireEvent.click(screen.getByRole("button", { name: "90° Right" }));
|
|
3285
|
+
expect(screen.getByTestId("media-transforms").textContent).toBe(
|
|
3286
|
+
'[{"rotationDeg":90},null]',
|
|
3287
|
+
);
|
|
3288
|
+
|
|
2897
3289
|
fireEvent.click(screen.getByRole("button", { name: "Remove two.svg" }));
|
|
2898
3290
|
|
|
2899
3291
|
expect(screen.getByTestId("media-count").textContent).toBe("1");
|
|
@@ -3863,7 +4255,10 @@ describe("ControlsPanel", () => {
|
|
|
3863
4255
|
fireEvent.change(promptInput, { target: { value: "Updated prompt" } });
|
|
3864
4256
|
expect(screen.getByTestId("prompt-value").textContent).toBe("Updated prompt");
|
|
3865
4257
|
|
|
3866
|
-
|
|
4258
|
+
const enabledField = screen.getByText("Enabled").closest('[role="group"]');
|
|
4259
|
+
const enabledSwitch = enabledField?.querySelector<HTMLElement>('[role="switch"]');
|
|
4260
|
+
|
|
4261
|
+
fireEvent.click(enabledSwitch as HTMLElement);
|
|
3867
4262
|
expect(screen.getByTestId("enabled-value").textContent).toBe("false");
|
|
3868
4263
|
|
|
3869
4264
|
fireEvent.click(screen.getByRole("button", { name: "Reset controls" }));
|
|
@@ -4313,6 +4708,54 @@ describe("ControlsPanel", () => {
|
|
|
4313
4708
|
expect(values["canvas.renderScale"]).toBe(2);
|
|
4314
4709
|
});
|
|
4315
4710
|
|
|
4711
|
+
it("renders timeline extended mode as runtime panel state instead of product values", () => {
|
|
4712
|
+
const schema = defineToolcraft({
|
|
4713
|
+
canvas: {
|
|
4714
|
+
enabled: true,
|
|
4715
|
+
renderScale: true,
|
|
4716
|
+
size: { height: 1080, unit: "px", width: 1920 },
|
|
4717
|
+
sizing: { mode: "editable-output" },
|
|
4718
|
+
},
|
|
4719
|
+
panels: {
|
|
4720
|
+
controls: {
|
|
4721
|
+
sections: [],
|
|
4722
|
+
title: "Controls",
|
|
4723
|
+
},
|
|
4724
|
+
timeline: { mode: "playback" },
|
|
4725
|
+
},
|
|
4726
|
+
});
|
|
4727
|
+
|
|
4728
|
+
renderControlsPanelWithSchema(schema, undefined, {
|
|
4729
|
+
timeline: { currentTimeSeconds: 2, expanded: true, isPlaying: true },
|
|
4730
|
+
});
|
|
4731
|
+
|
|
4732
|
+
const setupText = screen.getByText("Timeline").closest("section")?.textContent ?? "";
|
|
4733
|
+
|
|
4734
|
+
expect(setupText.indexOf("Resolution scale")).toBeLessThan(setupText.indexOf("Timeline"));
|
|
4735
|
+
const timelineField = screen.getByText("Timeline").closest('[role="group"]');
|
|
4736
|
+
const timelineSwitch = timelineField?.querySelector<HTMLElement>('[role="switch"]');
|
|
4737
|
+
|
|
4738
|
+
expect(timelineSwitch?.getAttribute("aria-checked")).toBe("false");
|
|
4739
|
+
expect(screen.getByTestId("timeline-panel-extended").textContent).toBe("undefined");
|
|
4740
|
+
expect(screen.getByTestId("timeline-panel-hidden").textContent).toBe("undefined");
|
|
4741
|
+
expect(screen.getByTestId("timeline-expanded").textContent).toBe("true");
|
|
4742
|
+
|
|
4743
|
+
fireEvent.click(timelineSwitch as HTMLElement);
|
|
4744
|
+
|
|
4745
|
+
expect(timelineSwitch?.getAttribute("aria-checked")).toBe("true");
|
|
4746
|
+
expect(screen.getByTestId("timeline-panel-extended").textContent).toBe("true");
|
|
4747
|
+
expect(screen.getByTestId("timeline-panel-hidden").textContent).toBe("undefined");
|
|
4748
|
+
expect(screen.getByTestId("timeline-expanded").textContent).toBe("true");
|
|
4749
|
+
expect(JSON.parse(screen.getByTestId("values-json").textContent ?? "{}")).not.toHaveProperty(
|
|
4750
|
+
"panels.timeline.extended",
|
|
4751
|
+
);
|
|
4752
|
+
|
|
4753
|
+
fireEvent.click(screen.getByRole("button", { name: "Reset controls" }));
|
|
4754
|
+
|
|
4755
|
+
expect(timelineSwitch?.getAttribute("aria-checked")).toBe("true");
|
|
4756
|
+
expect(screen.getByTestId("timeline-panel-extended").textContent).toBe("true");
|
|
4757
|
+
});
|
|
4758
|
+
|
|
4316
4759
|
it("applies empty text input values while typing and reset restores defaults", () => {
|
|
4317
4760
|
renderControlsPanel();
|
|
4318
4761
|
|
|
@@ -4549,7 +4992,7 @@ describe("ControlsPanel", () => {
|
|
|
4549
4992
|
expect(handledActions).toEqual(["download:Generation Controls"]);
|
|
4550
4993
|
});
|
|
4551
4994
|
|
|
4552
|
-
it("renders runtime settings transfer as a body section instead of sticky footer actions", () => {
|
|
4995
|
+
it("renders runtime settings transfer as a headerless body section instead of sticky footer actions", () => {
|
|
4553
4996
|
const schema = defineToolcraft({
|
|
4554
4997
|
canvas: { enabled: true, size: { height: 720, unit: "px", width: 1280 } },
|
|
4555
4998
|
panels: {
|
|
@@ -4613,10 +5056,12 @@ describe("ControlsPanel", () => {
|
|
|
4613
5056
|
).not.toContain("Setup");
|
|
4614
5057
|
expect(firstSection?.querySelector('[data-slot="panel-title"]')).toBeNull();
|
|
4615
5058
|
expect(firstSection?.querySelector("[data-control-section-collapse-button]")).toBeNull();
|
|
5059
|
+
expect(firstSection?.querySelector('[data-slot="control-section-header"]')).toBeNull();
|
|
5060
|
+
expect(firstSection?.querySelector('[data-slot="panel-section-collapsible-body"]')).toBeNull();
|
|
5061
|
+
expect(firstSection?.getAttribute("data-collapsed")).toBeNull();
|
|
4616
5062
|
expect(firstSection?.className).toContain("py-0");
|
|
4617
|
-
expect(firstSection?.querySelector("[data-control-list]")?.className).toContain("
|
|
4618
|
-
expect(firstSection?.querySelector("[data-control-list]")?.className).
|
|
4619
|
-
expect(firstSection?.querySelector("[data-control-list]")?.className).not.toContain("pb-5");
|
|
5063
|
+
expect(firstSection?.querySelector("[data-control-list]")?.className).toContain("pt-2");
|
|
5064
|
+
expect(firstSection?.querySelector("[data-control-list]")?.className).toContain("pb-6");
|
|
4620
5065
|
expect(firstInlineGroup?.textContent).toContain("Canvas width");
|
|
4621
5066
|
expect(firstInlineGroup?.textContent).toContain("Canvas height");
|
|
4622
5067
|
expect(firstSection?.textContent).toContain("Aspect ratio");
|
|
@@ -4858,10 +5303,12 @@ describe("ControlsPanel", () => {
|
|
|
4858
5303
|
const stickyFooter = container.querySelector(
|
|
4859
5304
|
'[data-slot="toolcraft-panel-sticky-actions"]',
|
|
4860
5305
|
);
|
|
5306
|
+
const exportPngButton = screen.getByRole("button", { name: "Export PNG" });
|
|
4861
5307
|
|
|
4862
5308
|
expect(stickyFooter?.getAttribute("data-sticky-footer-active")).toBeNull();
|
|
5309
|
+
expect(exportPngButton.querySelector('[data-icon-name="upload-simple"]')).toBeTruthy();
|
|
4863
5310
|
|
|
4864
|
-
fireEvent.click(
|
|
5311
|
+
fireEvent.click(exportPngButton);
|
|
4865
5312
|
|
|
4866
5313
|
expect(onPanelAction).toHaveBeenCalledTimes(1);
|
|
4867
5314
|
await waitFor(() => {
|