@pixel-point/toolcraft 0.0.7 → 0.0.9
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/package.json +1 -1
- package/src/generate.mjs +34 -5
- package/src/generate.test.mjs +12 -0
- package/src/package-json.mjs +15 -0
- package/src/package-json.test.mjs +14 -1
- package/templates/runtime/contracts/component-contracts.test.ts +185 -23
- package/templates/runtime/contracts/component-contracts.ts +91 -36
- package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
- package/templates/runtime/contracts/decision-contracts.ts +4 -4
- package/templates/runtime/export/export.test.ts +31 -0
- package/templates/runtime/export/export.ts +41 -0
- package/templates/runtime/react/canvas-shell.test.tsx +77 -1
- package/templates/runtime/react/canvas-shell.tsx +178 -23
- package/templates/runtime/react/control-conditions.ts +166 -0
- package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
- package/templates/runtime/react/controls-panel.test.tsx +774 -17
- package/templates/runtime/react/controls-panel.tsx +155 -8
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +46 -1
- package/templates/runtime/schema/define-toolcraft.ts +29 -3
- package/templates/runtime/schema/types.ts +7 -0
- package/templates/runtime/state/reducer.test.ts +304 -0
- package/templates/runtime/state/reducer.ts +148 -9
- package/templates/runtime/state/types.ts +10 -2
- package/templates/runtime/testing/performance.test.ts +1424 -56
- package/templates/runtime/testing/performance.ts +710 -43
- package/templates/starter/AGENTS.md +10 -9
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +16 -8
- package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -7
- package/templates/starter/docs/toolcraft/component-rules.md +46 -16
- package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
- package/templates/starter/docs/toolcraft/performance.md +54 -6
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +48 -19
- package/templates/starter/docs/toolcraft/workflow.md +2 -2
- package/templates/starter/e2e/app-performance.spec.ts +136 -3
- package/templates/starter/e2e/performance-helpers.ts +197 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/package.json +5 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
- package/templates/starter/scripts/toolcraft-port.mjs +102 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +1288 -105
- package/templates/starter/src/app/starter-acceptance.ts +740 -24
- package/templates/starter/src/app/starter-performance.test.ts +66 -5
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +10 -4
- package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
- package/templates/ui/components/controls/color/index.ts +4 -1
- package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
- package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +340 -44
- package/templates/ui/components/controls/file-drop/index.ts +1 -1
- package/templates/ui/components/controls/index.ts +3 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
- package/templates/ui/components/controls/select/select-control.tsx +9 -4
- package/templates/ui/components/controls/slider/slider-value.ts +0 -1
- package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
- package/templates/ui/components/controls/vector/index.ts +1 -0
- package/templates/ui/components/controls/vector/vector-control.tsx +84 -8
- package/templates/ui/components/panel/panel-section.tsx +82 -6
|
@@ -76,6 +76,34 @@ const ordinarySliderSchema = defineToolcraft({
|
|
|
76
76
|
},
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
const rangeSliderSchema = defineToolcraft({
|
|
80
|
+
canvas: {
|
|
81
|
+
enabled: true,
|
|
82
|
+
sizing: { mode: "intrinsic-media" },
|
|
83
|
+
},
|
|
84
|
+
panels: {
|
|
85
|
+
controls: {
|
|
86
|
+
sections: [
|
|
87
|
+
{
|
|
88
|
+
controls: {
|
|
89
|
+
band: {
|
|
90
|
+
defaultValue: [20, 80],
|
|
91
|
+
label: "Band",
|
|
92
|
+
max: 100,
|
|
93
|
+
min: 0,
|
|
94
|
+
performanceReason: "Band changes a lightweight bounded interval.",
|
|
95
|
+
performanceRole: "responsiveness",
|
|
96
|
+
target: "render.band",
|
|
97
|
+
type: "rangeSlider",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
title: "Runtime Controls",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
79
107
|
const largeTextSchema = defineToolcraft({
|
|
80
108
|
canvas: {
|
|
81
109
|
enabled: true,
|
|
@@ -102,6 +130,34 @@ const largeTextSchema = defineToolcraft({
|
|
|
102
130
|
},
|
|
103
131
|
});
|
|
104
132
|
|
|
133
|
+
const mediaUploadSchema = defineToolcraft({
|
|
134
|
+
canvas: {
|
|
135
|
+
enabled: true,
|
|
136
|
+
sizing: { mode: "intrinsic-media" },
|
|
137
|
+
upload: true,
|
|
138
|
+
},
|
|
139
|
+
panels: {
|
|
140
|
+
controls: {
|
|
141
|
+
sections: [
|
|
142
|
+
{
|
|
143
|
+
controls: {
|
|
144
|
+
source: {
|
|
145
|
+
defaultValue: null,
|
|
146
|
+
label: "Source image",
|
|
147
|
+
performanceReason: "Uploaded media dimensions change renderer workload.",
|
|
148
|
+
performanceRole: "responsiveness",
|
|
149
|
+
target: "source.image",
|
|
150
|
+
type: "fileDrop",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
title: "Source",
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
title: "Runtime Controls",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
105
161
|
function createViewportZoomStressScenario() {
|
|
106
162
|
return {
|
|
107
163
|
automated: true,
|
|
@@ -115,6 +171,7 @@ function createViewportZoomStressScenario() {
|
|
|
115
171
|
id: "viewport-zoom-stress",
|
|
116
172
|
interaction: "viewport-zoom-stress" as const,
|
|
117
173
|
stress: true,
|
|
174
|
+
stressFixture: createCombinedRendererStressFixture(),
|
|
118
175
|
target: "canvas.viewport",
|
|
119
176
|
workload: false,
|
|
120
177
|
};
|
|
@@ -128,6 +185,24 @@ function createMaxValueStressFixture(value: unknown, reason = "Maximum workload
|
|
|
128
185
|
};
|
|
129
186
|
}
|
|
130
187
|
|
|
188
|
+
function createCustomStressFixture(
|
|
189
|
+
value: unknown = { density: 12 },
|
|
190
|
+
reason = "Combined renderer stress state.",
|
|
191
|
+
) {
|
|
192
|
+
return {
|
|
193
|
+
kind: "custom" as const,
|
|
194
|
+
reason,
|
|
195
|
+
value,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function createCombinedRendererStressFixture() {
|
|
200
|
+
return createCustomStressFixture(
|
|
201
|
+
{ density: 12, zoom: "toolbar" },
|
|
202
|
+
"Stress checks must run with the densest product output visible.",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
131
206
|
function createLargeTextStressValue(): string {
|
|
132
207
|
return Array.from(
|
|
133
208
|
{ length: 1_000 },
|
|
@@ -146,6 +221,271 @@ function createLargeTextStressFixture() {
|
|
|
146
221
|
};
|
|
147
222
|
}
|
|
148
223
|
|
|
224
|
+
function createMediaStressFixture(
|
|
225
|
+
value: { height: number; width: number } = { height: 1080, width: 1920 },
|
|
226
|
+
reason = "Large uploaded source media is the heaviest realistic import fixture.",
|
|
227
|
+
) {
|
|
228
|
+
return {
|
|
229
|
+
kind: "media" as const,
|
|
230
|
+
reason,
|
|
231
|
+
value,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function createTextRendererPipeline() {
|
|
236
|
+
return {
|
|
237
|
+
interactionInvalidation: [
|
|
238
|
+
{
|
|
239
|
+
interaction: "control-drag",
|
|
240
|
+
invalidates: ["text-layout"],
|
|
241
|
+
targets: ["render.density"],
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
interaction: "control-change",
|
|
245
|
+
invalidates: ["text-layout"],
|
|
246
|
+
targets: ["render.mode"],
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
interaction: "viewport-zoom",
|
|
250
|
+
invalidates: [],
|
|
251
|
+
mustNotInvalidate: ["text-layout"],
|
|
252
|
+
targets: ["canvas.viewport"],
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
passes: [
|
|
256
|
+
{
|
|
257
|
+
cacheKey: ["product.content", "render.density", "render.mode"],
|
|
258
|
+
id: "text-layout",
|
|
259
|
+
inputs: ["product.content", "render.density", "render.mode"],
|
|
260
|
+
invalidatedBy: ["product.content", "render.density", "render.mode"],
|
|
261
|
+
kind: "text-layout",
|
|
262
|
+
output: "preview",
|
|
263
|
+
quality: "full",
|
|
264
|
+
runsOn: "main",
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
} as const;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function createAnimatedVectorRendererPipeline() {
|
|
271
|
+
return {
|
|
272
|
+
interactionInvalidation: [
|
|
273
|
+
{
|
|
274
|
+
interaction: "control-drag",
|
|
275
|
+
invalidates: ["vector-build"],
|
|
276
|
+
targets: ["render.density"],
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
interaction: "control-change",
|
|
280
|
+
invalidates: ["vector-build"],
|
|
281
|
+
targets: ["render.mode"],
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
interaction: "animation-frame",
|
|
285
|
+
invalidates: ["animation-composite"],
|
|
286
|
+
mustNotInvalidate: ["vector-build"],
|
|
287
|
+
targets: ["animation.time"],
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
interaction: "timeline-playback",
|
|
291
|
+
invalidates: ["animation-composite"],
|
|
292
|
+
mustNotInvalidate: ["vector-build"],
|
|
293
|
+
targets: ["timeline.currentTime"],
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
interaction: "timeline-scrub",
|
|
297
|
+
invalidates: ["animation-composite"],
|
|
298
|
+
mustNotInvalidate: ["vector-build"],
|
|
299
|
+
targets: ["timeline.currentTime"],
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
interaction: "viewport-drag",
|
|
303
|
+
invalidates: [],
|
|
304
|
+
mustNotInvalidate: ["vector-build", "animation-composite"],
|
|
305
|
+
targets: ["canvas.viewport"],
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
interaction: "viewport-zoom",
|
|
309
|
+
invalidates: [],
|
|
310
|
+
mustNotInvalidate: ["vector-build", "animation-composite"],
|
|
311
|
+
targets: ["canvas.viewport"],
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
passes: [
|
|
315
|
+
{
|
|
316
|
+
id: "vector-build",
|
|
317
|
+
inputs: ["render.density", "render.mode"],
|
|
318
|
+
invalidatedBy: ["render.density", "render.mode"],
|
|
319
|
+
kind: "vector-build",
|
|
320
|
+
output: "preview",
|
|
321
|
+
quality: "full",
|
|
322
|
+
runsOn: "main",
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
cacheKey: ["vector-build", "animation.time", "timeline.currentTime"],
|
|
326
|
+
id: "animation-composite",
|
|
327
|
+
inputs: ["vector-build", "animation.time", "timeline.currentTime"],
|
|
328
|
+
invalidatedBy: ["animation.time", "timeline.currentTime"],
|
|
329
|
+
kind: "composite",
|
|
330
|
+
output: "preview",
|
|
331
|
+
quality: "preview",
|
|
332
|
+
runsOn: "main",
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
} as const;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function createDenseCanvasPipeline() {
|
|
339
|
+
return {
|
|
340
|
+
interactionInvalidation: [
|
|
341
|
+
{
|
|
342
|
+
interaction: "control-drag",
|
|
343
|
+
invalidates: ["dense-background", "export-composite"],
|
|
344
|
+
targets: ["render.density"],
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
interaction: "control-change",
|
|
348
|
+
invalidates: ["export-composite"],
|
|
349
|
+
targets: ["render.mode"],
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
interaction: "viewport-zoom",
|
|
353
|
+
invalidates: [],
|
|
354
|
+
mustNotInvalidate: ["dense-background", "export-composite"],
|
|
355
|
+
targets: ["canvas.viewport"],
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
interaction: "export",
|
|
359
|
+
invalidates: ["export-composite"],
|
|
360
|
+
mustNotInvalidate: ["dense-background"],
|
|
361
|
+
targets: ["export.image.resolution"],
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
passes: [
|
|
365
|
+
{
|
|
366
|
+
cacheKey: ["render.density", "canvas.size", "canvas.renderScale"],
|
|
367
|
+
id: "dense-background",
|
|
368
|
+
inputs: ["render.density", "canvas.size", "canvas.renderScale"],
|
|
369
|
+
invalidatedBy: ["render.density", "canvas.size", "canvas.renderScale"],
|
|
370
|
+
kind: "rasterize",
|
|
371
|
+
output: "preview",
|
|
372
|
+
quality: "retina",
|
|
373
|
+
runsOn: "worker",
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
id: "semantic-foreground",
|
|
377
|
+
inputs: ["product.geometry"],
|
|
378
|
+
invalidatedBy: ["product.geometry"],
|
|
379
|
+
kind: "vector-build",
|
|
380
|
+
output: "preview",
|
|
381
|
+
quality: "full",
|
|
382
|
+
runsOn: "main",
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
cacheKey: ["dense-background", "semantic-foreground", "export.image.resolution"],
|
|
386
|
+
id: "export-composite",
|
|
387
|
+
inputs: ["dense-background", "semantic-foreground", "export.image.resolution"],
|
|
388
|
+
invalidatedBy: ["dense-background", "semantic-foreground", "export.image.resolution"],
|
|
389
|
+
kind: "composite",
|
|
390
|
+
output: "export",
|
|
391
|
+
quality: "export",
|
|
392
|
+
runsOn: "export-only",
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
} as const;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function createPixelRendererPipeline() {
|
|
399
|
+
return {
|
|
400
|
+
interactionInvalidation: [
|
|
401
|
+
{
|
|
402
|
+
interaction: "control-drag",
|
|
403
|
+
invalidates: ["pixel-transform"],
|
|
404
|
+
targets: ["render.density"],
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
interaction: "control-change",
|
|
408
|
+
invalidates: ["pixel-transform"],
|
|
409
|
+
targets: ["render.mode"],
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
interaction: "viewport-zoom",
|
|
413
|
+
invalidates: [],
|
|
414
|
+
mustNotInvalidate: ["pixel-transform"],
|
|
415
|
+
targets: ["canvas.viewport"],
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
passes: [
|
|
419
|
+
{
|
|
420
|
+
cacheKey: ["render.density", "render.mode", "canvas.size", "canvas.renderScale"],
|
|
421
|
+
id: "pixel-transform",
|
|
422
|
+
inputs: ["render.density", "render.mode", "canvas.size", "canvas.renderScale"],
|
|
423
|
+
invalidatedBy: ["render.density", "render.mode", "canvas.size", "canvas.renderScale"],
|
|
424
|
+
kind: "pixel-transform",
|
|
425
|
+
output: "preview",
|
|
426
|
+
quality: "retina",
|
|
427
|
+
runsOn: "worker",
|
|
428
|
+
},
|
|
429
|
+
],
|
|
430
|
+
} as const;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function createMediaRendererPipeline() {
|
|
434
|
+
return {
|
|
435
|
+
interactionInvalidation: [
|
|
436
|
+
{
|
|
437
|
+
interaction: "media-import",
|
|
438
|
+
invalidates: ["source-decode", "media-preprocess"],
|
|
439
|
+
targets: ["source.image"],
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
interaction: "control-drag",
|
|
443
|
+
invalidates: ["effect-composite"],
|
|
444
|
+
mustNotInvalidate: ["source-decode", "media-preprocess"],
|
|
445
|
+
targets: ["render.density"],
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
interaction: "viewport-zoom",
|
|
449
|
+
invalidates: [],
|
|
450
|
+
mustNotInvalidate: ["source-decode", "media-preprocess", "effect-composite"],
|
|
451
|
+
targets: ["canvas.viewport"],
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
passes: [
|
|
455
|
+
{
|
|
456
|
+
cacheKey: ["source.image.id", "source.image.width", "source.image.height"],
|
|
457
|
+
id: "source-decode",
|
|
458
|
+
inputs: ["source.image"],
|
|
459
|
+
invalidatedBy: ["source.image"],
|
|
460
|
+
kind: "decode",
|
|
461
|
+
output: "source",
|
|
462
|
+
quality: "full",
|
|
463
|
+
runsOn: "worker",
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
cacheKey: ["source-decode", "source.image.id"],
|
|
467
|
+
id: "media-preprocess",
|
|
468
|
+
inputs: ["source-decode"],
|
|
469
|
+
invalidatedBy: ["source-decode"],
|
|
470
|
+
kind: "preprocess",
|
|
471
|
+
output: "intermediate",
|
|
472
|
+
quality: "full",
|
|
473
|
+
runsOn: "worker-or-gpu",
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
cacheKey: ["media-preprocess", "render.density"],
|
|
477
|
+
id: "effect-composite",
|
|
478
|
+
inputs: ["media-preprocess", "render.density"],
|
|
479
|
+
invalidatedBy: ["media-preprocess", "render.density"],
|
|
480
|
+
kind: "composite",
|
|
481
|
+
output: "preview",
|
|
482
|
+
quality: "retina",
|
|
483
|
+
runsOn: "gpu",
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
} as const;
|
|
487
|
+
}
|
|
488
|
+
|
|
149
489
|
function createTextRendererScenarios(
|
|
150
490
|
options: { includeStressPreview?: boolean; includeViewportZoomStress?: boolean } = {},
|
|
151
491
|
) {
|
|
@@ -164,6 +504,9 @@ function createTextRendererScenarios(
|
|
|
164
504
|
id: "text-preview-render",
|
|
165
505
|
interaction: "preview-render" as const,
|
|
166
506
|
stress: includeStressPreview,
|
|
507
|
+
...(includeStressPreview
|
|
508
|
+
? { stressFixture: createCombinedRendererStressFixture() }
|
|
509
|
+
: {}),
|
|
167
510
|
workload: false,
|
|
168
511
|
},
|
|
169
512
|
{
|
|
@@ -183,6 +526,7 @@ function createTextRendererScenarios(
|
|
|
183
526
|
),
|
|
184
527
|
target: "render.density",
|
|
185
528
|
values: { default: 4, max: 12, min: 1 },
|
|
529
|
+
workloadFixture: createLargeTextStressFixture(),
|
|
186
530
|
workload: true,
|
|
187
531
|
},
|
|
188
532
|
{
|
|
@@ -228,6 +572,10 @@ function createAnimationFrameScenario() {
|
|
|
228
572
|
id: "animation-frame-loop",
|
|
229
573
|
interaction: "animation-frame" as const,
|
|
230
574
|
stress: true,
|
|
575
|
+
stressFixture: createCustomStressFixture(
|
|
576
|
+
{ animation: "fastest", density: 12 },
|
|
577
|
+
"Animation stress must sample the heaviest animated output.",
|
|
578
|
+
),
|
|
231
579
|
workload: false,
|
|
232
580
|
};
|
|
233
581
|
}
|
|
@@ -244,6 +592,10 @@ function createAnimationViewportDragScenario() {
|
|
|
244
592
|
id: "animation-viewport-drag",
|
|
245
593
|
interaction: "animation-viewport-drag" as const,
|
|
246
594
|
stress: true,
|
|
595
|
+
stressFixture: createCustomStressFixture(
|
|
596
|
+
{ animation: "fastest", density: 12 },
|
|
597
|
+
"Animated viewport drag must run while the heaviest animated output is visible.",
|
|
598
|
+
),
|
|
247
599
|
target: "canvas.viewport",
|
|
248
600
|
workload: false,
|
|
249
601
|
};
|
|
@@ -264,6 +616,7 @@ function createAnimatedVectorRendererConfig(options: { includeViewportDrag: bool
|
|
|
264
616
|
whyNotAlternativeStrategies: ["svg preserves semantic vector output for this fixture"],
|
|
265
617
|
},
|
|
266
618
|
rendererWorkload: "vector-output",
|
|
619
|
+
rendererPipeline: createAnimatedVectorRendererPipeline(),
|
|
267
620
|
scenarios: [
|
|
268
621
|
...createTextRendererScenarios(),
|
|
269
622
|
createAnimationFrameScenario(),
|
|
@@ -389,6 +742,64 @@ describe("Toolcraft template performance contract", () => {
|
|
|
389
742
|
expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toEqual([]);
|
|
390
743
|
});
|
|
391
744
|
|
|
745
|
+
it("rejects slider performance coverage that does not drag the real control", () => {
|
|
746
|
+
const config = defineToolcraftPerformance({
|
|
747
|
+
rendererStrategy: "none",
|
|
748
|
+
rendererWorkload: "none",
|
|
749
|
+
scenarios: [
|
|
750
|
+
{
|
|
751
|
+
automated: true,
|
|
752
|
+
automatedTestName: "perf: opacity change stays responsive",
|
|
753
|
+
browser: true,
|
|
754
|
+
browserTestName: "browser perf: opacity change stays responsive",
|
|
755
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
756
|
+
controlLabel: "Opacity",
|
|
757
|
+
expectedObservable: "Changing Opacity stays responsive.",
|
|
758
|
+
fixture: "opacity responsiveness fixture",
|
|
759
|
+
id: "opacity-change",
|
|
760
|
+
interaction: "control-change",
|
|
761
|
+
target: "render.opacity",
|
|
762
|
+
workload: false,
|
|
763
|
+
},
|
|
764
|
+
],
|
|
765
|
+
usesCustomRenderer: false,
|
|
766
|
+
workloadTargets: [],
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
|
|
770
|
+
"render.opacity is a slider and must have a control-drag performance scenario proving live canvas/product feedback while dragging.",
|
|
771
|
+
);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it("rejects range slider performance coverage that does not drag the real control", () => {
|
|
775
|
+
const config = defineToolcraftPerformance({
|
|
776
|
+
rendererStrategy: "none",
|
|
777
|
+
rendererWorkload: "none",
|
|
778
|
+
scenarios: [
|
|
779
|
+
{
|
|
780
|
+
automated: true,
|
|
781
|
+
automatedTestName: "perf: band change stays responsive",
|
|
782
|
+
browser: true,
|
|
783
|
+
browserTestName: "browser perf: band change stays responsive",
|
|
784
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
785
|
+
controlLabel: "Band",
|
|
786
|
+
expectedObservable: "Changing Band stays responsive.",
|
|
787
|
+
fixture: "band responsiveness fixture",
|
|
788
|
+
id: "band-change",
|
|
789
|
+
interaction: "control-change",
|
|
790
|
+
target: "render.band",
|
|
791
|
+
workload: false,
|
|
792
|
+
},
|
|
793
|
+
],
|
|
794
|
+
usesCustomRenderer: false,
|
|
795
|
+
workloadTargets: [],
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
expect(validateToolcraftPerformanceCoverage(rangeSliderSchema, config)).toContain(
|
|
799
|
+
"render.band is a rangeSlider and must have a control-drag performance scenario proving live canvas/product feedback while dragging.",
|
|
800
|
+
);
|
|
801
|
+
});
|
|
802
|
+
|
|
392
803
|
it("requires performance-sensitive controls to be listed in workloadTargets", () => {
|
|
393
804
|
const config = defineToolcraftPerformance({
|
|
394
805
|
rendererStrategy: "none",
|
|
@@ -433,6 +844,54 @@ describe("Toolcraft template performance contract", () => {
|
|
|
433
844
|
);
|
|
434
845
|
});
|
|
435
846
|
|
|
847
|
+
it("requires workload slider coverage to use real control drag", () => {
|
|
848
|
+
const config = defineToolcraftPerformance({
|
|
849
|
+
rendererStrategy: "none",
|
|
850
|
+
rendererWorkload: "none",
|
|
851
|
+
scenarios: [
|
|
852
|
+
{
|
|
853
|
+
automated: true,
|
|
854
|
+
automatedTestName: "perf: density change stays responsive",
|
|
855
|
+
browser: true,
|
|
856
|
+
browserTestName: "browser perf: density change stays responsive",
|
|
857
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
858
|
+
controlLabel: "Density",
|
|
859
|
+
expectedObservable: "Changing Density updates the product without blocking the UI.",
|
|
860
|
+
fixture: "runtime density fixture",
|
|
861
|
+
id: "density-change",
|
|
862
|
+
interaction: "control-change",
|
|
863
|
+
target: "render.density",
|
|
864
|
+
values: { default: 4, max: 12, min: 1 },
|
|
865
|
+
workload: true,
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
automated: true,
|
|
869
|
+
automatedTestName: "perf: mode change stays responsive",
|
|
870
|
+
browser: true,
|
|
871
|
+
browserTestName: "browser perf: mode change stays responsive",
|
|
872
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
873
|
+
controlLabel: "Mode",
|
|
874
|
+
expectedObservable: "Changing Mode updates the product without blocking the UI.",
|
|
875
|
+
fixture: "runtime mode fixture",
|
|
876
|
+
id: "mode-change",
|
|
877
|
+
interaction: "control-change",
|
|
878
|
+
target: "render.mode",
|
|
879
|
+
values: { default: "soft", max: "sharp", min: "soft" },
|
|
880
|
+
workload: false,
|
|
881
|
+
},
|
|
882
|
+
],
|
|
883
|
+
usesCustomRenderer: false,
|
|
884
|
+
workloadTargets: ["render.density"],
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual(
|
|
888
|
+
expect.arrayContaining([
|
|
889
|
+
"render.density must have min/default/max workload performance coverage through a real control-drag scenario.",
|
|
890
|
+
"render.density is a slider and must have a control-drag performance scenario proving live canvas/product feedback while dragging.",
|
|
891
|
+
]),
|
|
892
|
+
);
|
|
893
|
+
});
|
|
894
|
+
|
|
436
895
|
it("rejects responsiveness role on semantically workload controls", () => {
|
|
437
896
|
const misclassifiedSchema = defineToolcraft({
|
|
438
897
|
canvas: {
|
|
@@ -570,98 +1029,415 @@ describe("Toolcraft template performance contract", () => {
|
|
|
570
1029
|
);
|
|
571
1030
|
});
|
|
572
1031
|
|
|
573
|
-
it("
|
|
1032
|
+
it("requires stress scenarios to declare an exact stress fixture value", () => {
|
|
574
1033
|
const config = defineToolcraftPerformance({
|
|
575
1034
|
rendererStrategy: "none",
|
|
576
1035
|
rendererWorkload: "none",
|
|
577
1036
|
scenarios: [
|
|
578
1037
|
{
|
|
579
1038
|
automated: true,
|
|
580
|
-
automatedTestName: "perf:
|
|
1039
|
+
automatedTestName: "perf: preview render stays under budget",
|
|
581
1040
|
browser: true,
|
|
582
|
-
browserTestName: "browser perf:
|
|
583
|
-
budget: {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
kind: "large-text",
|
|
591
|
-
minChars: 50_000,
|
|
592
|
-
minLines: 1_000,
|
|
593
|
-
reason: "Long multiline content is the heaviest realistic text workload.",
|
|
594
|
-
value: "short text",
|
|
595
|
-
},
|
|
596
|
-
target: "product.content",
|
|
597
|
-
values: { default: "", max: "short text", min: "" },
|
|
598
|
-
workload: true,
|
|
1041
|
+
browserTestName: "browser perf: preview render stays under budget",
|
|
1042
|
+
budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
|
|
1043
|
+
expectedObservable: "Preview renders the heaviest output without freezing.",
|
|
1044
|
+
fixture: "combined heavy renderer fixture",
|
|
1045
|
+
id: "preview-render",
|
|
1046
|
+
interaction: "preview-render",
|
|
1047
|
+
stress: true,
|
|
1048
|
+
workload: false,
|
|
599
1049
|
},
|
|
600
1050
|
],
|
|
601
1051
|
usesCustomRenderer: false,
|
|
602
|
-
workloadTargets: [
|
|
1052
|
+
workloadTargets: [],
|
|
603
1053
|
});
|
|
604
1054
|
|
|
605
|
-
expect(validateToolcraftPerformanceCoverage(
|
|
606
|
-
|
|
607
|
-
"content-change large-text stressFixture.value must contain at least 50000 characters.",
|
|
608
|
-
"content-change large-text stressFixture.value must contain at least 1000 lines.",
|
|
609
|
-
]),
|
|
1055
|
+
expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
|
|
1056
|
+
"preview-render stress scenario must declare stressFixture with the real heaviest state used by browser performance tests.",
|
|
610
1057
|
);
|
|
611
1058
|
});
|
|
612
1059
|
|
|
613
|
-
it("
|
|
1060
|
+
it("rejects stress fixtures without a browser-applicable value", () => {
|
|
614
1061
|
const config = defineToolcraftPerformance({
|
|
615
1062
|
rendererStrategy: "none",
|
|
616
1063
|
rendererWorkload: "none",
|
|
617
1064
|
scenarios: [
|
|
618
1065
|
{
|
|
619
1066
|
automated: true,
|
|
620
|
-
automatedTestName: "perf:
|
|
1067
|
+
automatedTestName: "perf: preview render stays under budget",
|
|
621
1068
|
browser: true,
|
|
622
|
-
browserTestName: "browser perf:
|
|
623
|
-
budget: {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
stressFixture:
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1069
|
+
browserTestName: "browser perf: preview render stays under budget",
|
|
1070
|
+
budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
|
|
1071
|
+
expectedObservable: "Preview renders the heaviest output without freezing.",
|
|
1072
|
+
fixture: "combined heavy renderer fixture",
|
|
1073
|
+
id: "preview-render",
|
|
1074
|
+
interaction: "preview-render",
|
|
1075
|
+
stress: true,
|
|
1076
|
+
stressFixture: {
|
|
1077
|
+
kind: "custom",
|
|
1078
|
+
reason: "Combined renderer stress state.",
|
|
1079
|
+
},
|
|
1080
|
+
workload: false,
|
|
633
1081
|
},
|
|
634
1082
|
],
|
|
635
1083
|
usesCustomRenderer: false,
|
|
636
|
-
workloadTargets: [
|
|
1084
|
+
workloadTargets: [],
|
|
637
1085
|
});
|
|
638
1086
|
|
|
639
|
-
expect(validateToolcraftPerformanceCoverage(
|
|
1087
|
+
expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
|
|
1088
|
+
"preview-render stressFixture must include value so browser tests can apply the exact heavy state.",
|
|
1089
|
+
);
|
|
640
1090
|
});
|
|
641
1091
|
|
|
642
|
-
it("
|
|
1092
|
+
it("requires custom stress fixtures to be object-shaped heavy states", () => {
|
|
643
1093
|
const config = defineToolcraftPerformance({
|
|
644
1094
|
rendererStrategy: "none",
|
|
645
1095
|
rendererWorkload: "none",
|
|
646
1096
|
scenarios: [
|
|
647
1097
|
{
|
|
648
1098
|
automated: true,
|
|
649
|
-
automatedTestName: "perf:
|
|
1099
|
+
automatedTestName: "perf: preview render stays under budget",
|
|
650
1100
|
browser: true,
|
|
651
|
-
browserTestName: "browser perf:
|
|
652
|
-
budget: {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
1101
|
+
browserTestName: "browser perf: preview render stays under budget",
|
|
1102
|
+
budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
|
|
1103
|
+
expectedObservable: "Preview renders the heaviest output without freezing.",
|
|
1104
|
+
fixture: "combined heavy renderer fixture",
|
|
1105
|
+
id: "preview-render",
|
|
1106
|
+
interaction: "preview-render",
|
|
1107
|
+
stress: true,
|
|
1108
|
+
stressFixture: {
|
|
1109
|
+
kind: "custom",
|
|
1110
|
+
reason: "Combined renderer stress state.",
|
|
1111
|
+
value: 12,
|
|
1112
|
+
},
|
|
1113
|
+
workload: false,
|
|
661
1114
|
},
|
|
662
1115
|
],
|
|
663
1116
|
usesCustomRenderer: false,
|
|
664
|
-
workloadTargets: [
|
|
1117
|
+
workloadTargets: [],
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
|
|
1121
|
+
"preview-render custom stressFixture.value must be an object with one key per heavy state part so browser tests can apply every key.",
|
|
1122
|
+
);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
it("rejects empty custom stress fixture objects", () => {
|
|
1126
|
+
const config = defineToolcraftPerformance({
|
|
1127
|
+
rendererStrategy: "none",
|
|
1128
|
+
rendererWorkload: "none",
|
|
1129
|
+
scenarios: [
|
|
1130
|
+
{
|
|
1131
|
+
automated: true,
|
|
1132
|
+
automatedTestName: "perf: preview render stays under budget",
|
|
1133
|
+
browser: true,
|
|
1134
|
+
browserTestName: "browser perf: preview render stays under budget",
|
|
1135
|
+
budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
|
|
1136
|
+
expectedObservable: "Preview renders the heaviest output without freezing.",
|
|
1137
|
+
fixture: "combined heavy renderer fixture",
|
|
1138
|
+
id: "preview-render",
|
|
1139
|
+
interaction: "preview-render",
|
|
1140
|
+
stress: true,
|
|
1141
|
+
stressFixture: {
|
|
1142
|
+
kind: "custom",
|
|
1143
|
+
reason: "Combined renderer stress state.",
|
|
1144
|
+
value: {},
|
|
1145
|
+
},
|
|
1146
|
+
workload: false,
|
|
1147
|
+
},
|
|
1148
|
+
],
|
|
1149
|
+
usesCustomRenderer: false,
|
|
1150
|
+
workloadTargets: [],
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
|
|
1154
|
+
"preview-render custom stressFixture.value must include at least one heavy state key.",
|
|
1155
|
+
);
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
it("rejects short stress fixtures for large text workload scenarios", () => {
|
|
1159
|
+
const config = defineToolcraftPerformance({
|
|
1160
|
+
rendererStrategy: "none",
|
|
1161
|
+
rendererWorkload: "none",
|
|
1162
|
+
scenarios: [
|
|
1163
|
+
{
|
|
1164
|
+
automated: true,
|
|
1165
|
+
automatedTestName: "perf: content input stays responsive",
|
|
1166
|
+
browser: true,
|
|
1167
|
+
browserTestName: "browser perf: content input stays responsive",
|
|
1168
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
1169
|
+
controlLabel: "Content",
|
|
1170
|
+
expectedObservable: "Large text content updates without blocking the UI.",
|
|
1171
|
+
fixture: "large text content fixture",
|
|
1172
|
+
id: "content-change",
|
|
1173
|
+
interaction: "control-change",
|
|
1174
|
+
stressFixture: {
|
|
1175
|
+
kind: "large-text",
|
|
1176
|
+
minChars: 50_000,
|
|
1177
|
+
minLines: 1_000,
|
|
1178
|
+
reason: "Long multiline content is the heaviest realistic text workload.",
|
|
1179
|
+
value: "short text",
|
|
1180
|
+
},
|
|
1181
|
+
target: "product.content",
|
|
1182
|
+
values: { default: "", max: "short text", min: "" },
|
|
1183
|
+
workload: true,
|
|
1184
|
+
},
|
|
1185
|
+
],
|
|
1186
|
+
usesCustomRenderer: false,
|
|
1187
|
+
workloadTargets: ["product.content"],
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
expect(validateToolcraftPerformanceCoverage(largeTextSchema, config)).toEqual(
|
|
1191
|
+
expect.arrayContaining([
|
|
1192
|
+
"content-change large-text stressFixture.value must contain at least 50000 characters.",
|
|
1193
|
+
"content-change large-text stressFixture.value must contain at least 1000 lines.",
|
|
1194
|
+
]),
|
|
1195
|
+
);
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
it("accepts large text workload scenarios with machine-checkable stress fixtures", () => {
|
|
1199
|
+
const config = defineToolcraftPerformance({
|
|
1200
|
+
rendererStrategy: "none",
|
|
1201
|
+
rendererWorkload: "none",
|
|
1202
|
+
scenarios: [
|
|
1203
|
+
{
|
|
1204
|
+
automated: true,
|
|
1205
|
+
automatedTestName: "perf: content input stays responsive",
|
|
1206
|
+
browser: true,
|
|
1207
|
+
browserTestName: "browser perf: content input stays responsive",
|
|
1208
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
1209
|
+
controlLabel: "Content",
|
|
1210
|
+
expectedObservable: "Large text content updates without blocking the UI.",
|
|
1211
|
+
fixture: "large text content fixture",
|
|
1212
|
+
id: "content-change",
|
|
1213
|
+
interaction: "control-change",
|
|
1214
|
+
stressFixture: createLargeTextStressFixture(),
|
|
1215
|
+
target: "product.content",
|
|
1216
|
+
values: { default: "", max: createLargeTextStressValue(), min: "" },
|
|
1217
|
+
workload: true,
|
|
1218
|
+
},
|
|
1219
|
+
],
|
|
1220
|
+
usesCustomRenderer: false,
|
|
1221
|
+
workloadTargets: ["product.content"],
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
expect(validateToolcraftPerformanceCoverage(largeTextSchema, config)).toEqual([]);
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
it("requires workload controls in custom renderers to declare an independent workload fixture", () => {
|
|
1228
|
+
const config = defineToolcraftPerformance({
|
|
1229
|
+
rendererStrategy: "canvas-2d",
|
|
1230
|
+
rendererWorkload: "text-output",
|
|
1231
|
+
scenarios: [
|
|
1232
|
+
{
|
|
1233
|
+
automated: true,
|
|
1234
|
+
automatedTestName: "perf: density drag stays responsive",
|
|
1235
|
+
browser: true,
|
|
1236
|
+
browserTestName: "browser perf: density drag stays responsive",
|
|
1237
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
1238
|
+
controlLabel: "Density",
|
|
1239
|
+
expectedObservable: "Dragging Density updates text output without blocking the UI.",
|
|
1240
|
+
fixture: "runtime density fixture",
|
|
1241
|
+
id: "density-drag",
|
|
1242
|
+
interaction: "control-drag",
|
|
1243
|
+
stressFixture: createMaxValueStressFixture(
|
|
1244
|
+
12,
|
|
1245
|
+
"Density max is the heaviest control value.",
|
|
1246
|
+
),
|
|
1247
|
+
target: "render.density",
|
|
1248
|
+
values: { default: 4, max: 12, min: 1 },
|
|
1249
|
+
workload: true,
|
|
1250
|
+
},
|
|
1251
|
+
],
|
|
1252
|
+
usesCustomRenderer: true,
|
|
1253
|
+
workloadTargets: ["render.density"],
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1257
|
+
"density-drag workload control scenario must declare workloadFixture for the app's heavy baseline state; stressFixture covers the control value only.",
|
|
1258
|
+
);
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
it("rejects toy media workload fixtures for control-drag baselines", () => {
|
|
1262
|
+
const config = defineToolcraftPerformance({
|
|
1263
|
+
rendererStrategy: "webgl",
|
|
1264
|
+
rendererWorkload: "pixel-output",
|
|
1265
|
+
scenarios: [
|
|
1266
|
+
{
|
|
1267
|
+
automated: true,
|
|
1268
|
+
automatedTestName: "perf: density drag stays responsive",
|
|
1269
|
+
browser: true,
|
|
1270
|
+
browserTestName: "browser perf: density drag stays responsive",
|
|
1271
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
1272
|
+
controlLabel: "Density",
|
|
1273
|
+
expectedObservable: "Dragging Density updates source media output without blocking the UI.",
|
|
1274
|
+
fixture: "runtime density fixture",
|
|
1275
|
+
id: "density-drag",
|
|
1276
|
+
interaction: "control-drag",
|
|
1277
|
+
stressFixture: createMaxValueStressFixture(
|
|
1278
|
+
12,
|
|
1279
|
+
"Density max is the heaviest control value.",
|
|
1280
|
+
),
|
|
1281
|
+
target: "render.density",
|
|
1282
|
+
values: { default: 4, max: 12, min: 1 },
|
|
1283
|
+
workloadFixture: createMediaStressFixture(
|
|
1284
|
+
{ height: 480, width: 640 },
|
|
1285
|
+
"This small source should be rejected as too light.",
|
|
1286
|
+
),
|
|
1287
|
+
workload: true,
|
|
1288
|
+
},
|
|
1289
|
+
],
|
|
1290
|
+
usesCustomRenderer: true,
|
|
1291
|
+
workloadTargets: ["render.density"],
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1295
|
+
"density-drag media workloadFixture.value must be at least 1920x1080-equivalent; received 640x480.",
|
|
1296
|
+
);
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
it("accepts workload controls with separate control stress and app baseline fixtures", () => {
|
|
1300
|
+
const config = defineToolcraftPerformance({
|
|
1301
|
+
rendererStrategy: "canvas-2d",
|
|
1302
|
+
rendererTechnique: {
|
|
1303
|
+
exportRenderer: "canvas-2d",
|
|
1304
|
+
fidelityRisks: ["text output must stay crisp"],
|
|
1305
|
+
performanceRisks: ["density changes can relayout text"],
|
|
1306
|
+
previewRenderer: "canvas-2d",
|
|
1307
|
+
productRepresentation: "text",
|
|
1308
|
+
rendererStrategy: "canvas-2d",
|
|
1309
|
+
rendererWorkload: "text-output",
|
|
1310
|
+
sourceRepresentation: "dom-text",
|
|
1311
|
+
whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
|
|
1312
|
+
},
|
|
1313
|
+
rendererWorkload: "text-output",
|
|
1314
|
+
rendererPipeline: createTextRendererPipeline(),
|
|
1315
|
+
scenarios: createTextRendererScenarios(),
|
|
1316
|
+
usesCustomRenderer: true,
|
|
1317
|
+
workloadTargets: ["render.density"],
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
it("rejects workload fixtures that are not paired with scenario stress fixtures", () => {
|
|
1324
|
+
const config = defineToolcraftPerformance({
|
|
1325
|
+
rendererStrategy: "none",
|
|
1326
|
+
rendererWorkload: "none",
|
|
1327
|
+
scenarios: [
|
|
1328
|
+
{
|
|
1329
|
+
automated: true,
|
|
1330
|
+
automatedTestName: "perf: preview baseline stays responsive",
|
|
1331
|
+
browser: true,
|
|
1332
|
+
browserTestName: "browser perf: preview baseline stays responsive",
|
|
1333
|
+
budget: { maxPreviewMs: 1000 },
|
|
1334
|
+
expectedObservable: "Preview renders inside a heavy baseline.",
|
|
1335
|
+
fixture: "baseline fixture",
|
|
1336
|
+
id: "preview-heavy-baseline",
|
|
1337
|
+
interaction: "preview-render",
|
|
1338
|
+
workload: false,
|
|
1339
|
+
workloadFixture: createLargeTextStressFixture(),
|
|
1340
|
+
},
|
|
1341
|
+
],
|
|
1342
|
+
usesCustomRenderer: false,
|
|
1343
|
+
workloadTargets: [],
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1347
|
+
"preview-heavy-baseline workloadFixture must be paired with stressFixture so tests apply a heavy baseline and then the measured scenario value.",
|
|
1348
|
+
);
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
it("rejects media-import scenarios that use toy source dimensions", () => {
|
|
1352
|
+
const config = defineToolcraftPerformance({
|
|
1353
|
+
rendererStrategy: "none",
|
|
1354
|
+
rendererWorkload: "none",
|
|
1355
|
+
scenarios: [
|
|
1356
|
+
{
|
|
1357
|
+
automated: true,
|
|
1358
|
+
automatedTestName: "perf: source image import stays responsive",
|
|
1359
|
+
browser: true,
|
|
1360
|
+
browserTestName: "browser perf: source image import stays responsive",
|
|
1361
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
1362
|
+
expectedObservable: "Importing source media does not block the app.",
|
|
1363
|
+
fixture: "large source image fixture",
|
|
1364
|
+
id: "source-media-import",
|
|
1365
|
+
interaction: "media-import",
|
|
1366
|
+
stressFixture: createMediaStressFixture(
|
|
1367
|
+
{ height: 480, width: 640 },
|
|
1368
|
+
"This small fixture should be rejected as too light.",
|
|
1369
|
+
),
|
|
1370
|
+
target: "source.image",
|
|
1371
|
+
workload: true,
|
|
1372
|
+
},
|
|
1373
|
+
],
|
|
1374
|
+
usesCustomRenderer: false,
|
|
1375
|
+
workloadTargets: [],
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
expect(validateToolcraftPerformanceCoverage(mediaUploadSchema, config)).toContain(
|
|
1379
|
+
"source-media-import media stressFixture.value must be at least 1920x1080-equivalent; received 640x480.",
|
|
1380
|
+
);
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
it("requires media-import scenarios to be workload media fixtures", () => {
|
|
1384
|
+
const config = defineToolcraftPerformance({
|
|
1385
|
+
rendererStrategy: "none",
|
|
1386
|
+
rendererWorkload: "none",
|
|
1387
|
+
scenarios: [
|
|
1388
|
+
{
|
|
1389
|
+
automated: true,
|
|
1390
|
+
automatedTestName: "perf: source image import stays responsive",
|
|
1391
|
+
browser: true,
|
|
1392
|
+
browserTestName: "browser perf: source image import stays responsive",
|
|
1393
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
1394
|
+
expectedObservable: "Importing source media does not block the app.",
|
|
1395
|
+
fixture: "large source image fixture",
|
|
1396
|
+
id: "source-media-import",
|
|
1397
|
+
interaction: "media-import",
|
|
1398
|
+
stressFixture: createCustomStressFixture(
|
|
1399
|
+
{ height: 1080, width: 1920 },
|
|
1400
|
+
"Wrong fixture kind for media import.",
|
|
1401
|
+
),
|
|
1402
|
+
target: "source.image",
|
|
1403
|
+
workload: false,
|
|
1404
|
+
},
|
|
1405
|
+
],
|
|
1406
|
+
usesCustomRenderer: false,
|
|
1407
|
+
workloadTargets: [],
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
expect(validateToolcraftPerformanceCoverage(mediaUploadSchema, config)).toEqual(
|
|
1411
|
+
expect.arrayContaining([
|
|
1412
|
+
"source-media-import media-import scenario must set workload true because decoded source size changes renderer workload.",
|
|
1413
|
+
'source-media-import media-import scenario must use stressFixture.kind "media" with a realistic uploaded source size.',
|
|
1414
|
+
]),
|
|
1415
|
+
);
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
it("rejects visible controls without performance scenarios", () => {
|
|
1419
|
+
const config = defineToolcraftPerformance({
|
|
1420
|
+
rendererStrategy: "none",
|
|
1421
|
+
rendererWorkload: "none",
|
|
1422
|
+
scenarios: [
|
|
1423
|
+
{
|
|
1424
|
+
automated: true,
|
|
1425
|
+
automatedTestName: "perf: density drag stays responsive",
|
|
1426
|
+
browser: true,
|
|
1427
|
+
browserTestName: "browser perf: density drag stays responsive",
|
|
1428
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
1429
|
+
controlLabel: "Density",
|
|
1430
|
+
expectedObservable: "Dragging Density updates the product without blocking the UI.",
|
|
1431
|
+
fixture: "runtime density fixture",
|
|
1432
|
+
id: "density-drag",
|
|
1433
|
+
interaction: "control-drag",
|
|
1434
|
+
target: "render.density",
|
|
1435
|
+
values: { default: 4, max: 12, min: 1 },
|
|
1436
|
+
workload: true,
|
|
1437
|
+
},
|
|
1438
|
+
],
|
|
1439
|
+
usesCustomRenderer: false,
|
|
1440
|
+
workloadTargets: ["render.density"],
|
|
665
1441
|
});
|
|
666
1442
|
|
|
667
1443
|
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
@@ -684,6 +1460,7 @@ describe("Toolcraft template performance contract", () => {
|
|
|
684
1460
|
whyNotAlternativeStrategies: ["webgl texture output would rasterize text too early"],
|
|
685
1461
|
},
|
|
686
1462
|
rendererWorkload: "text-output",
|
|
1463
|
+
rendererPipeline: createTextRendererPipeline(),
|
|
687
1464
|
scenarios: createTextRendererScenarios(),
|
|
688
1465
|
usesCustomRenderer: true,
|
|
689
1466
|
workloadTargets: ["render.density"],
|
|
@@ -812,9 +1589,76 @@ describe("Toolcraft template performance contract", () => {
|
|
|
812
1589
|
);
|
|
813
1590
|
});
|
|
814
1591
|
|
|
815
|
-
it("
|
|
816
|
-
const config =
|
|
817
|
-
|
|
1592
|
+
it("requires detail-heavy Canvas 2D renderers to document measured WebGL/WebGPU evaluation", () => {
|
|
1593
|
+
const config = defineToolcraftPerformance({
|
|
1594
|
+
rendererStrategy: "canvas-2d",
|
|
1595
|
+
rendererTechnique: {
|
|
1596
|
+
exportRenderer: "canvas-2d",
|
|
1597
|
+
fidelityRisks: ["dense texture output must match source media colors"],
|
|
1598
|
+
layers: [
|
|
1599
|
+
{
|
|
1600
|
+
content: ["bitmap-media", "noise"],
|
|
1601
|
+
exportMode: "included",
|
|
1602
|
+
id: "media-effect",
|
|
1603
|
+
kind: "product-foreground",
|
|
1604
|
+
primitiveCount: "high",
|
|
1605
|
+
renderer: "canvas-2d",
|
|
1606
|
+
uiSelector: '[data-toolcraft-renderer-layer="media-effect"]',
|
|
1607
|
+
},
|
|
1608
|
+
],
|
|
1609
|
+
performanceRisks: ["large uploaded media can require millions of pixel operations"],
|
|
1610
|
+
previewRenderer: "canvas-2d",
|
|
1611
|
+
productRepresentation: "pixel",
|
|
1612
|
+
rendererStrategy: "canvas-2d",
|
|
1613
|
+
rendererWorkload: "pixel-output",
|
|
1614
|
+
sourceRepresentation: "image-media",
|
|
1615
|
+
whyNotAlternativeStrategies: ["canvas is convenient"],
|
|
1616
|
+
},
|
|
1617
|
+
rendererWorkload: "pixel-output",
|
|
1618
|
+
scenarios: createTextRendererScenarios(),
|
|
1619
|
+
usesCustomRenderer: true,
|
|
1620
|
+
workloadTargets: ["render.density"],
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1624
|
+
"Detail-heavy Canvas 2D renderers must evaluate WebGL/WebGPU in rendererTechnique.whyNotAlternativeStrategies or performanceRisks with measured stress evidence before keeping the pixel work on CPU.",
|
|
1625
|
+
);
|
|
1626
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1627
|
+
'rendererWorkload "pixel-output" should use rendererStrategy "webgl" or "webgpu", received "canvas-2d". Keeping a CPU renderer requires measured WebGL/WebGPU evidence in rendererTechnique.whyNotAlternativeStrategies or performanceRisks.',
|
|
1628
|
+
);
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
it("allows pixel-output Canvas 2D only with measured WebGL/WebGPU evidence", () => {
|
|
1632
|
+
const config = defineToolcraftPerformance({
|
|
1633
|
+
rendererStrategy: "canvas-2d",
|
|
1634
|
+
rendererTechnique: {
|
|
1635
|
+
exportRenderer: "canvas-2d",
|
|
1636
|
+
fidelityRisks: ["pixel output must preserve exact color stops"],
|
|
1637
|
+
performanceRisks: [
|
|
1638
|
+
"Measured WebGL stress evidence showed setup overhead exceeded cached Canvas 2D worker output for this static fixture.",
|
|
1639
|
+
],
|
|
1640
|
+
previewRenderer: "canvas-2d",
|
|
1641
|
+
productRepresentation: "pixel",
|
|
1642
|
+
rendererStrategy: "canvas-2d",
|
|
1643
|
+
rendererWorkload: "pixel-output",
|
|
1644
|
+
sourceRepresentation: "procedural-data",
|
|
1645
|
+
whyNotAlternativeStrategies: [
|
|
1646
|
+
"WebGL measured stress evidence stayed within budget but did not improve interaction latency for this renderer.",
|
|
1647
|
+
],
|
|
1648
|
+
},
|
|
1649
|
+
rendererWorkload: "pixel-output",
|
|
1650
|
+
rendererPipeline: createPixelRendererPipeline(),
|
|
1651
|
+
scenarios: createTextRendererScenarios(),
|
|
1652
|
+
usesCustomRenderer: true,
|
|
1653
|
+
workloadTargets: ["render.density"],
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
it("accepts animated custom renderers with stress coverage while dragging the viewport", () => {
|
|
1660
|
+
const config = createAnimatedVectorRendererConfig({ includeViewportDrag: true });
|
|
1661
|
+
|
|
818
1662
|
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
|
|
819
1663
|
});
|
|
820
1664
|
|
|
@@ -834,6 +1678,528 @@ describe("Toolcraft template performance contract", () => {
|
|
|
834
1678
|
);
|
|
835
1679
|
});
|
|
836
1680
|
|
|
1681
|
+
it("requires custom renderers to declare a render pipeline inventory", () => {
|
|
1682
|
+
const config = defineToolcraftPerformance({
|
|
1683
|
+
rendererStrategy: "canvas-2d",
|
|
1684
|
+
rendererTechnique: {
|
|
1685
|
+
exportRenderer: "canvas-2d",
|
|
1686
|
+
fidelityRisks: ["canvas output can soften edges"],
|
|
1687
|
+
performanceRisks: ["dense output can jank during drag"],
|
|
1688
|
+
previewRenderer: "canvas-2d",
|
|
1689
|
+
productRepresentation: "mixed",
|
|
1690
|
+
rendererStrategy: "canvas-2d",
|
|
1691
|
+
rendererWorkload: "simple-composition",
|
|
1692
|
+
sourceRepresentation: "procedural-data",
|
|
1693
|
+
whyNotAlternativeStrategies: [
|
|
1694
|
+
"svg cannot preserve dense procedural background output",
|
|
1695
|
+
],
|
|
1696
|
+
},
|
|
1697
|
+
rendererWorkload: "simple-composition",
|
|
1698
|
+
scenarios: createTextRendererScenarios(),
|
|
1699
|
+
usesCustomRenderer: true,
|
|
1700
|
+
workloadTargets: ["render.density"],
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1704
|
+
"Custom renderers must declare rendererPipeline so render passes, cache keys, and invalidation are machine-checkable.",
|
|
1705
|
+
);
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
it("rejects vague renderer pipeline pass dependencies", () => {
|
|
1709
|
+
const config = defineToolcraftPerformance({
|
|
1710
|
+
rendererStrategy: "canvas-2d",
|
|
1711
|
+
rendererTechnique: {
|
|
1712
|
+
exportRenderer: "canvas-2d",
|
|
1713
|
+
fidelityRisks: ["dense text output can soften at small sizes"],
|
|
1714
|
+
performanceRisks: ["text layout can be expensive"],
|
|
1715
|
+
previewRenderer: "canvas-2d",
|
|
1716
|
+
productRepresentation: "text",
|
|
1717
|
+
rendererStrategy: "canvas-2d",
|
|
1718
|
+
rendererWorkload: "text-output",
|
|
1719
|
+
sourceRepresentation: "dom-text",
|
|
1720
|
+
whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
|
|
1721
|
+
},
|
|
1722
|
+
rendererWorkload: "text-output",
|
|
1723
|
+
rendererPipeline: {
|
|
1724
|
+
interactionInvalidation: [
|
|
1725
|
+
{
|
|
1726
|
+
interaction: "control-drag",
|
|
1727
|
+
invalidates: ["text-layout"],
|
|
1728
|
+
targets: ["render.density"],
|
|
1729
|
+
},
|
|
1730
|
+
],
|
|
1731
|
+
passes: [
|
|
1732
|
+
{
|
|
1733
|
+
cacheKey: ["state"],
|
|
1734
|
+
id: "text-layout",
|
|
1735
|
+
inputs: ["state"],
|
|
1736
|
+
invalidatedBy: ["all values"],
|
|
1737
|
+
kind: "text-layout",
|
|
1738
|
+
output: "preview",
|
|
1739
|
+
quality: "full",
|
|
1740
|
+
runsOn: "main",
|
|
1741
|
+
},
|
|
1742
|
+
],
|
|
1743
|
+
},
|
|
1744
|
+
scenarios: createTextRendererScenarios(),
|
|
1745
|
+
usesCustomRenderer: true,
|
|
1746
|
+
workloadTargets: ["render.density"],
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual(
|
|
1750
|
+
expect.arrayContaining([
|
|
1751
|
+
'rendererPipeline pass "text-layout" inputs entry "state" is too vague. Name the concrete runtime target, source key, resource key, or cache key part.',
|
|
1752
|
+
'rendererPipeline pass "text-layout" invalidatedBy entry "all values" is too vague. Name the concrete runtime target, source key, resource key, or cache key part.',
|
|
1753
|
+
'rendererPipeline pass "text-layout" cacheKey entry "state" is too vague. Name the concrete runtime target, source key, resource key, or cache key part.',
|
|
1754
|
+
]),
|
|
1755
|
+
);
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
it("requires cache-sensitive renderer pipeline passes to declare cache keys", () => {
|
|
1759
|
+
const config = defineToolcraftPerformance({
|
|
1760
|
+
rendererStrategy: "canvas-2d",
|
|
1761
|
+
rendererTechnique: {
|
|
1762
|
+
exportRenderer: "canvas-2d",
|
|
1763
|
+
fidelityRisks: ["source pixels must stay crisp"],
|
|
1764
|
+
performanceRisks: ["pixel transforms can block the main thread"],
|
|
1765
|
+
previewRenderer: "canvas-2d",
|
|
1766
|
+
productRepresentation: "pixel",
|
|
1767
|
+
rendererStrategy: "canvas-2d",
|
|
1768
|
+
rendererWorkload: "pixel-output",
|
|
1769
|
+
sourceRepresentation: "image-media",
|
|
1770
|
+
whyNotAlternativeStrategies: [
|
|
1771
|
+
"WebGL/WebGPU measured performance evidence showed no improvement for this fixture",
|
|
1772
|
+
],
|
|
1773
|
+
},
|
|
1774
|
+
rendererWorkload: "pixel-output",
|
|
1775
|
+
rendererPipeline: {
|
|
1776
|
+
interactionInvalidation: [
|
|
1777
|
+
{
|
|
1778
|
+
interaction: "control-drag",
|
|
1779
|
+
invalidates: ["pixel-pass"],
|
|
1780
|
+
targets: ["render.density"],
|
|
1781
|
+
},
|
|
1782
|
+
],
|
|
1783
|
+
passes: [
|
|
1784
|
+
{
|
|
1785
|
+
id: "pixel-pass",
|
|
1786
|
+
inputs: ["source.image", "render.density"],
|
|
1787
|
+
invalidatedBy: ["source.image", "render.density"],
|
|
1788
|
+
kind: "pixel-transform",
|
|
1789
|
+
output: "preview",
|
|
1790
|
+
quality: "retina",
|
|
1791
|
+
runsOn: "worker",
|
|
1792
|
+
},
|
|
1793
|
+
],
|
|
1794
|
+
},
|
|
1795
|
+
scenarios: createTextRendererScenarios(),
|
|
1796
|
+
usesCustomRenderer: true,
|
|
1797
|
+
workloadTargets: ["render.density"],
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1801
|
+
'rendererPipeline pass "pixel-pass" is a cache-sensitive pixel-transform pass and must declare cacheKey so tests can reject full recomputation on every control change.',
|
|
1802
|
+
);
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
it("requires composite renderer pipeline passes to declare cache keys", () => {
|
|
1806
|
+
const config = defineToolcraftPerformance({
|
|
1807
|
+
rendererStrategy: "svg",
|
|
1808
|
+
rendererTechnique: {
|
|
1809
|
+
exportRenderer: "svg",
|
|
1810
|
+
fidelityRisks: ["animated composite must preserve vector output"],
|
|
1811
|
+
performanceRisks: ["compositing can be rebuilt too often"],
|
|
1812
|
+
previewRenderer: "svg",
|
|
1813
|
+
productRepresentation: "vector",
|
|
1814
|
+
rendererStrategy: "svg",
|
|
1815
|
+
rendererWorkload: "vector-output",
|
|
1816
|
+
sourceRepresentation: "procedural-data",
|
|
1817
|
+
whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
|
|
1818
|
+
},
|
|
1819
|
+
rendererWorkload: "vector-output",
|
|
1820
|
+
rendererPipeline: {
|
|
1821
|
+
...createAnimatedVectorRendererPipeline(),
|
|
1822
|
+
passes: createAnimatedVectorRendererPipeline().passes.map((pass) =>
|
|
1823
|
+
pass.id === "animation-composite"
|
|
1824
|
+
? {
|
|
1825
|
+
id: pass.id,
|
|
1826
|
+
inputs: pass.inputs,
|
|
1827
|
+
invalidatedBy: pass.invalidatedBy,
|
|
1828
|
+
kind: pass.kind,
|
|
1829
|
+
output: pass.output,
|
|
1830
|
+
quality: pass.quality,
|
|
1831
|
+
runsOn: pass.runsOn,
|
|
1832
|
+
}
|
|
1833
|
+
: pass,
|
|
1834
|
+
),
|
|
1835
|
+
},
|
|
1836
|
+
scenarios: createTextRendererScenarios(),
|
|
1837
|
+
usesCustomRenderer: true,
|
|
1838
|
+
workloadTargets: ["render.density"],
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1842
|
+
'rendererPipeline pass "animation-composite" is a cache-sensitive composite pass and must declare cacheKey so tests can reject full recomputation on every control change.',
|
|
1843
|
+
);
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
it("rejects high-frequency viewport interactions that invalidate expensive passes", () => {
|
|
1847
|
+
const config = defineToolcraftPerformance({
|
|
1848
|
+
rendererStrategy: "canvas-2d",
|
|
1849
|
+
rendererTechnique: {
|
|
1850
|
+
exportRenderer: "canvas-2d",
|
|
1851
|
+
fidelityRisks: ["dense background must stay stable while zooming"],
|
|
1852
|
+
layers: [
|
|
1853
|
+
{
|
|
1854
|
+
content: ["dense-pattern"],
|
|
1855
|
+
exportMode: "included",
|
|
1856
|
+
id: "dense-pattern",
|
|
1857
|
+
kind: "background",
|
|
1858
|
+
primitiveCount: "high",
|
|
1859
|
+
renderer: "canvas-2d",
|
|
1860
|
+
},
|
|
1861
|
+
],
|
|
1862
|
+
performanceRisks: [
|
|
1863
|
+
"WebGL measured stress evidence was reviewed before keeping dense background on Canvas 2D",
|
|
1864
|
+
],
|
|
1865
|
+
previewRenderer: "canvas-2d",
|
|
1866
|
+
productRepresentation: "pixel",
|
|
1867
|
+
rendererStrategy: "canvas-2d",
|
|
1868
|
+
rendererWorkload: "pixel-output",
|
|
1869
|
+
sourceRepresentation: "procedural-data",
|
|
1870
|
+
whyNotAlternativeStrategies: [
|
|
1871
|
+
"WebGL stress evidence showed shader setup overhead exceeded static cache reuse",
|
|
1872
|
+
],
|
|
1873
|
+
},
|
|
1874
|
+
rendererWorkload: "pixel-output",
|
|
1875
|
+
rendererPipeline: {
|
|
1876
|
+
interactionInvalidation: [
|
|
1877
|
+
{
|
|
1878
|
+
interaction: "viewport-zoom",
|
|
1879
|
+
invalidates: ["dense-background"],
|
|
1880
|
+
mustNotInvalidate: ["dense-background"],
|
|
1881
|
+
targets: ["canvas.viewport"],
|
|
1882
|
+
},
|
|
1883
|
+
{
|
|
1884
|
+
interaction: "control-drag",
|
|
1885
|
+
invalidates: ["dense-background"],
|
|
1886
|
+
targets: ["render.density"],
|
|
1887
|
+
},
|
|
1888
|
+
],
|
|
1889
|
+
passes: [
|
|
1890
|
+
{
|
|
1891
|
+
cacheKey: ["render.density", "canvas.size", "canvas.renderScale"],
|
|
1892
|
+
id: "dense-background",
|
|
1893
|
+
inputs: ["render.density", "canvas.size", "canvas.renderScale"],
|
|
1894
|
+
invalidatedBy: ["render.density", "canvas.size", "canvas.renderScale"],
|
|
1895
|
+
kind: "rasterize",
|
|
1896
|
+
output: "preview",
|
|
1897
|
+
quality: "retina",
|
|
1898
|
+
runsOn: "worker",
|
|
1899
|
+
},
|
|
1900
|
+
],
|
|
1901
|
+
},
|
|
1902
|
+
scenarios: createTextRendererScenarios(),
|
|
1903
|
+
usesCustomRenderer: true,
|
|
1904
|
+
workloadTargets: ["render.density"],
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual(
|
|
1908
|
+
expect.arrayContaining([
|
|
1909
|
+
'rendererPipeline viewport-zoom cannot both invalidate and mustNotInvalidate pass "dense-background".',
|
|
1910
|
+
'rendererPipeline viewport-zoom must not invalidate expensive pass "dense-background" (rasterize). Move viewport work to transforms/uniforms or explain it through a cheaper pass.',
|
|
1911
|
+
]),
|
|
1912
|
+
);
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
it("requires media decoding pipelines to include media-import coverage", () => {
|
|
1916
|
+
const config = defineToolcraftPerformance({
|
|
1917
|
+
rendererStrategy: "webgl",
|
|
1918
|
+
rendererTechnique: {
|
|
1919
|
+
exportRenderer: "webgl",
|
|
1920
|
+
fidelityRisks: ["large source media must preserve color"],
|
|
1921
|
+
performanceRisks: ["source decode can be expensive"],
|
|
1922
|
+
previewRenderer: "webgl",
|
|
1923
|
+
productRepresentation: "pixel",
|
|
1924
|
+
rendererStrategy: "webgl",
|
|
1925
|
+
rendererWorkload: "pixel-output",
|
|
1926
|
+
sourceRepresentation: "image-media",
|
|
1927
|
+
whyNotAlternativeStrategies: ["canvas-2d would run per-pixel work on the main thread"],
|
|
1928
|
+
},
|
|
1929
|
+
rendererWorkload: "pixel-output",
|
|
1930
|
+
rendererPipeline: createMediaRendererPipeline(),
|
|
1931
|
+
scenarios: createTextRendererScenarios(),
|
|
1932
|
+
usesCustomRenderer: true,
|
|
1933
|
+
workloadTargets: ["render.density"],
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1937
|
+
'rendererPipeline pass "source-decode" decodes media, so performance scenarios must include media-import coverage.',
|
|
1938
|
+
);
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1941
|
+
it("requires workload targets to appear in renderer pipeline invalidation targets", () => {
|
|
1942
|
+
const config = defineToolcraftPerformance({
|
|
1943
|
+
rendererStrategy: "canvas-2d",
|
|
1944
|
+
rendererTechnique: {
|
|
1945
|
+
exportRenderer: "canvas-2d",
|
|
1946
|
+
fidelityRisks: ["text output must stay crisp"],
|
|
1947
|
+
performanceRisks: ["density changes can relayout text"],
|
|
1948
|
+
previewRenderer: "canvas-2d",
|
|
1949
|
+
productRepresentation: "text",
|
|
1950
|
+
rendererStrategy: "canvas-2d",
|
|
1951
|
+
rendererWorkload: "text-output",
|
|
1952
|
+
sourceRepresentation: "dom-text",
|
|
1953
|
+
whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
|
|
1954
|
+
},
|
|
1955
|
+
rendererWorkload: "text-output",
|
|
1956
|
+
rendererPipeline: {
|
|
1957
|
+
...createTextRendererPipeline(),
|
|
1958
|
+
interactionInvalidation: [
|
|
1959
|
+
{
|
|
1960
|
+
interaction: "control-change",
|
|
1961
|
+
invalidates: ["text-layout"],
|
|
1962
|
+
targets: ["render.mode"],
|
|
1963
|
+
},
|
|
1964
|
+
],
|
|
1965
|
+
},
|
|
1966
|
+
scenarios: createTextRendererScenarios(),
|
|
1967
|
+
usesCustomRenderer: true,
|
|
1968
|
+
workloadTargets: ["render.density"],
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1972
|
+
"Performance workload target render.density must appear in rendererPipeline interactionInvalidation targets.",
|
|
1973
|
+
);
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
it("requires performance scenario interactions to appear in renderer pipeline invalidation", () => {
|
|
1977
|
+
const config = defineToolcraftPerformance({
|
|
1978
|
+
rendererStrategy: "canvas-2d",
|
|
1979
|
+
rendererTechnique: {
|
|
1980
|
+
exportRenderer: "canvas-2d",
|
|
1981
|
+
fidelityRisks: ["text output must stay crisp"],
|
|
1982
|
+
performanceRisks: ["density changes can relayout text"],
|
|
1983
|
+
previewRenderer: "canvas-2d",
|
|
1984
|
+
productRepresentation: "text",
|
|
1985
|
+
rendererStrategy: "canvas-2d",
|
|
1986
|
+
rendererWorkload: "text-output",
|
|
1987
|
+
sourceRepresentation: "dom-text",
|
|
1988
|
+
whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
|
|
1989
|
+
},
|
|
1990
|
+
rendererWorkload: "text-output",
|
|
1991
|
+
rendererPipeline: {
|
|
1992
|
+
...createTextRendererPipeline(),
|
|
1993
|
+
interactionInvalidation: [
|
|
1994
|
+
{
|
|
1995
|
+
interaction: "control-drag",
|
|
1996
|
+
invalidates: ["text-layout"],
|
|
1997
|
+
targets: ["render.density"],
|
|
1998
|
+
},
|
|
1999
|
+
{
|
|
2000
|
+
interaction: "control-change",
|
|
2001
|
+
invalidates: ["text-layout"],
|
|
2002
|
+
targets: ["render.mode"],
|
|
2003
|
+
},
|
|
2004
|
+
],
|
|
2005
|
+
},
|
|
2006
|
+
scenarios: createTextRendererScenarios(),
|
|
2007
|
+
usesCustomRenderer: true,
|
|
2008
|
+
workloadTargets: ["render.density"],
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
2012
|
+
"Performance scenario viewport-zoom-stress exercises viewport-zoom, so rendererPipeline.interactionInvalidation must declare that interaction.",
|
|
2013
|
+
);
|
|
2014
|
+
});
|
|
2015
|
+
|
|
2016
|
+
it("requires timeline playback scenarios to appear in renderer pipeline invalidation", () => {
|
|
2017
|
+
const config = defineToolcraftPerformance({
|
|
2018
|
+
rendererStrategy: "svg",
|
|
2019
|
+
rendererTechnique: {
|
|
2020
|
+
exportRenderer: "svg",
|
|
2021
|
+
fidelityRisks: ["timeline playback must preserve vector motion"],
|
|
2022
|
+
performanceRisks: ["timeline playback can jank at high density"],
|
|
2023
|
+
previewRenderer: "svg",
|
|
2024
|
+
productRepresentation: "vector",
|
|
2025
|
+
rendererStrategy: "svg",
|
|
2026
|
+
rendererWorkload: "vector-output",
|
|
2027
|
+
sourceRepresentation: "procedural-data",
|
|
2028
|
+
whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
|
|
2029
|
+
},
|
|
2030
|
+
rendererWorkload: "vector-output",
|
|
2031
|
+
rendererPipeline: {
|
|
2032
|
+
...createAnimatedVectorRendererPipeline(),
|
|
2033
|
+
interactionInvalidation: createAnimatedVectorRendererPipeline().interactionInvalidation.filter(
|
|
2034
|
+
(entry) => entry.interaction !== "timeline-playback",
|
|
2035
|
+
),
|
|
2036
|
+
},
|
|
2037
|
+
scenarios: [
|
|
2038
|
+
...createTextRendererScenarios(),
|
|
2039
|
+
{
|
|
2040
|
+
automated: true,
|
|
2041
|
+
automatedTestName: "perf: timeline playback stays smooth",
|
|
2042
|
+
browser: true,
|
|
2043
|
+
browserTestName: "browser perf: timeline playback stays smooth",
|
|
2044
|
+
budget: { maxFrameGapMs: 80, maxLongTaskMs: 80 },
|
|
2045
|
+
expectedObservable: "Timeline playback advances without blocking frames.",
|
|
2046
|
+
fixture: "timeline playback fixture",
|
|
2047
|
+
id: "timeline-playback",
|
|
2048
|
+
interaction: "timeline-playback",
|
|
2049
|
+
stress: true,
|
|
2050
|
+
stressFixture: createCombinedRendererStressFixture(),
|
|
2051
|
+
target: "timeline.currentTime",
|
|
2052
|
+
uiSelector: '[data-slot="timeline-playback-handle"]',
|
|
2053
|
+
workload: false,
|
|
2054
|
+
},
|
|
2055
|
+
],
|
|
2056
|
+
usesCustomRenderer: true,
|
|
2057
|
+
workloadTargets: ["render.density"],
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
2061
|
+
"Performance scenario timeline-playback exercises timeline-playback, so rendererPipeline.interactionInvalidation must declare that interaction.",
|
|
2062
|
+
);
|
|
2063
|
+
});
|
|
2064
|
+
|
|
2065
|
+
it("validates timeline scrub scenarios through renderer pipeline invalidation", () => {
|
|
2066
|
+
const config = defineToolcraftPerformance({
|
|
2067
|
+
rendererStrategy: "svg",
|
|
2068
|
+
rendererTechnique: {
|
|
2069
|
+
exportRenderer: "svg",
|
|
2070
|
+
fidelityRisks: ["timeline scrub must preserve vector motion"],
|
|
2071
|
+
performanceRisks: ["timeline scrub can jank at high density"],
|
|
2072
|
+
previewRenderer: "svg",
|
|
2073
|
+
productRepresentation: "vector",
|
|
2074
|
+
rendererStrategy: "svg",
|
|
2075
|
+
rendererWorkload: "vector-output",
|
|
2076
|
+
sourceRepresentation: "procedural-data",
|
|
2077
|
+
whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
|
|
2078
|
+
},
|
|
2079
|
+
rendererWorkload: "vector-output",
|
|
2080
|
+
rendererPipeline: createAnimatedVectorRendererPipeline(),
|
|
2081
|
+
scenarios: [
|
|
2082
|
+
...createTextRendererScenarios(),
|
|
2083
|
+
{
|
|
2084
|
+
automated: true,
|
|
2085
|
+
automatedTestName: "perf: timeline scrub stays responsive",
|
|
2086
|
+
browser: true,
|
|
2087
|
+
browserTestName: "browser perf: timeline scrub stays responsive",
|
|
2088
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500, maxLongTaskMs: 80 },
|
|
2089
|
+
expectedObservable: "Timeline scrub updates output without blocking frames.",
|
|
2090
|
+
fixture: "timeline scrub fixture",
|
|
2091
|
+
id: "timeline-scrub",
|
|
2092
|
+
interaction: "timeline-scrub",
|
|
2093
|
+
stress: true,
|
|
2094
|
+
stressFixture: createCombinedRendererStressFixture(),
|
|
2095
|
+
target: "timeline.currentTime",
|
|
2096
|
+
uiSelector: '[data-slot="timeline-playback-handle"]',
|
|
2097
|
+
workload: false,
|
|
2098
|
+
},
|
|
2099
|
+
],
|
|
2100
|
+
usesCustomRenderer: true,
|
|
2101
|
+
workloadTargets: ["render.density"],
|
|
2102
|
+
});
|
|
2103
|
+
|
|
2104
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
it("requires timeline performance scenarios to name the real timeline UI", () => {
|
|
2108
|
+
const config = defineToolcraftPerformance({
|
|
2109
|
+
rendererStrategy: "svg",
|
|
2110
|
+
rendererTechnique: {
|
|
2111
|
+
exportRenderer: "svg",
|
|
2112
|
+
fidelityRisks: ["timeline scrub must preserve vector motion"],
|
|
2113
|
+
performanceRisks: ["timeline scrub can jank at high density"],
|
|
2114
|
+
previewRenderer: "svg",
|
|
2115
|
+
productRepresentation: "vector",
|
|
2116
|
+
rendererStrategy: "svg",
|
|
2117
|
+
rendererWorkload: "vector-output",
|
|
2118
|
+
sourceRepresentation: "procedural-data",
|
|
2119
|
+
whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
|
|
2120
|
+
},
|
|
2121
|
+
rendererWorkload: "vector-output",
|
|
2122
|
+
rendererPipeline: createAnimatedVectorRendererPipeline(),
|
|
2123
|
+
scenarios: [
|
|
2124
|
+
...createTextRendererScenarios(),
|
|
2125
|
+
{
|
|
2126
|
+
automated: true,
|
|
2127
|
+
automatedTestName: "perf: timeline scrub stays responsive",
|
|
2128
|
+
browser: true,
|
|
2129
|
+
browserTestName: "browser perf: timeline scrub stays responsive",
|
|
2130
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500, maxLongTaskMs: 80 },
|
|
2131
|
+
expectedObservable: "Timeline scrub updates output without blocking frames.",
|
|
2132
|
+
fixture: "timeline scrub fixture",
|
|
2133
|
+
id: "timeline-scrub",
|
|
2134
|
+
interaction: "timeline-scrub",
|
|
2135
|
+
stress: true,
|
|
2136
|
+
stressFixture: createCombinedRendererStressFixture(),
|
|
2137
|
+
target: "timeline.currentTime",
|
|
2138
|
+
workload: false,
|
|
2139
|
+
},
|
|
2140
|
+
],
|
|
2141
|
+
usesCustomRenderer: true,
|
|
2142
|
+
workloadTargets: ["render.density"],
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
2146
|
+
"timeline-scrub timeline-scrub scenario must declare controlLabel or uiSelector for its real browser interaction.",
|
|
2147
|
+
);
|
|
2148
|
+
});
|
|
2149
|
+
|
|
2150
|
+
it("validates mask drag scenarios through renderer pipeline invalidation", () => {
|
|
2151
|
+
const config = defineToolcraftPerformance({
|
|
2152
|
+
rendererStrategy: "svg",
|
|
2153
|
+
rendererTechnique: {
|
|
2154
|
+
exportRenderer: "svg",
|
|
2155
|
+
fidelityRisks: ["mask handles must stay aligned with vector output"],
|
|
2156
|
+
performanceRisks: ["dragging a mask handle can rebuild output too often"],
|
|
2157
|
+
previewRenderer: "svg",
|
|
2158
|
+
productRepresentation: "vector",
|
|
2159
|
+
rendererStrategy: "svg",
|
|
2160
|
+
rendererWorkload: "vector-output",
|
|
2161
|
+
sourceRepresentation: "procedural-data",
|
|
2162
|
+
whyNotAlternativeStrategies: ["svg preserves semantic handle geometry"],
|
|
2163
|
+
},
|
|
2164
|
+
rendererWorkload: "vector-output",
|
|
2165
|
+
rendererPipeline: {
|
|
2166
|
+
...createAnimatedVectorRendererPipeline(),
|
|
2167
|
+
interactionInvalidation: [
|
|
2168
|
+
...createAnimatedVectorRendererPipeline().interactionInvalidation,
|
|
2169
|
+
{
|
|
2170
|
+
interaction: "mask-drag",
|
|
2171
|
+
invalidates: ["animation-composite"],
|
|
2172
|
+
mustNotInvalidate: ["vector-build"],
|
|
2173
|
+
targets: ["mask.anchor"],
|
|
2174
|
+
},
|
|
2175
|
+
],
|
|
2176
|
+
},
|
|
2177
|
+
scenarios: [
|
|
2178
|
+
...createTextRendererScenarios(),
|
|
2179
|
+
{
|
|
2180
|
+
automated: true,
|
|
2181
|
+
automatedTestName: "perf: mask drag stays responsive",
|
|
2182
|
+
browser: true,
|
|
2183
|
+
browserTestName: "browser perf: mask drag stays responsive",
|
|
2184
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500, maxLongTaskMs: 80 },
|
|
2185
|
+
expectedObservable: "Dragging the mask handle updates output without blocking frames.",
|
|
2186
|
+
fixture: "mask drag fixture",
|
|
2187
|
+
id: "mask-drag",
|
|
2188
|
+
interaction: "mask-drag",
|
|
2189
|
+
stress: true,
|
|
2190
|
+
stressFixture: createCombinedRendererStressFixture(),
|
|
2191
|
+
target: "mask.anchor",
|
|
2192
|
+
uiSelector: '[data-toolcraft-canvas-handle][data-testid="mask-anchor"]',
|
|
2193
|
+
workload: false,
|
|
2194
|
+
},
|
|
2195
|
+
],
|
|
2196
|
+
usesCustomRenderer: true,
|
|
2197
|
+
workloadTargets: ["render.density"],
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
|
|
2201
|
+
});
|
|
2202
|
+
|
|
837
2203
|
it("rejects renderer technique mismatches and missing explanations", () => {
|
|
838
2204
|
const config = defineToolcraftPerformance({
|
|
839
2205
|
rendererStrategy: "canvas-2d",
|
|
@@ -909,9 +2275,11 @@ describe("Toolcraft template performance contract", () => {
|
|
|
909
2275
|
sourceRepresentation: "procedural-data",
|
|
910
2276
|
whyNotAlternativeStrategies: [
|
|
911
2277
|
"rendering thousands of background dots as DOM nodes would be expensive",
|
|
2278
|
+
"WebGL/WebGPU stress evidence is not needed yet because measured canvas-2d preview budgets pass for this dense static background.",
|
|
912
2279
|
],
|
|
913
2280
|
},
|
|
914
2281
|
rendererWorkload: "simple-composition",
|
|
2282
|
+
rendererPipeline: createDenseCanvasPipeline(),
|
|
915
2283
|
scenarios: createTextRendererScenarios(),
|
|
916
2284
|
usesCustomRenderer: true,
|
|
917
2285
|
workloadTargets: ["render.density"],
|