@pixel-point/toolcraft 0.0.9 → 0.0.12
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 +2 -1
- package/src/cli.mjs +4 -2
- package/src/cli.test.mjs +5 -2
- package/src/generate.mjs +14 -6
- package/src/generate.test.mjs +33 -2
- package/templates/runtime/contracts/component-contracts.test.ts +175 -36
- package/templates/runtime/contracts/component-contracts.ts +90 -45
- 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 +478 -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 +41 -8
- 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 +58 -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 +57 -4
- package/templates/starter/e2e/performance-helpers.ts +45 -0
- package/templates/starter/index.html +1 -0
- package/templates/starter/package.json +3 -2
- 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 +130 -8
- 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 +19 -5
- 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,16 @@ describe("ControlsPanel", () => {
|
|
|
2567
2782
|
|
|
2568
2783
|
for (const [draftValue, expectedValue, expectedLabel] of [
|
|
2569
2784
|
["0/1", '"shape.range":[0,1]', "0% – 1%"],
|
|
2785
|
+
["3/5", '"shape.range":[3,5]', "3% – 5%"],
|
|
2786
|
+
["1-6", '"shape.range":[1,6]', "1% – 6%"],
|
|
2570
2787
|
["2-3", '"shape.range":[2,3]', "2% – 3%"],
|
|
2571
2788
|
["4 - 5", '"shape.range":[4,5]', "4% – 5%"],
|
|
2789
|
+
["30%-150%", '"shape.range":[30,100]', "30% – 100%"],
|
|
2790
|
+
["30% - 90%", '"shape.range":[30,90]', "30% – 90%"],
|
|
2791
|
+
["30 % - 90 %", '"shape.range":[30,90]', "30% – 90%"],
|
|
2572
2792
|
["6–7", '"shape.range":[6,7]', "6% – 7%"],
|
|
2793
|
+
["7—8", '"shape.range":[7,8]', "7% – 8%"],
|
|
2794
|
+
["8−9", '"shape.range":[8,9]', "8% – 9%"],
|
|
2573
2795
|
] as const) {
|
|
2574
2796
|
fireEvent.click(screen.getByRole("button", { name: "Edit Range value" }));
|
|
2575
2797
|
const editor = screen.getByRole("textbox", { name: "Range value" });
|
|
@@ -2708,7 +2930,7 @@ describe("ControlsPanel", () => {
|
|
|
2708
2930
|
},
|
|
2709
2931
|
});
|
|
2710
2932
|
|
|
2711
|
-
renderControlsPanelWithSchema(schema, undefined, {
|
|
2933
|
+
const { container } = renderControlsPanelWithSchema(schema, undefined, {
|
|
2712
2934
|
layers: [
|
|
2713
2935
|
{
|
|
2714
2936
|
id: "layer-1",
|
|
@@ -2733,9 +2955,66 @@ describe("ControlsPanel", () => {
|
|
|
2733
2955
|
});
|
|
2734
2956
|
|
|
2735
2957
|
expect(screen.getByRole("img", { name: "material.svg" })).toBeTruthy();
|
|
2958
|
+
const transformActions = container.querySelector(
|
|
2959
|
+
'[data-slot="actions-control"][aria-label="Image transforms"]',
|
|
2960
|
+
);
|
|
2961
|
+
const transformActionButtons = transformActions?.querySelector(
|
|
2962
|
+
'[data-slot="actions-control-buttons"]',
|
|
2963
|
+
);
|
|
2964
|
+
|
|
2965
|
+
expect(transformActions).toBeTruthy();
|
|
2966
|
+
expect(transformActions?.parentElement?.style.gap).toBe("6px");
|
|
2967
|
+
expect(transformActionButtons?.className).toContain("grid-cols-3");
|
|
2968
|
+
expect(transformActionButtons?.getAttribute("data-actions-columns")).toBe("3");
|
|
2969
|
+
expect(screen.queryByRole("button", { name: "90° Left" })).toBeNull();
|
|
2970
|
+
|
|
2971
|
+
for (const [name, visibleLabel] of [
|
|
2972
|
+
["90° Right", "90°"],
|
|
2973
|
+
["Flip horizontal", "Flip H"],
|
|
2974
|
+
["Flip vertical", "Flip V"],
|
|
2975
|
+
] as const) {
|
|
2976
|
+
const button = screen.getByRole("button", { name });
|
|
2977
|
+
|
|
2978
|
+
expect(button).toBeTruthy();
|
|
2979
|
+
expect(button.textContent).toContain(visibleLabel);
|
|
2980
|
+
}
|
|
2736
2981
|
expect(
|
|
2737
2982
|
screen.queryByText("Click to upload an image"),
|
|
2738
2983
|
).toBeNull();
|
|
2984
|
+
const initialPreviewFrameStyle = container
|
|
2985
|
+
.querySelector('[data-slot="file-upload-preview-frame"]')
|
|
2986
|
+
?.getAttribute("style");
|
|
2987
|
+
|
|
2988
|
+
expect(initialPreviewFrameStyle).toBe(
|
|
2989
|
+
"aspect-ratio: 96 / 64; max-height: 196px; width: 100%;",
|
|
2990
|
+
);
|
|
2991
|
+
expect(screen.getByRole("img", { name: "material.svg" }).getAttribute("style")).toBe(
|
|
2992
|
+
"transform: translate(-50%, -50%);",
|
|
2993
|
+
);
|
|
2994
|
+
|
|
2995
|
+
fireEvent.click(screen.getByRole("button", { name: "90° Right" }));
|
|
2996
|
+
expect(screen.getByTestId("media-transforms").textContent).toBe(
|
|
2997
|
+
'[{"rotationDeg":90}]',
|
|
2998
|
+
);
|
|
2999
|
+
expect(
|
|
3000
|
+
container.querySelector('[data-slot="file-upload-preview-frame"]')?.getAttribute("style"),
|
|
3001
|
+
).toBe(initialPreviewFrameStyle);
|
|
3002
|
+
expect(screen.getByRole("img", { name: "material.svg" }).getAttribute("style")).toBe(
|
|
3003
|
+
"transform: translate(-50%, -50%) rotate(90deg); width: 66.6667%;",
|
|
3004
|
+
);
|
|
3005
|
+
|
|
3006
|
+
fireEvent.click(screen.getByRole("button", { name: "Flip horizontal" }));
|
|
3007
|
+
expect(screen.getByTestId("media-transforms").textContent).toBe(
|
|
3008
|
+
'[{"rotationDeg":90,"flipHorizontal":true}]',
|
|
3009
|
+
);
|
|
3010
|
+
expect(screen.getByRole("img", { name: "material.svg" }).getAttribute("style")).toBe(
|
|
3011
|
+
"transform: translate(-50%, -50%) rotate(90deg) scale(-1, 1); width: 66.6667%;",
|
|
3012
|
+
);
|
|
3013
|
+
|
|
3014
|
+
fireEvent.click(screen.getByRole("button", { name: "Flip vertical" }));
|
|
3015
|
+
expect(screen.getByTestId("media-transforms").textContent).toBe(
|
|
3016
|
+
'[{"rotationDeg":90,"flipHorizontal":true,"flipVertical":true}]',
|
|
3017
|
+
);
|
|
2739
3018
|
|
|
2740
3019
|
fireEvent.click(screen.getByRole("button", { name: "Remove image" }));
|
|
2741
3020
|
|
|
@@ -2744,6 +3023,113 @@ describe("ControlsPanel", () => {
|
|
|
2744
3023
|
expect(screen.getByText("Click to upload an image")).toBeTruthy();
|
|
2745
3024
|
});
|
|
2746
3025
|
|
|
3026
|
+
it("renders schema default media as a removable fileDrop attachment", () => {
|
|
3027
|
+
const schema = defineToolcraft({
|
|
3028
|
+
canvas: { enabled: true, upload: true },
|
|
3029
|
+
media: {
|
|
3030
|
+
defaultAssets: [
|
|
3031
|
+
{
|
|
3032
|
+
dataUrl:
|
|
3033
|
+
"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",
|
|
3034
|
+
fileName: "default-background.svg",
|
|
3035
|
+
id: "default-background",
|
|
3036
|
+
layerId: "default-background-layer",
|
|
3037
|
+
mimeType: "image/svg+xml",
|
|
3038
|
+
size: { height: 64, unit: "px", width: 96 },
|
|
3039
|
+
sourceTarget: "input.source",
|
|
3040
|
+
},
|
|
3041
|
+
],
|
|
3042
|
+
},
|
|
3043
|
+
panels: {
|
|
3044
|
+
controls: {
|
|
3045
|
+
sections: [
|
|
3046
|
+
{
|
|
3047
|
+
controls: {
|
|
3048
|
+
source: {
|
|
3049
|
+
accept: "PNG, SVG",
|
|
3050
|
+
label: "Image",
|
|
3051
|
+
target: "input.source",
|
|
3052
|
+
type: "fileDrop",
|
|
3053
|
+
},
|
|
3054
|
+
},
|
|
3055
|
+
layout: "standalone",
|
|
3056
|
+
title: "Input",
|
|
3057
|
+
},
|
|
3058
|
+
],
|
|
3059
|
+
title: "Generation Controls",
|
|
3060
|
+
},
|
|
3061
|
+
},
|
|
3062
|
+
});
|
|
3063
|
+
|
|
3064
|
+
renderControlsPanelWithSchema(schema);
|
|
3065
|
+
|
|
3066
|
+
expect(screen.getByRole("img", { name: "default-background.svg" })).toBeTruthy();
|
|
3067
|
+
expect(screen.getByTestId("media-ids").textContent).toBe("default-background");
|
|
3068
|
+
|
|
3069
|
+
fireEvent.click(screen.getByRole("button", { name: "Remove image" }));
|
|
3070
|
+
|
|
3071
|
+
expect(screen.getByTestId("media-count").textContent).toBe("0");
|
|
3072
|
+
expect(screen.queryByRole("img", { name: "default-background.svg" })).toBeNull();
|
|
3073
|
+
expect(screen.getByText("Click to upload an image")).toBeTruthy();
|
|
3074
|
+
|
|
3075
|
+
fireEvent.click(screen.getByRole("button", { name: "Reset Input section" }));
|
|
3076
|
+
|
|
3077
|
+
expect(screen.getByRole("img", { name: "default-background.svg" })).toBeTruthy();
|
|
3078
|
+
expect(screen.getByTestId("media-count").textContent).toBe("1");
|
|
3079
|
+
|
|
3080
|
+
fireEvent.click(screen.getByRole("button", { name: "Remove image" }));
|
|
3081
|
+
fireEvent.click(screen.getByRole("button", { name: "Reset controls" }));
|
|
3082
|
+
|
|
3083
|
+
expect(screen.getByRole("img", { name: "default-background.svg" })).toBeTruthy();
|
|
3084
|
+
expect(screen.getByTestId("media-count").textContent).toBe("1");
|
|
3085
|
+
});
|
|
3086
|
+
|
|
3087
|
+
it("keeps the current editable canvas size when uploading an image through fileDrop", async () => {
|
|
3088
|
+
const schema = defineToolcraft({
|
|
3089
|
+
canvas: {
|
|
3090
|
+
enabled: true,
|
|
3091
|
+
size: { height: 720, unit: "px", width: 1280 },
|
|
3092
|
+
upload: true,
|
|
3093
|
+
},
|
|
3094
|
+
panels: {
|
|
3095
|
+
controls: {
|
|
3096
|
+
sections: [
|
|
3097
|
+
{
|
|
3098
|
+
controls: {
|
|
3099
|
+
source: {
|
|
3100
|
+
label: "Image",
|
|
3101
|
+
target: "input.source",
|
|
3102
|
+
type: "fileDrop",
|
|
3103
|
+
},
|
|
3104
|
+
},
|
|
3105
|
+
layout: "standalone",
|
|
3106
|
+
title: "Input",
|
|
3107
|
+
},
|
|
3108
|
+
],
|
|
3109
|
+
title: "Generation Controls",
|
|
3110
|
+
},
|
|
3111
|
+
},
|
|
3112
|
+
});
|
|
3113
|
+
const { container } = renderControlsPanelWithSchema(schema);
|
|
3114
|
+
const input = container.querySelector<HTMLInputElement>('input[type="file"]');
|
|
3115
|
+
|
|
3116
|
+
expect(input).toBeTruthy();
|
|
3117
|
+
expect(screen.getByTestId("canvas-size").textContent).toBe("1280,720");
|
|
3118
|
+
|
|
3119
|
+
fireEvent.change(input as HTMLInputElement, {
|
|
3120
|
+
target: {
|
|
3121
|
+
files: [new File(["image"], "wide.png", { type: "image/png" })],
|
|
3122
|
+
},
|
|
3123
|
+
});
|
|
3124
|
+
|
|
3125
|
+
await waitFor(() => {
|
|
3126
|
+
expect(screen.getByTestId("media-count").textContent).toBe("1");
|
|
3127
|
+
});
|
|
3128
|
+
|
|
3129
|
+
expect(screen.getByTestId("canvas-size").textContent).toBe("1280,720");
|
|
3130
|
+
expect(screen.getByTestId("media-sizes").textContent).toBe("1280x720");
|
|
3131
|
+
});
|
|
3132
|
+
|
|
2747
3133
|
it("removes single-layer file uploads from the canvas and returns the file control to empty", () => {
|
|
2748
3134
|
const schema = defineToolcraft({
|
|
2749
3135
|
canvas: { enabled: true, upload: true },
|
|
@@ -2878,6 +3264,7 @@ describe("ControlsPanel", () => {
|
|
|
2878
3264
|
expect(screen.getByRole("img", { name: "one.svg" })).toBeTruthy();
|
|
2879
3265
|
expect(screen.getByRole("img", { name: "two.svg" })).toBeTruthy();
|
|
2880
3266
|
expect(screen.getByTestId("media-ids").textContent).toBe("media-1,media-2");
|
|
3267
|
+
expect(screen.queryByRole("button", { name: "90° Left" })).toBeNull();
|
|
2881
3268
|
|
|
2882
3269
|
const firstPreview = screen
|
|
2883
3270
|
.getByRole("img", { name: "one.svg" })
|
|
@@ -2894,6 +3281,15 @@ describe("ControlsPanel", () => {
|
|
|
2894
3281
|
expect(firstPreview.getAttribute("tabindex")).toBe("0");
|
|
2895
3282
|
expect(firstPreview.className).toContain("cursor-grab");
|
|
2896
3283
|
|
|
3284
|
+
fireEvent.click(firstPreview);
|
|
3285
|
+
expect(firstPreview.getAttribute("data-selected")).toBe("true");
|
|
3286
|
+
expect(screen.getByRole("button", { name: "90° Right" })).toBeTruthy();
|
|
3287
|
+
|
|
3288
|
+
fireEvent.click(screen.getByRole("button", { name: "90° Right" }));
|
|
3289
|
+
expect(screen.getByTestId("media-transforms").textContent).toBe(
|
|
3290
|
+
'[{"rotationDeg":90},null]',
|
|
3291
|
+
);
|
|
3292
|
+
|
|
2897
3293
|
fireEvent.click(screen.getByRole("button", { name: "Remove two.svg" }));
|
|
2898
3294
|
|
|
2899
3295
|
expect(screen.getByTestId("media-count").textContent).toBe("1");
|
|
@@ -3863,7 +4259,10 @@ describe("ControlsPanel", () => {
|
|
|
3863
4259
|
fireEvent.change(promptInput, { target: { value: "Updated prompt" } });
|
|
3864
4260
|
expect(screen.getByTestId("prompt-value").textContent).toBe("Updated prompt");
|
|
3865
4261
|
|
|
3866
|
-
|
|
4262
|
+
const enabledField = screen.getByText("Enabled").closest('[role="group"]');
|
|
4263
|
+
const enabledSwitch = enabledField?.querySelector<HTMLElement>('[role="switch"]');
|
|
4264
|
+
|
|
4265
|
+
fireEvent.click(enabledSwitch as HTMLElement);
|
|
3867
4266
|
expect(screen.getByTestId("enabled-value").textContent).toBe("false");
|
|
3868
4267
|
|
|
3869
4268
|
fireEvent.click(screen.getByRole("button", { name: "Reset controls" }));
|
|
@@ -4313,6 +4712,54 @@ describe("ControlsPanel", () => {
|
|
|
4313
4712
|
expect(values["canvas.renderScale"]).toBe(2);
|
|
4314
4713
|
});
|
|
4315
4714
|
|
|
4715
|
+
it("renders timeline extended mode as runtime panel state instead of product values", () => {
|
|
4716
|
+
const schema = defineToolcraft({
|
|
4717
|
+
canvas: {
|
|
4718
|
+
enabled: true,
|
|
4719
|
+
renderScale: true,
|
|
4720
|
+
size: { height: 1080, unit: "px", width: 1920 },
|
|
4721
|
+
sizing: { mode: "editable-output" },
|
|
4722
|
+
},
|
|
4723
|
+
panels: {
|
|
4724
|
+
controls: {
|
|
4725
|
+
sections: [],
|
|
4726
|
+
title: "Controls",
|
|
4727
|
+
},
|
|
4728
|
+
timeline: { mode: "playback" },
|
|
4729
|
+
},
|
|
4730
|
+
});
|
|
4731
|
+
|
|
4732
|
+
renderControlsPanelWithSchema(schema, undefined, {
|
|
4733
|
+
timeline: { currentTimeSeconds: 2, expanded: true, isPlaying: true },
|
|
4734
|
+
});
|
|
4735
|
+
|
|
4736
|
+
const setupText = screen.getByText("Timeline").closest("section")?.textContent ?? "";
|
|
4737
|
+
|
|
4738
|
+
expect(setupText.indexOf("Resolution scale")).toBeLessThan(setupText.indexOf("Timeline"));
|
|
4739
|
+
const timelineField = screen.getByText("Timeline").closest('[role="group"]');
|
|
4740
|
+
const timelineSwitch = timelineField?.querySelector<HTMLElement>('[role="switch"]');
|
|
4741
|
+
|
|
4742
|
+
expect(timelineSwitch?.getAttribute("aria-checked")).toBe("false");
|
|
4743
|
+
expect(screen.getByTestId("timeline-panel-extended").textContent).toBe("undefined");
|
|
4744
|
+
expect(screen.getByTestId("timeline-panel-hidden").textContent).toBe("undefined");
|
|
4745
|
+
expect(screen.getByTestId("timeline-expanded").textContent).toBe("true");
|
|
4746
|
+
|
|
4747
|
+
fireEvent.click(timelineSwitch as HTMLElement);
|
|
4748
|
+
|
|
4749
|
+
expect(timelineSwitch?.getAttribute("aria-checked")).toBe("true");
|
|
4750
|
+
expect(screen.getByTestId("timeline-panel-extended").textContent).toBe("true");
|
|
4751
|
+
expect(screen.getByTestId("timeline-panel-hidden").textContent).toBe("undefined");
|
|
4752
|
+
expect(screen.getByTestId("timeline-expanded").textContent).toBe("true");
|
|
4753
|
+
expect(JSON.parse(screen.getByTestId("values-json").textContent ?? "{}")).not.toHaveProperty(
|
|
4754
|
+
"panels.timeline.extended",
|
|
4755
|
+
);
|
|
4756
|
+
|
|
4757
|
+
fireEvent.click(screen.getByRole("button", { name: "Reset controls" }));
|
|
4758
|
+
|
|
4759
|
+
expect(timelineSwitch?.getAttribute("aria-checked")).toBe("true");
|
|
4760
|
+
expect(screen.getByTestId("timeline-panel-extended").textContent).toBe("true");
|
|
4761
|
+
});
|
|
4762
|
+
|
|
4316
4763
|
it("applies empty text input values while typing and reset restores defaults", () => {
|
|
4317
4764
|
renderControlsPanel();
|
|
4318
4765
|
|
|
@@ -4549,7 +4996,7 @@ describe("ControlsPanel", () => {
|
|
|
4549
4996
|
expect(handledActions).toEqual(["download:Generation Controls"]);
|
|
4550
4997
|
});
|
|
4551
4998
|
|
|
4552
|
-
it("renders runtime settings transfer as a body section instead of sticky footer actions", () => {
|
|
4999
|
+
it("renders runtime settings transfer as a headerless body section instead of sticky footer actions", () => {
|
|
4553
5000
|
const schema = defineToolcraft({
|
|
4554
5001
|
canvas: { enabled: true, size: { height: 720, unit: "px", width: 1280 } },
|
|
4555
5002
|
panels: {
|
|
@@ -4613,10 +5060,12 @@ describe("ControlsPanel", () => {
|
|
|
4613
5060
|
).not.toContain("Setup");
|
|
4614
5061
|
expect(firstSection?.querySelector('[data-slot="panel-title"]')).toBeNull();
|
|
4615
5062
|
expect(firstSection?.querySelector("[data-control-section-collapse-button]")).toBeNull();
|
|
5063
|
+
expect(firstSection?.querySelector('[data-slot="control-section-header"]')).toBeNull();
|
|
5064
|
+
expect(firstSection?.querySelector('[data-slot="panel-section-collapsible-body"]')).toBeNull();
|
|
5065
|
+
expect(firstSection?.getAttribute("data-collapsed")).toBeNull();
|
|
4616
5066
|
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");
|
|
5067
|
+
expect(firstSection?.querySelector("[data-control-list]")?.className).toContain("pt-2");
|
|
5068
|
+
expect(firstSection?.querySelector("[data-control-list]")?.className).toContain("pb-6");
|
|
4620
5069
|
expect(firstInlineGroup?.textContent).toContain("Canvas width");
|
|
4621
5070
|
expect(firstInlineGroup?.textContent).toContain("Canvas height");
|
|
4622
5071
|
expect(firstSection?.textContent).toContain("Aspect ratio");
|
|
@@ -4858,10 +5307,12 @@ describe("ControlsPanel", () => {
|
|
|
4858
5307
|
const stickyFooter = container.querySelector(
|
|
4859
5308
|
'[data-slot="toolcraft-panel-sticky-actions"]',
|
|
4860
5309
|
);
|
|
5310
|
+
const exportPngButton = screen.getByRole("button", { name: "Export PNG" });
|
|
4861
5311
|
|
|
4862
5312
|
expect(stickyFooter?.getAttribute("data-sticky-footer-active")).toBeNull();
|
|
5313
|
+
expect(exportPngButton.querySelector('[data-icon-name="upload-simple"]')).toBeTruthy();
|
|
4863
5314
|
|
|
4864
|
-
fireEvent.click(
|
|
5315
|
+
fireEvent.click(exportPngButton);
|
|
4865
5316
|
|
|
4866
5317
|
expect(onPanelAction).toHaveBeenCalledTimes(1);
|
|
4867
5318
|
await waitFor(() => {
|