@pixel-point/toolcraft 0.0.8 → 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.
Files changed (88) hide show
  1. package/README.md +42 -9
  2. package/package.json +1 -1
  3. package/src/generate.mjs +42 -5
  4. package/src/generate.test.mjs +40 -0
  5. package/src/package-json.mjs +15 -0
  6. package/src/package-json.test.mjs +14 -1
  7. package/templates/runtime/contracts/component-contracts.test.ts +251 -47
  8. package/templates/runtime/contracts/component-contracts.ts +130 -57
  9. package/templates/runtime/contracts/decision-contracts.test.ts +38 -1
  10. package/templates/runtime/contracts/decision-contracts.ts +22 -9
  11. package/templates/runtime/export/export.test.ts +63 -0
  12. package/templates/runtime/export/export.ts +55 -0
  13. package/templates/runtime/index.ts +1 -0
  14. package/templates/runtime/react/canvas-shell.test.tsx +58 -4
  15. package/templates/runtime/react/canvas-shell.tsx +31 -9
  16. package/templates/runtime/react/controls-panel.test.tsx +916 -108
  17. package/templates/runtime/react/controls-panel.tsx +136 -30
  18. package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
  19. package/templates/runtime/react/settings-transfer.test.ts +4 -0
  20. package/templates/runtime/react/settings-transfer.ts +6 -1
  21. package/templates/runtime/react/timeline-panel.test.tsx +14 -0
  22. package/templates/runtime/react/timeline-panel.tsx +44 -7
  23. package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
  24. package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
  25. package/templates/runtime/react/toolcraft-app.tsx +56 -37
  26. package/templates/runtime/schema/define-toolcraft.test.ts +266 -170
  27. package/templates/runtime/schema/define-toolcraft.ts +140 -246
  28. package/templates/runtime/schema/runtime-targets.ts +21 -0
  29. package/templates/runtime/schema/types.ts +44 -0
  30. package/templates/runtime/state/create-template-state.test.ts +156 -0
  31. package/templates/runtime/state/create-template-state.ts +38 -8
  32. package/templates/runtime/state/media-defaults.ts +105 -0
  33. package/templates/runtime/state/persistence.test.ts +58 -0
  34. package/templates/runtime/state/persistence.ts +105 -1
  35. package/templates/runtime/state/reducer.test.ts +280 -4
  36. package/templates/runtime/state/reducer.ts +195 -9
  37. package/templates/runtime/state/timeline-loop.test.ts +71 -0
  38. package/templates/runtime/state/timeline-loop.ts +35 -0
  39. package/templates/runtime/state/types.ts +27 -0
  40. package/templates/runtime/testing/performance.test.ts +810 -21
  41. package/templates/runtime/testing/performance.ts +823 -53
  42. package/templates/starter/AGENTS.md +24 -18
  43. package/templates/starter/docs/toolcraft/README.md +8 -4
  44. package/templates/starter/docs/toolcraft/acceptance-testing.md +43 -10
  45. package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
  46. package/templates/starter/docs/toolcraft/assembly-workflow.md +55 -20
  47. package/templates/starter/docs/toolcraft/component-rules.md +75 -42
  48. package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
  49. package/templates/starter/docs/toolcraft/performance.md +37 -11
  50. package/templates/starter/docs/toolcraft/schema-reference.md +171 -41
  51. package/templates/starter/docs/toolcraft/workflow.md +5 -2
  52. package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
  53. package/templates/starter/e2e/app-performance.spec.ts +55 -2
  54. package/templates/starter/e2e/performance-helpers.ts +45 -0
  55. package/templates/starter/gitignore +1 -0
  56. package/templates/starter/index.html +1 -0
  57. package/templates/starter/package.json +3 -0
  58. package/templates/starter/playwright.config.ts +1 -1
  59. package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
  60. package/templates/starter/scripts/run-vite-on-free-port.mjs +114 -13
  61. package/templates/starter/scripts/toolcraft-port.mjs +280 -0
  62. package/templates/starter/scripts/toolcraft-port.test.mjs +207 -1
  63. package/templates/starter/src/app/starter-acceptance.test.ts +3412 -479
  64. package/templates/starter/src/app/starter-acceptance.ts +1453 -97
  65. package/templates/starter/src/app/starter-performance.test.ts +111 -7
  66. package/templates/starter/src/app/starter-performance.ts +5 -0
  67. package/templates/starter/src/app/starter-schema.test.ts +32 -7
  68. package/templates/starter/src/app/starter-schema.ts +6 -2
  69. package/templates/starter/vite.config.ts +58 -2
  70. package/templates/ui/components/control-layout/index.tsx +8 -3
  71. package/templates/ui/components/controls/actions/actions-control.tsx +56 -5
  72. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
  73. package/templates/ui/components/controls/color/index.ts +4 -1
  74. package/templates/ui/components/controls/color/palette-control.tsx +34 -4
  75. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
  76. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
  77. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +186 -14
  78. package/templates/ui/components/controls/file-drop/index.ts +6 -1
  79. package/templates/ui/components/controls/index.ts +4 -0
  80. package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
  81. package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
  82. package/templates/ui/components/controls/select/select-control.tsx +8 -25
  83. package/templates/ui/components/controls/slider/slider-value.ts +0 -1
  84. package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
  85. package/templates/ui/components/controls/vector/index.ts +1 -0
  86. package/templates/ui/components/controls/vector/vector-control.tsx +109 -12
  87. package/templates/ui/components/panel/panel-actions.tsx +1 -1
  88. package/templates/ui/components/panel/panel-section.tsx +29 -5
@@ -2,10 +2,12 @@ import type {
2
2
  ToolcraftLocalStoragePersistenceSchema,
3
3
  ResolvedToolcraftAppSchema,
4
4
  } from "../schema/types";
5
+ import { isToolcraftTimelinePanelRuntimeTarget } from "../schema/runtime-targets";
5
6
  import type {
6
7
  ToolcraftCanvasState,
7
8
  ToolcraftInitialState,
8
9
  ToolcraftLayer,
10
+ ToolcraftMediaAsset,
9
11
  ToolcraftPanelId,
10
12
  ToolcraftPanelState,
11
13
  ToolcraftState,
@@ -92,6 +94,14 @@ function readPanel(value: unknown): Partial<ToolcraftPanelState> | undefined {
92
94
  panel.collapsed = value.collapsed;
93
95
  }
94
96
 
97
+ if (typeof value.extended === "boolean") {
98
+ panel.extended = value.extended;
99
+ }
100
+
101
+ if (typeof value.hidden === "boolean") {
102
+ panel.hidden = value.hidden;
103
+ }
104
+
95
105
  return Object.keys(panel).length > 0 ? panel : undefined;
96
106
  }
97
107
 
@@ -161,6 +171,85 @@ function readLayers(value: unknown): ToolcraftLayer[] | undefined {
161
171
  });
162
172
  }
163
173
 
174
+ function readMediaAsset(value: unknown): ToolcraftMediaAsset | undefined {
175
+ if (
176
+ !isRecord(value) ||
177
+ typeof value.id !== "string" ||
178
+ typeof value.layerId !== "string" ||
179
+ typeof value.dataUrl !== "string" ||
180
+ typeof value.fileName !== "string" ||
181
+ typeof value.mimeType !== "string"
182
+ ) {
183
+ return undefined;
184
+ }
185
+
186
+ const position = readPoint(value.position);
187
+
188
+ if (!position) {
189
+ return undefined;
190
+ }
191
+
192
+ const mediaAsset: ToolcraftMediaAsset = {
193
+ dataUrl: value.dataUrl,
194
+ fileName: value.fileName,
195
+ id: value.id,
196
+ layerId: value.layerId,
197
+ mimeType: value.mimeType,
198
+ position,
199
+ };
200
+ const size = readCanvasSize(value.size);
201
+
202
+ if (value.assetKind === "file" || value.assetKind === "image") {
203
+ mediaAsset.assetKind = value.assetKind;
204
+ }
205
+
206
+ if (size) {
207
+ mediaAsset.size = size;
208
+ }
209
+
210
+ if (typeof value.sourceTarget === "string") {
211
+ mediaAsset.sourceTarget = value.sourceTarget;
212
+ }
213
+
214
+ if (isRecord(value.transform)) {
215
+ mediaAsset.transform = {};
216
+
217
+ if (typeof value.transform.flipHorizontal === "boolean") {
218
+ mediaAsset.transform.flipHorizontal = value.transform.flipHorizontal;
219
+ }
220
+
221
+ if (typeof value.transform.flipVertical === "boolean") {
222
+ mediaAsset.transform.flipVertical = value.transform.flipVertical;
223
+ }
224
+
225
+ if (
226
+ value.transform.rotationDeg === 0 ||
227
+ value.transform.rotationDeg === 90 ||
228
+ value.transform.rotationDeg === 180 ||
229
+ value.transform.rotationDeg === 270
230
+ ) {
231
+ mediaAsset.transform.rotationDeg = value.transform.rotationDeg;
232
+ }
233
+
234
+ if (Object.keys(mediaAsset.transform).length === 0) {
235
+ delete mediaAsset.transform;
236
+ }
237
+ }
238
+
239
+ return mediaAsset;
240
+ }
241
+
242
+ function readMediaAssets(value: unknown): ToolcraftMediaAsset[] | undefined {
243
+ if (!Array.isArray(value)) {
244
+ return undefined;
245
+ }
246
+
247
+ return value.flatMap((item) => {
248
+ const mediaAsset = readMediaAsset(item);
249
+ return mediaAsset ? [mediaAsset] : [];
250
+ });
251
+ }
252
+
164
253
  function readBezierControlPoints(
165
254
  value: unknown,
166
255
  ): ToolcraftTimelineBezierControlPoints | undefined {
@@ -296,7 +385,10 @@ function getKnownValueTargets(schema: ResolvedToolcraftAppSchema): Set<string> {
296
385
 
297
386
  for (const section of schema.panels.controls?.sections ?? []) {
298
387
  for (const control of Object.values(section.controls)) {
299
- if (control.type !== "panelActions") {
388
+ if (
389
+ control.type !== "panelActions" &&
390
+ !isToolcraftTimelinePanelRuntimeTarget(control.target)
391
+ ) {
300
392
  targets.add(control.target);
301
393
  }
302
394
  }
@@ -369,6 +461,10 @@ export function createToolcraftPersistenceSnapshot(
369
461
  initialState.selectedLayerId = state.selectedLayerId;
370
462
  }
371
463
 
464
+ if (included.has("media")) {
465
+ initialState.mediaAssets = state.mediaAssets;
466
+ }
467
+
372
468
  return {
373
469
  state: initialState,
374
470
  version: persistence.version,
@@ -450,6 +546,14 @@ export function parseToolcraftPersistenceSnapshot(
450
546
  }
451
547
  }
452
548
 
549
+ if (included.has("media")) {
550
+ const mediaAssets = readMediaAssets(persistedState.mediaAssets);
551
+
552
+ if (mediaAssets) {
553
+ initialState.mediaAssets = mediaAssets;
554
+ }
555
+ }
556
+
453
557
  return Object.keys(initialState).length > 0 ? initialState : undefined;
454
558
  }
455
559
 
@@ -1,10 +1,11 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
3
  import { defineToolcraft } from "../schema/define-toolcraft";
4
+ import type { ToolcraftInitialState } from "./types";
4
5
  import { createToolcraftState } from "./create-template-state";
5
6
  import { toolcraftReducer } from "./reducer";
6
7
 
7
- function createState() {
8
+ function createState(initialState?: ToolcraftInitialState) {
8
9
  const app = defineToolcraft({
9
10
  canvas: {
10
11
  enabled: true,
@@ -28,7 +29,7 @@ function createState() {
28
29
  },
29
30
  });
30
31
 
31
- return createToolcraftState(app);
32
+ return createToolcraftState(app, initialState);
32
33
  }
33
34
 
34
35
  describe("toolcraftReducer", () => {
@@ -147,6 +148,70 @@ describe("toolcraftReducer", () => {
147
148
  });
148
149
  });
149
150
 
151
+ it("restores predefined fileDrop media on global and section reset", () => {
152
+ const app = defineToolcraft({
153
+ canvas: { enabled: true, upload: true },
154
+ media: {
155
+ defaultAssets: [
156
+ {
157
+ dataUrl: "data:image/png;base64,AAAA",
158
+ fileName: "default-background.png",
159
+ id: "default-background",
160
+ layerId: "default-background-layer",
161
+ mimeType: "image/png",
162
+ size: { height: 1080, unit: "px", width: 1920 },
163
+ sourceTarget: "media.background",
164
+ },
165
+ ],
166
+ },
167
+ panels: {
168
+ controls: {
169
+ sections: [
170
+ {
171
+ controls: {
172
+ background: {
173
+ defaultValue: null,
174
+ target: "media.background",
175
+ type: "fileDrop",
176
+ },
177
+ },
178
+ title: "Background",
179
+ },
180
+ ],
181
+ title: "Controls",
182
+ },
183
+ },
184
+ });
185
+ const initial = createToolcraftState(app);
186
+ const deleted = toolcraftReducer(initial, {
187
+ mediaId: "default-background",
188
+ type: "media.delete",
189
+ });
190
+ const sectionReset = toolcraftReducer(deleted, {
191
+ label: "Reset Background section",
192
+ targets: ["media.background"],
193
+ type: "controls.resetTargets",
194
+ });
195
+ const deletedAgain = toolcraftReducer(sectionReset, {
196
+ mediaId: "default-background",
197
+ type: "media.delete",
198
+ });
199
+ const globalReset = toolcraftReducer(deletedAgain, { type: "controls.reset" });
200
+
201
+ expect(initial.mediaAssets.map((asset) => asset.id)).toEqual(["default-background"]);
202
+ expect(deleted.mediaAssets).toEqual([]);
203
+ expect(sectionReset.mediaAssets.map((asset) => asset.id)).toEqual([
204
+ "default-background",
205
+ ]);
206
+ expect(sectionReset.layers.map((layer) => layer.id)).toEqual([
207
+ "default-background-layer",
208
+ ]);
209
+ expect(sectionReset.selectedLayerId).toBe("default-background-layer");
210
+ expect(globalReset.mediaAssets.map((asset) => asset.id)).toEqual([
211
+ "default-background",
212
+ ]);
213
+ });
214
+
150
215
  it("stores arbitrary fileDrop media without image dimensions and clears it on reset", () => {
151
216
  const app = defineToolcraft({
152
217
  canvas: { enabled: true, upload: true },
@@ -194,6 +259,64 @@ describe("toolcraftReducer", () => {
194
259
  expect(state.mediaAssets).toEqual([]);
195
260
  });
196
261
 
262
+ it("records image media rotate and flip transforms in runtime history", () => {
263
+ const imported = toolcraftReducer(createState(), {
264
+ asset: {
265
+ assetKind: "image",
266
+ dataUrl: "data:image/png;base64,test",
267
+ fileName: "source.png",
268
+ mimeType: "image/png",
269
+ position: { x: 0, y: 0 },
270
+ size: { height: 768, unit: "px", width: 1024 },
271
+ },
272
+ type: "media.import",
273
+ });
274
+
275
+ const rotated = toolcraftReducer(imported, {
276
+ mediaId: imported.mediaAssets[0]?.id ?? "",
277
+ operation: "rotate-right",
278
+ type: "media.transform",
279
+ });
280
+ const flipped = toolcraftReducer(rotated, {
281
+ mediaId: imported.mediaAssets[0]?.id ?? "",
282
+ operation: "flip-horizontal",
283
+ type: "media.transform",
284
+ });
285
+ const undone = toolcraftReducer(flipped, { type: "history.undo" });
286
+
287
+ expect(rotated.mediaAssets[0]?.transform).toEqual({ rotationDeg: 90 });
288
+ expect(flipped.mediaAssets[0]?.transform).toEqual({
289
+ flipHorizontal: true,
290
+ rotationDeg: 90,
291
+ });
292
+ expect(flipped.history.undo.at(-1)).toMatchObject({
293
+ before: { mediaAssets: rotated.mediaAssets },
294
+ label: "Transform media",
295
+ });
296
+ expect(undone.mediaAssets[0]?.transform).toEqual({ rotationDeg: 90 });
297
+ });
298
+
299
+ it("ignores image transforms for arbitrary file media", () => {
300
+ const imported = toolcraftReducer(createState(), {
301
+ asset: {
302
+ assetKind: "file",
303
+ dataUrl: "data:text/plain;base64,aGVsbG8=",
304
+ fileName: "notes.txt",
305
+ mimeType: "text/plain",
306
+ position: { x: 0, y: 0 },
307
+ },
308
+ type: "media.import",
309
+ });
310
+
311
+ const transformed = toolcraftReducer(imported, {
312
+ mediaId: imported.mediaAssets[0]?.id ?? "",
313
+ operation: "rotate-right",
314
+ type: "media.transform",
315
+ });
316
+
317
+ expect(transformed).toBe(imported);
318
+ });
319
+
197
320
  it("clears only selected fileDrop media on section reset", () => {
198
321
  const app = defineToolcraft({
199
322
  canvas: { enabled: true, upload: true },
@@ -554,6 +677,52 @@ describe("toolcraftReducer", () => {
554
677
  expect(next.values).toBe(state.values);
555
678
  });
556
679
 
680
+ it("stores timeline extended presentation outside control values", () => {
681
+ const state = createState();
682
+
683
+ const extended = toolcraftReducer(state, {
684
+ target: "panels.timeline.extended",
685
+ type: "controls.setValue",
686
+ value: true,
687
+ });
688
+
689
+ expect(extended.panels.timeline.extended).toBe(true);
690
+ expect(extended.values).toBe(state.values);
691
+ expect(extended.history).toBe(state.history);
692
+
693
+ const compact = toolcraftReducer(extended, {
694
+ target: "panels.timeline.extended",
695
+ type: "controls.setValue",
696
+ value: false,
697
+ });
698
+
699
+ expect(compact.panels.timeline.extended).toBe(false);
700
+ expect(compact.values).toBe(state.values);
701
+ });
702
+
703
+ it("keeps legacy timeline panel visibility outside control values", () => {
704
+ const state = createState();
705
+
706
+ const hidden = toolcraftReducer(state, {
707
+ target: "panels.timeline.visible",
708
+ type: "controls.setValue",
709
+ value: false,
710
+ });
711
+
712
+ expect(hidden.panels.timeline.hidden).toBe(true);
713
+ expect(hidden.values).toBe(state.values);
714
+ expect(hidden.history).toBe(state.history);
715
+
716
+ const shown = toolcraftReducer(hidden, {
717
+ hidden: false,
718
+ panelId: "timeline",
719
+ type: "panels.setHidden",
720
+ });
721
+
722
+ expect(shown.panels.timeline.hidden).toBe(false);
723
+ expect(shown.values).toBe(state.values);
724
+ });
725
+
557
726
  it("resets a panel offset to its default position", () => {
558
727
  const moved = toolcraftReducer(createState(), {
559
728
  offset: { x: 40, y: 80 },
@@ -630,9 +799,85 @@ describe("toolcraftReducer", () => {
630
799
  expect(next.history.undo.at(-1)?.label).toBe("Delete media");
631
800
  });
632
801
 
633
- it("sizes intrinsic media apps from the imported image and replaces single-layer media", () => {
802
+ it("keeps the current editable-output canvas size when importing background/source images", () => {
634
803
  const app = defineToolcraft({
635
- canvas: { enabled: true, upload: true },
804
+ canvas: {
805
+ enabled: true,
806
+ size: { height: 720, unit: "px", width: 1280 },
807
+ upload: true,
808
+ },
809
+ panels: {},
810
+ });
811
+ const state = createToolcraftState(app);
812
+ const imported = toolcraftReducer(state, {
813
+ asset: {
814
+ dataUrl: "data:image/png;base64,wide",
815
+ fileName: "wide-source.png",
816
+ mimeType: "image/png",
817
+ position: { x: 42, y: 24 },
818
+ size: { height: 1000, unit: "px", width: 4000 },
819
+ },
820
+ type: "media.import",
821
+ });
822
+
823
+ expect(imported.schema.canvas.sizing).toEqual({ mode: "editable-output" });
824
+ expect(imported.canvas.size).toEqual({ height: 720, unit: "px", width: 1280 });
825
+ expect(imported.mediaAssets[0]).toMatchObject({
826
+ fileName: "wide-source.png",
827
+ position: { x: 42, y: 24 },
828
+ size: { height: 1000, unit: "px", width: 4000 },
829
+ });
830
+ expect(imported.history.undo.at(-1)?.after).not.toHaveProperty("canvas.size");
831
+ });
832
+
833
+ it("keeps manually edited editable-output canvas size when importing source images", () => {
834
+ const app = defineToolcraft({
835
+ canvas: {
836
+ enabled: true,
837
+ upload: true,
838
+ },
839
+ panels: {
840
+ controls: {
841
+ sections: [],
842
+ title: "Controls",
843
+ },
844
+ },
845
+ });
846
+ const widthChanged = toolcraftReducer(createToolcraftState(app), {
847
+ target: "canvas.size.width",
848
+ type: "controls.setValue",
849
+ value: "1440",
850
+ });
851
+ const heightChanged = toolcraftReducer(widthChanged, {
852
+ target: "canvas.size.height",
853
+ type: "controls.setValue",
854
+ value: "900",
855
+ });
856
+ const imported = toolcraftReducer(heightChanged, {
857
+ asset: {
858
+ dataUrl: "data:image/png;base64,tall",
859
+ fileName: "tall-source.png",
860
+ mimeType: "image/png",
861
+ position: { x: 0, y: 0 },
862
+ size: { height: 4000, unit: "px", width: 1000 },
863
+ },
864
+ type: "media.import",
865
+ });
866
+
867
+ expect(imported.schema.canvas.sizing).toEqual({ mode: "editable-output" });
868
+ expect(imported.canvas.size).toEqual({ height: 900, unit: "px", width: 1440 });
869
+ expect(imported.values["canvas.size.width"]).toBe(1440);
870
+ expect(imported.values["canvas.size.height"]).toBe(900);
871
+ expect(imported.mediaAssets[0]).toMatchObject({
872
+ fileName: "tall-source.png",
873
+ size: { height: 4000, unit: "px", width: 1000 },
874
+ });
875
+ expect(imported.history.undo.at(-1)?.after).not.toHaveProperty("canvas.size");
876
+ });
877
+
878
+ it("sizes explicit intrinsic media apps from the imported image and replaces single-layer media", () => {
879
+ const app = defineToolcraft({
880
+ canvas: { enabled: true, sizing: { mode: "intrinsic-media" }, upload: true },
636
881
  panels: {},
637
882
  });
638
883
  const state = createToolcraftState(app);
@@ -1236,6 +1481,37 @@ describe("toolcraftReducer", () => {
1236
1481
  expect(scrubbed.history.undo).toHaveLength(0);
1237
1482
  });
1238
1483
 
1484
+ it("keeps loop state when timeline duration changes", () => {
1485
+ const state = toolcraftReducer(
1486
+ createState({
1487
+ timeline: {
1488
+ currentTimeSeconds: 9,
1489
+ durationSeconds: 10,
1490
+ isLooping: true,
1491
+ isPlaying: true,
1492
+ },
1493
+ }),
1494
+ {
1495
+ target: "selectedLayer.opacity",
1496
+ type: "controls.setValue",
1497
+ value: 42,
1498
+ },
1499
+ );
1500
+
1501
+ const resized = toolcraftReducer(state, {
1502
+ durationSeconds: 6,
1503
+ type: "timeline.setDuration",
1504
+ });
1505
+
1506
+ expect(resized.timeline.durationSeconds).toBe(6);
1507
+ expect(resized.timeline.currentTimeSeconds).toBe(6);
1508
+ expect(resized.timeline.isPlaying).toBe(true);
1509
+ expect(resized.timeline.isLooping).toBe(true);
1510
+ expect(resized.values).toEqual(state.values);
1511
+ expect(resized.values["selectedLayer.opacity"]).toBe(42);
1512
+ expect(resized.defaults).toEqual(state.defaults);
1513
+ });
1514
+
1239
1515
  it("restarts playback from the beginning when play is pressed at the non-looping end", () => {
1240
1516
  const paused = toolcraftReducer(createState(), { type: "timeline.togglePlayback" });
1241
1517
  const loopDisabled = toolcraftReducer(paused, { type: "timeline.toggleLoop" });