@pixel-point/toolcraft 0.0.7 → 0.0.8
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/templates/runtime/contracts/component-contracts.test.ts +84 -12
- package/templates/runtime/contracts/component-contracts.ts +40 -17
- package/templates/runtime/contracts/decision-contracts.ts +1 -1
- 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 +404 -8
- package/templates/runtime/react/controls-panel.tsx +90 -4
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +1 -0
- package/templates/runtime/schema/define-toolcraft.ts +4 -2
- package/templates/runtime/schema/types.ts +4 -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 +1282 -48
- package/templates/runtime/testing/performance.ts +676 -39
- package/templates/starter/AGENTS.md +6 -5
- package/templates/starter/docs/toolcraft/acceptance-testing.md +11 -5
- package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -2
- package/templates/starter/docs/toolcraft/component-rules.md +16 -7
- package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
- package/templates/starter/docs/toolcraft/performance.md +47 -5
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +6 -6
- 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/package.json +3 -0
- package/templates/starter/src/app/starter-acceptance.test.ts +529 -19
- package/templates/starter/src/app/starter-acceptance.ts +269 -12
- package/templates/starter/src/app/starter-performance.test.ts +66 -5
- package/templates/ui/components/controls/actions/actions-control.tsx +3 -5
- 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 +1 -0
- package/templates/ui/components/panel/panel-section.tsx +53 -1
|
@@ -102,6 +102,34 @@ const largeTextSchema = defineToolcraft({
|
|
|
102
102
|
},
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
const mediaUploadSchema = defineToolcraft({
|
|
106
|
+
canvas: {
|
|
107
|
+
enabled: true,
|
|
108
|
+
sizing: { mode: "intrinsic-media" },
|
|
109
|
+
upload: true,
|
|
110
|
+
},
|
|
111
|
+
panels: {
|
|
112
|
+
controls: {
|
|
113
|
+
sections: [
|
|
114
|
+
{
|
|
115
|
+
controls: {
|
|
116
|
+
source: {
|
|
117
|
+
defaultValue: null,
|
|
118
|
+
label: "Source image",
|
|
119
|
+
performanceReason: "Uploaded media dimensions change renderer workload.",
|
|
120
|
+
performanceRole: "responsiveness",
|
|
121
|
+
target: "source.image",
|
|
122
|
+
type: "fileDrop",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
title: "Source",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
title: "Runtime Controls",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
105
133
|
function createViewportZoomStressScenario() {
|
|
106
134
|
return {
|
|
107
135
|
automated: true,
|
|
@@ -115,6 +143,7 @@ function createViewportZoomStressScenario() {
|
|
|
115
143
|
id: "viewport-zoom-stress",
|
|
116
144
|
interaction: "viewport-zoom-stress" as const,
|
|
117
145
|
stress: true,
|
|
146
|
+
stressFixture: createCombinedRendererStressFixture(),
|
|
118
147
|
target: "canvas.viewport",
|
|
119
148
|
workload: false,
|
|
120
149
|
};
|
|
@@ -128,6 +157,24 @@ function createMaxValueStressFixture(value: unknown, reason = "Maximum workload
|
|
|
128
157
|
};
|
|
129
158
|
}
|
|
130
159
|
|
|
160
|
+
function createCustomStressFixture(
|
|
161
|
+
value: unknown = { density: 12 },
|
|
162
|
+
reason = "Combined renderer stress state.",
|
|
163
|
+
) {
|
|
164
|
+
return {
|
|
165
|
+
kind: "custom" as const,
|
|
166
|
+
reason,
|
|
167
|
+
value,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function createCombinedRendererStressFixture() {
|
|
172
|
+
return createCustomStressFixture(
|
|
173
|
+
{ density: 12, zoom: "toolbar" },
|
|
174
|
+
"Stress checks must run with the densest product output visible.",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
131
178
|
function createLargeTextStressValue(): string {
|
|
132
179
|
return Array.from(
|
|
133
180
|
{ length: 1_000 },
|
|
@@ -146,6 +193,271 @@ function createLargeTextStressFixture() {
|
|
|
146
193
|
};
|
|
147
194
|
}
|
|
148
195
|
|
|
196
|
+
function createMediaStressFixture(
|
|
197
|
+
value: { height: number; width: number } = { height: 1080, width: 1920 },
|
|
198
|
+
reason = "Large uploaded source media is the heaviest realistic import fixture.",
|
|
199
|
+
) {
|
|
200
|
+
return {
|
|
201
|
+
kind: "media" as const,
|
|
202
|
+
reason,
|
|
203
|
+
value,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function createTextRendererPipeline() {
|
|
208
|
+
return {
|
|
209
|
+
interactionInvalidation: [
|
|
210
|
+
{
|
|
211
|
+
interaction: "control-drag",
|
|
212
|
+
invalidates: ["text-layout"],
|
|
213
|
+
targets: ["render.density"],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
interaction: "control-change",
|
|
217
|
+
invalidates: ["text-layout"],
|
|
218
|
+
targets: ["render.mode"],
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
interaction: "viewport-zoom",
|
|
222
|
+
invalidates: [],
|
|
223
|
+
mustNotInvalidate: ["text-layout"],
|
|
224
|
+
targets: ["canvas.viewport"],
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
passes: [
|
|
228
|
+
{
|
|
229
|
+
cacheKey: ["product.content", "render.density", "render.mode"],
|
|
230
|
+
id: "text-layout",
|
|
231
|
+
inputs: ["product.content", "render.density", "render.mode"],
|
|
232
|
+
invalidatedBy: ["product.content", "render.density", "render.mode"],
|
|
233
|
+
kind: "text-layout",
|
|
234
|
+
output: "preview",
|
|
235
|
+
quality: "full",
|
|
236
|
+
runsOn: "main",
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
} as const;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function createAnimatedVectorRendererPipeline() {
|
|
243
|
+
return {
|
|
244
|
+
interactionInvalidation: [
|
|
245
|
+
{
|
|
246
|
+
interaction: "control-drag",
|
|
247
|
+
invalidates: ["vector-build"],
|
|
248
|
+
targets: ["render.density"],
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
interaction: "control-change",
|
|
252
|
+
invalidates: ["vector-build"],
|
|
253
|
+
targets: ["render.mode"],
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
interaction: "animation-frame",
|
|
257
|
+
invalidates: ["animation-composite"],
|
|
258
|
+
mustNotInvalidate: ["vector-build"],
|
|
259
|
+
targets: ["animation.time"],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
interaction: "timeline-playback",
|
|
263
|
+
invalidates: ["animation-composite"],
|
|
264
|
+
mustNotInvalidate: ["vector-build"],
|
|
265
|
+
targets: ["timeline.currentTime"],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
interaction: "timeline-scrub",
|
|
269
|
+
invalidates: ["animation-composite"],
|
|
270
|
+
mustNotInvalidate: ["vector-build"],
|
|
271
|
+
targets: ["timeline.currentTime"],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
interaction: "viewport-drag",
|
|
275
|
+
invalidates: [],
|
|
276
|
+
mustNotInvalidate: ["vector-build", "animation-composite"],
|
|
277
|
+
targets: ["canvas.viewport"],
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
interaction: "viewport-zoom",
|
|
281
|
+
invalidates: [],
|
|
282
|
+
mustNotInvalidate: ["vector-build", "animation-composite"],
|
|
283
|
+
targets: ["canvas.viewport"],
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
passes: [
|
|
287
|
+
{
|
|
288
|
+
id: "vector-build",
|
|
289
|
+
inputs: ["render.density", "render.mode"],
|
|
290
|
+
invalidatedBy: ["render.density", "render.mode"],
|
|
291
|
+
kind: "vector-build",
|
|
292
|
+
output: "preview",
|
|
293
|
+
quality: "full",
|
|
294
|
+
runsOn: "main",
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
cacheKey: ["vector-build", "animation.time", "timeline.currentTime"],
|
|
298
|
+
id: "animation-composite",
|
|
299
|
+
inputs: ["vector-build", "animation.time", "timeline.currentTime"],
|
|
300
|
+
invalidatedBy: ["animation.time", "timeline.currentTime"],
|
|
301
|
+
kind: "composite",
|
|
302
|
+
output: "preview",
|
|
303
|
+
quality: "preview",
|
|
304
|
+
runsOn: "main",
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
} as const;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function createDenseCanvasPipeline() {
|
|
311
|
+
return {
|
|
312
|
+
interactionInvalidation: [
|
|
313
|
+
{
|
|
314
|
+
interaction: "control-drag",
|
|
315
|
+
invalidates: ["dense-background", "export-composite"],
|
|
316
|
+
targets: ["render.density"],
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
interaction: "control-change",
|
|
320
|
+
invalidates: ["export-composite"],
|
|
321
|
+
targets: ["render.mode"],
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
interaction: "viewport-zoom",
|
|
325
|
+
invalidates: [],
|
|
326
|
+
mustNotInvalidate: ["dense-background", "export-composite"],
|
|
327
|
+
targets: ["canvas.viewport"],
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
interaction: "export",
|
|
331
|
+
invalidates: ["export-composite"],
|
|
332
|
+
mustNotInvalidate: ["dense-background"],
|
|
333
|
+
targets: ["export.image.resolution"],
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
passes: [
|
|
337
|
+
{
|
|
338
|
+
cacheKey: ["render.density", "canvas.size", "canvas.renderScale"],
|
|
339
|
+
id: "dense-background",
|
|
340
|
+
inputs: ["render.density", "canvas.size", "canvas.renderScale"],
|
|
341
|
+
invalidatedBy: ["render.density", "canvas.size", "canvas.renderScale"],
|
|
342
|
+
kind: "rasterize",
|
|
343
|
+
output: "preview",
|
|
344
|
+
quality: "retina",
|
|
345
|
+
runsOn: "worker",
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
id: "semantic-foreground",
|
|
349
|
+
inputs: ["product.geometry"],
|
|
350
|
+
invalidatedBy: ["product.geometry"],
|
|
351
|
+
kind: "vector-build",
|
|
352
|
+
output: "preview",
|
|
353
|
+
quality: "full",
|
|
354
|
+
runsOn: "main",
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
cacheKey: ["dense-background", "semantic-foreground", "export.image.resolution"],
|
|
358
|
+
id: "export-composite",
|
|
359
|
+
inputs: ["dense-background", "semantic-foreground", "export.image.resolution"],
|
|
360
|
+
invalidatedBy: ["dense-background", "semantic-foreground", "export.image.resolution"],
|
|
361
|
+
kind: "composite",
|
|
362
|
+
output: "export",
|
|
363
|
+
quality: "export",
|
|
364
|
+
runsOn: "export-only",
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
} as const;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function createPixelRendererPipeline() {
|
|
371
|
+
return {
|
|
372
|
+
interactionInvalidation: [
|
|
373
|
+
{
|
|
374
|
+
interaction: "control-drag",
|
|
375
|
+
invalidates: ["pixel-transform"],
|
|
376
|
+
targets: ["render.density"],
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
interaction: "control-change",
|
|
380
|
+
invalidates: ["pixel-transform"],
|
|
381
|
+
targets: ["render.mode"],
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
interaction: "viewport-zoom",
|
|
385
|
+
invalidates: [],
|
|
386
|
+
mustNotInvalidate: ["pixel-transform"],
|
|
387
|
+
targets: ["canvas.viewport"],
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
passes: [
|
|
391
|
+
{
|
|
392
|
+
cacheKey: ["render.density", "render.mode", "canvas.size", "canvas.renderScale"],
|
|
393
|
+
id: "pixel-transform",
|
|
394
|
+
inputs: ["render.density", "render.mode", "canvas.size", "canvas.renderScale"],
|
|
395
|
+
invalidatedBy: ["render.density", "render.mode", "canvas.size", "canvas.renderScale"],
|
|
396
|
+
kind: "pixel-transform",
|
|
397
|
+
output: "preview",
|
|
398
|
+
quality: "retina",
|
|
399
|
+
runsOn: "worker",
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
} as const;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function createMediaRendererPipeline() {
|
|
406
|
+
return {
|
|
407
|
+
interactionInvalidation: [
|
|
408
|
+
{
|
|
409
|
+
interaction: "media-import",
|
|
410
|
+
invalidates: ["source-decode", "media-preprocess"],
|
|
411
|
+
targets: ["source.image"],
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
interaction: "control-drag",
|
|
415
|
+
invalidates: ["effect-composite"],
|
|
416
|
+
mustNotInvalidate: ["source-decode", "media-preprocess"],
|
|
417
|
+
targets: ["render.density"],
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
interaction: "viewport-zoom",
|
|
421
|
+
invalidates: [],
|
|
422
|
+
mustNotInvalidate: ["source-decode", "media-preprocess", "effect-composite"],
|
|
423
|
+
targets: ["canvas.viewport"],
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
passes: [
|
|
427
|
+
{
|
|
428
|
+
cacheKey: ["source.image.id", "source.image.width", "source.image.height"],
|
|
429
|
+
id: "source-decode",
|
|
430
|
+
inputs: ["source.image"],
|
|
431
|
+
invalidatedBy: ["source.image"],
|
|
432
|
+
kind: "decode",
|
|
433
|
+
output: "source",
|
|
434
|
+
quality: "full",
|
|
435
|
+
runsOn: "worker",
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
cacheKey: ["source-decode", "source.image.id"],
|
|
439
|
+
id: "media-preprocess",
|
|
440
|
+
inputs: ["source-decode"],
|
|
441
|
+
invalidatedBy: ["source-decode"],
|
|
442
|
+
kind: "preprocess",
|
|
443
|
+
output: "intermediate",
|
|
444
|
+
quality: "full",
|
|
445
|
+
runsOn: "worker-or-gpu",
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
cacheKey: ["media-preprocess", "render.density"],
|
|
449
|
+
id: "effect-composite",
|
|
450
|
+
inputs: ["media-preprocess", "render.density"],
|
|
451
|
+
invalidatedBy: ["media-preprocess", "render.density"],
|
|
452
|
+
kind: "composite",
|
|
453
|
+
output: "preview",
|
|
454
|
+
quality: "retina",
|
|
455
|
+
runsOn: "gpu",
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
} as const;
|
|
459
|
+
}
|
|
460
|
+
|
|
149
461
|
function createTextRendererScenarios(
|
|
150
462
|
options: { includeStressPreview?: boolean; includeViewportZoomStress?: boolean } = {},
|
|
151
463
|
) {
|
|
@@ -164,6 +476,9 @@ function createTextRendererScenarios(
|
|
|
164
476
|
id: "text-preview-render",
|
|
165
477
|
interaction: "preview-render" as const,
|
|
166
478
|
stress: includeStressPreview,
|
|
479
|
+
...(includeStressPreview
|
|
480
|
+
? { stressFixture: createCombinedRendererStressFixture() }
|
|
481
|
+
: {}),
|
|
167
482
|
workload: false,
|
|
168
483
|
},
|
|
169
484
|
{
|
|
@@ -183,6 +498,7 @@ function createTextRendererScenarios(
|
|
|
183
498
|
),
|
|
184
499
|
target: "render.density",
|
|
185
500
|
values: { default: 4, max: 12, min: 1 },
|
|
501
|
+
workloadFixture: createLargeTextStressFixture(),
|
|
186
502
|
workload: true,
|
|
187
503
|
},
|
|
188
504
|
{
|
|
@@ -228,6 +544,10 @@ function createAnimationFrameScenario() {
|
|
|
228
544
|
id: "animation-frame-loop",
|
|
229
545
|
interaction: "animation-frame" as const,
|
|
230
546
|
stress: true,
|
|
547
|
+
stressFixture: createCustomStressFixture(
|
|
548
|
+
{ animation: "fastest", density: 12 },
|
|
549
|
+
"Animation stress must sample the heaviest animated output.",
|
|
550
|
+
),
|
|
231
551
|
workload: false,
|
|
232
552
|
};
|
|
233
553
|
}
|
|
@@ -244,6 +564,10 @@ function createAnimationViewportDragScenario() {
|
|
|
244
564
|
id: "animation-viewport-drag",
|
|
245
565
|
interaction: "animation-viewport-drag" as const,
|
|
246
566
|
stress: true,
|
|
567
|
+
stressFixture: createCustomStressFixture(
|
|
568
|
+
{ animation: "fastest", density: 12 },
|
|
569
|
+
"Animated viewport drag must run while the heaviest animated output is visible.",
|
|
570
|
+
),
|
|
247
571
|
target: "canvas.viewport",
|
|
248
572
|
workload: false,
|
|
249
573
|
};
|
|
@@ -264,6 +588,7 @@ function createAnimatedVectorRendererConfig(options: { includeViewportDrag: bool
|
|
|
264
588
|
whyNotAlternativeStrategies: ["svg preserves semantic vector output for this fixture"],
|
|
265
589
|
},
|
|
266
590
|
rendererWorkload: "vector-output",
|
|
591
|
+
rendererPipeline: createAnimatedVectorRendererPipeline(),
|
|
267
592
|
scenarios: [
|
|
268
593
|
...createTextRendererScenarios(),
|
|
269
594
|
createAnimationFrameScenario(),
|
|
@@ -570,6 +895,132 @@ describe("Toolcraft template performance contract", () => {
|
|
|
570
895
|
);
|
|
571
896
|
});
|
|
572
897
|
|
|
898
|
+
it("requires stress scenarios to declare an exact stress fixture value", () => {
|
|
899
|
+
const config = defineToolcraftPerformance({
|
|
900
|
+
rendererStrategy: "none",
|
|
901
|
+
rendererWorkload: "none",
|
|
902
|
+
scenarios: [
|
|
903
|
+
{
|
|
904
|
+
automated: true,
|
|
905
|
+
automatedTestName: "perf: preview render stays under budget",
|
|
906
|
+
browser: true,
|
|
907
|
+
browserTestName: "browser perf: preview render stays under budget",
|
|
908
|
+
budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
|
|
909
|
+
expectedObservable: "Preview renders the heaviest output without freezing.",
|
|
910
|
+
fixture: "combined heavy renderer fixture",
|
|
911
|
+
id: "preview-render",
|
|
912
|
+
interaction: "preview-render",
|
|
913
|
+
stress: true,
|
|
914
|
+
workload: false,
|
|
915
|
+
},
|
|
916
|
+
],
|
|
917
|
+
usesCustomRenderer: false,
|
|
918
|
+
workloadTargets: [],
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
|
|
922
|
+
"preview-render stress scenario must declare stressFixture with the real heaviest state used by browser performance tests.",
|
|
923
|
+
);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it("rejects stress fixtures without a browser-applicable value", () => {
|
|
927
|
+
const config = defineToolcraftPerformance({
|
|
928
|
+
rendererStrategy: "none",
|
|
929
|
+
rendererWorkload: "none",
|
|
930
|
+
scenarios: [
|
|
931
|
+
{
|
|
932
|
+
automated: true,
|
|
933
|
+
automatedTestName: "perf: preview render stays under budget",
|
|
934
|
+
browser: true,
|
|
935
|
+
browserTestName: "browser perf: preview render stays under budget",
|
|
936
|
+
budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
|
|
937
|
+
expectedObservable: "Preview renders the heaviest output without freezing.",
|
|
938
|
+
fixture: "combined heavy renderer fixture",
|
|
939
|
+
id: "preview-render",
|
|
940
|
+
interaction: "preview-render",
|
|
941
|
+
stress: true,
|
|
942
|
+
stressFixture: {
|
|
943
|
+
kind: "custom",
|
|
944
|
+
reason: "Combined renderer stress state.",
|
|
945
|
+
},
|
|
946
|
+
workload: false,
|
|
947
|
+
},
|
|
948
|
+
],
|
|
949
|
+
usesCustomRenderer: false,
|
|
950
|
+
workloadTargets: [],
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
|
|
954
|
+
"preview-render stressFixture must include value so browser tests can apply the exact heavy state.",
|
|
955
|
+
);
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
it("requires custom stress fixtures to be object-shaped heavy states", () => {
|
|
959
|
+
const config = defineToolcraftPerformance({
|
|
960
|
+
rendererStrategy: "none",
|
|
961
|
+
rendererWorkload: "none",
|
|
962
|
+
scenarios: [
|
|
963
|
+
{
|
|
964
|
+
automated: true,
|
|
965
|
+
automatedTestName: "perf: preview render stays under budget",
|
|
966
|
+
browser: true,
|
|
967
|
+
browserTestName: "browser perf: preview render stays under budget",
|
|
968
|
+
budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
|
|
969
|
+
expectedObservable: "Preview renders the heaviest output without freezing.",
|
|
970
|
+
fixture: "combined heavy renderer fixture",
|
|
971
|
+
id: "preview-render",
|
|
972
|
+
interaction: "preview-render",
|
|
973
|
+
stress: true,
|
|
974
|
+
stressFixture: {
|
|
975
|
+
kind: "custom",
|
|
976
|
+
reason: "Combined renderer stress state.",
|
|
977
|
+
value: 12,
|
|
978
|
+
},
|
|
979
|
+
workload: false,
|
|
980
|
+
},
|
|
981
|
+
],
|
|
982
|
+
usesCustomRenderer: false,
|
|
983
|
+
workloadTargets: [],
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
|
|
987
|
+
"preview-render custom stressFixture.value must be an object with one key per heavy state part so browser tests can apply every key.",
|
|
988
|
+
);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
it("rejects empty custom stress fixture objects", () => {
|
|
992
|
+
const config = defineToolcraftPerformance({
|
|
993
|
+
rendererStrategy: "none",
|
|
994
|
+
rendererWorkload: "none",
|
|
995
|
+
scenarios: [
|
|
996
|
+
{
|
|
997
|
+
automated: true,
|
|
998
|
+
automatedTestName: "perf: preview render stays under budget",
|
|
999
|
+
browser: true,
|
|
1000
|
+
browserTestName: "browser perf: preview render stays under budget",
|
|
1001
|
+
budget: { maxLongTaskMs: 80, maxPreviewMs: 1000 },
|
|
1002
|
+
expectedObservable: "Preview renders the heaviest output without freezing.",
|
|
1003
|
+
fixture: "combined heavy renderer fixture",
|
|
1004
|
+
id: "preview-render",
|
|
1005
|
+
interaction: "preview-render",
|
|
1006
|
+
stress: true,
|
|
1007
|
+
stressFixture: {
|
|
1008
|
+
kind: "custom",
|
|
1009
|
+
reason: "Combined renderer stress state.",
|
|
1010
|
+
value: {},
|
|
1011
|
+
},
|
|
1012
|
+
workload: false,
|
|
1013
|
+
},
|
|
1014
|
+
],
|
|
1015
|
+
usesCustomRenderer: false,
|
|
1016
|
+
workloadTargets: [],
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
|
|
1020
|
+
"preview-render custom stressFixture.value must include at least one heavy state key.",
|
|
1021
|
+
);
|
|
1022
|
+
});
|
|
1023
|
+
|
|
573
1024
|
it("rejects short stress fixtures for large text workload scenarios", () => {
|
|
574
1025
|
const config = defineToolcraftPerformance({
|
|
575
1026
|
rendererStrategy: "none",
|
|
@@ -639,10 +1090,10 @@ describe("Toolcraft template performance contract", () => {
|
|
|
639
1090
|
expect(validateToolcraftPerformanceCoverage(largeTextSchema, config)).toEqual([]);
|
|
640
1091
|
});
|
|
641
1092
|
|
|
642
|
-
it("
|
|
1093
|
+
it("requires workload controls in custom renderers to declare an independent workload fixture", () => {
|
|
643
1094
|
const config = defineToolcraftPerformance({
|
|
644
|
-
rendererStrategy: "
|
|
645
|
-
rendererWorkload: "
|
|
1095
|
+
rendererStrategy: "canvas-2d",
|
|
1096
|
+
rendererWorkload: "text-output",
|
|
646
1097
|
scenarios: [
|
|
647
1098
|
{
|
|
648
1099
|
automated: true,
|
|
@@ -651,95 +1102,287 @@ describe("Toolcraft template performance contract", () => {
|
|
|
651
1102
|
browserTestName: "browser perf: density drag stays responsive",
|
|
652
1103
|
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
653
1104
|
controlLabel: "Density",
|
|
654
|
-
expectedObservable: "Dragging Density updates
|
|
1105
|
+
expectedObservable: "Dragging Density updates text output without blocking the UI.",
|
|
655
1106
|
fixture: "runtime density fixture",
|
|
656
1107
|
id: "density-drag",
|
|
657
1108
|
interaction: "control-drag",
|
|
1109
|
+
stressFixture: createMaxValueStressFixture(
|
|
1110
|
+
12,
|
|
1111
|
+
"Density max is the heaviest control value.",
|
|
1112
|
+
),
|
|
658
1113
|
target: "render.density",
|
|
659
1114
|
values: { default: 4, max: 12, min: 1 },
|
|
660
1115
|
workload: true,
|
|
661
1116
|
},
|
|
662
1117
|
],
|
|
663
|
-
usesCustomRenderer:
|
|
1118
|
+
usesCustomRenderer: true,
|
|
664
1119
|
workloadTargets: ["render.density"],
|
|
665
1120
|
});
|
|
666
1121
|
|
|
667
1122
|
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
668
|
-
"
|
|
1123
|
+
"density-drag workload control scenario must declare workloadFixture for the app's heavy baseline state; stressFixture covers the control value only.",
|
|
669
1124
|
);
|
|
670
1125
|
});
|
|
671
1126
|
|
|
672
|
-
it("
|
|
1127
|
+
it("rejects toy media workload fixtures for control-drag baselines", () => {
|
|
673
1128
|
const config = defineToolcraftPerformance({
|
|
674
|
-
rendererStrategy: "
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1129
|
+
rendererStrategy: "webgl",
|
|
1130
|
+
rendererWorkload: "pixel-output",
|
|
1131
|
+
scenarios: [
|
|
1132
|
+
{
|
|
1133
|
+
automated: true,
|
|
1134
|
+
automatedTestName: "perf: density drag stays responsive",
|
|
1135
|
+
browser: true,
|
|
1136
|
+
browserTestName: "browser perf: density drag stays responsive",
|
|
1137
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
1138
|
+
controlLabel: "Density",
|
|
1139
|
+
expectedObservable: "Dragging Density updates source media output without blocking the UI.",
|
|
1140
|
+
fixture: "runtime density fixture",
|
|
1141
|
+
id: "density-drag",
|
|
1142
|
+
interaction: "control-drag",
|
|
1143
|
+
stressFixture: createMaxValueStressFixture(
|
|
1144
|
+
12,
|
|
1145
|
+
"Density max is the heaviest control value.",
|
|
1146
|
+
),
|
|
1147
|
+
target: "render.density",
|
|
1148
|
+
values: { default: 4, max: 12, min: 1 },
|
|
1149
|
+
workloadFixture: createMediaStressFixture(
|
|
1150
|
+
{ height: 480, width: 640 },
|
|
1151
|
+
"This small source should be rejected as too light.",
|
|
1152
|
+
),
|
|
1153
|
+
workload: true,
|
|
1154
|
+
},
|
|
1155
|
+
],
|
|
688
1156
|
usesCustomRenderer: true,
|
|
689
1157
|
workloadTargets: ["render.density"],
|
|
690
1158
|
});
|
|
691
1159
|
|
|
692
|
-
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
it("requires animated custom renderers to test viewport dragging during animation", () => {
|
|
696
|
-
const config = createAnimatedVectorRendererConfig({ includeViewportDrag: false });
|
|
697
|
-
|
|
698
1160
|
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
699
|
-
"
|
|
1161
|
+
"density-drag media workloadFixture.value must be at least 1920x1080-equivalent; received 640x480.",
|
|
700
1162
|
);
|
|
701
1163
|
});
|
|
702
1164
|
|
|
703
|
-
it("
|
|
1165
|
+
it("accepts workload controls with separate control stress and app baseline fixtures", () => {
|
|
704
1166
|
const config = defineToolcraftPerformance({
|
|
705
1167
|
rendererStrategy: "canvas-2d",
|
|
706
1168
|
rendererTechnique: {
|
|
707
1169
|
exportRenderer: "canvas-2d",
|
|
708
|
-
fidelityRisks: ["
|
|
709
|
-
performanceRisks: ["
|
|
1170
|
+
fidelityRisks: ["text output must stay crisp"],
|
|
1171
|
+
performanceRisks: ["density changes can relayout text"],
|
|
710
1172
|
previewRenderer: "canvas-2d",
|
|
711
1173
|
productRepresentation: "text",
|
|
712
1174
|
rendererStrategy: "canvas-2d",
|
|
713
1175
|
rendererWorkload: "text-output",
|
|
714
1176
|
sourceRepresentation: "dom-text",
|
|
715
|
-
whyNotAlternativeStrategies: ["
|
|
1177
|
+
whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
|
|
716
1178
|
},
|
|
717
1179
|
rendererWorkload: "text-output",
|
|
718
|
-
|
|
1180
|
+
rendererPipeline: createTextRendererPipeline(),
|
|
1181
|
+
scenarios: createTextRendererScenarios(),
|
|
719
1182
|
usesCustomRenderer: true,
|
|
720
1183
|
workloadTargets: ["render.density"],
|
|
721
1184
|
});
|
|
722
1185
|
|
|
1186
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
it("rejects workload fixtures that are not paired with scenario stress fixtures", () => {
|
|
1190
|
+
const config = defineToolcraftPerformance({
|
|
1191
|
+
rendererStrategy: "none",
|
|
1192
|
+
rendererWorkload: "none",
|
|
1193
|
+
scenarios: [
|
|
1194
|
+
{
|
|
1195
|
+
automated: true,
|
|
1196
|
+
automatedTestName: "perf: preview baseline stays responsive",
|
|
1197
|
+
browser: true,
|
|
1198
|
+
browserTestName: "browser perf: preview baseline stays responsive",
|
|
1199
|
+
budget: { maxPreviewMs: 1000 },
|
|
1200
|
+
expectedObservable: "Preview renders inside a heavy baseline.",
|
|
1201
|
+
fixture: "baseline fixture",
|
|
1202
|
+
id: "preview-heavy-baseline",
|
|
1203
|
+
interaction: "preview-render",
|
|
1204
|
+
workload: false,
|
|
1205
|
+
workloadFixture: createLargeTextStressFixture(),
|
|
1206
|
+
},
|
|
1207
|
+
],
|
|
1208
|
+
usesCustomRenderer: false,
|
|
1209
|
+
workloadTargets: [],
|
|
1210
|
+
});
|
|
1211
|
+
|
|
723
1212
|
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
724
|
-
"
|
|
1213
|
+
"preview-heavy-baseline workloadFixture must be paired with stressFixture so tests apply a heavy baseline and then the measured scenario value.",
|
|
725
1214
|
);
|
|
726
1215
|
});
|
|
727
1216
|
|
|
728
|
-
it("
|
|
1217
|
+
it("rejects media-import scenarios that use toy source dimensions", () => {
|
|
729
1218
|
const config = defineToolcraftPerformance({
|
|
730
|
-
rendererStrategy: "
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1219
|
+
rendererStrategy: "none",
|
|
1220
|
+
rendererWorkload: "none",
|
|
1221
|
+
scenarios: [
|
|
1222
|
+
{
|
|
1223
|
+
automated: true,
|
|
1224
|
+
automatedTestName: "perf: source image import stays responsive",
|
|
1225
|
+
browser: true,
|
|
1226
|
+
browserTestName: "browser perf: source image import stays responsive",
|
|
1227
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
1228
|
+
expectedObservable: "Importing source media does not block the app.",
|
|
1229
|
+
fixture: "large source image fixture",
|
|
1230
|
+
id: "source-media-import",
|
|
1231
|
+
interaction: "media-import",
|
|
1232
|
+
stressFixture: createMediaStressFixture(
|
|
1233
|
+
{ height: 480, width: 640 },
|
|
1234
|
+
"This small fixture should be rejected as too light.",
|
|
1235
|
+
),
|
|
1236
|
+
target: "source.image",
|
|
1237
|
+
workload: true,
|
|
1238
|
+
},
|
|
1239
|
+
],
|
|
1240
|
+
usesCustomRenderer: false,
|
|
1241
|
+
workloadTargets: [],
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
expect(validateToolcraftPerformanceCoverage(mediaUploadSchema, config)).toContain(
|
|
1245
|
+
"source-media-import media stressFixture.value must be at least 1920x1080-equivalent; received 640x480.",
|
|
1246
|
+
);
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
it("requires media-import scenarios to be workload media fixtures", () => {
|
|
1250
|
+
const config = defineToolcraftPerformance({
|
|
1251
|
+
rendererStrategy: "none",
|
|
1252
|
+
rendererWorkload: "none",
|
|
1253
|
+
scenarios: [
|
|
1254
|
+
{
|
|
1255
|
+
automated: true,
|
|
1256
|
+
automatedTestName: "perf: source image import stays responsive",
|
|
1257
|
+
browser: true,
|
|
1258
|
+
browserTestName: "browser perf: source image import stays responsive",
|
|
1259
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
1260
|
+
expectedObservable: "Importing source media does not block the app.",
|
|
1261
|
+
fixture: "large source image fixture",
|
|
1262
|
+
id: "source-media-import",
|
|
1263
|
+
interaction: "media-import",
|
|
1264
|
+
stressFixture: createCustomStressFixture(
|
|
1265
|
+
{ height: 1080, width: 1920 },
|
|
1266
|
+
"Wrong fixture kind for media import.",
|
|
1267
|
+
),
|
|
1268
|
+
target: "source.image",
|
|
1269
|
+
workload: false,
|
|
1270
|
+
},
|
|
1271
|
+
],
|
|
1272
|
+
usesCustomRenderer: false,
|
|
1273
|
+
workloadTargets: [],
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
expect(validateToolcraftPerformanceCoverage(mediaUploadSchema, config)).toEqual(
|
|
1277
|
+
expect.arrayContaining([
|
|
1278
|
+
"source-media-import media-import scenario must set workload true because decoded source size changes renderer workload.",
|
|
1279
|
+
'source-media-import media-import scenario must use stressFixture.kind "media" with a realistic uploaded source size.',
|
|
1280
|
+
]),
|
|
1281
|
+
);
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
it("rejects visible controls without performance scenarios", () => {
|
|
1285
|
+
const config = defineToolcraftPerformance({
|
|
1286
|
+
rendererStrategy: "none",
|
|
1287
|
+
rendererWorkload: "none",
|
|
1288
|
+
scenarios: [
|
|
1289
|
+
{
|
|
1290
|
+
automated: true,
|
|
1291
|
+
automatedTestName: "perf: density drag stays responsive",
|
|
1292
|
+
browser: true,
|
|
1293
|
+
browserTestName: "browser perf: density drag stays responsive",
|
|
1294
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
|
|
1295
|
+
controlLabel: "Density",
|
|
1296
|
+
expectedObservable: "Dragging Density updates the product without blocking the UI.",
|
|
1297
|
+
fixture: "runtime density fixture",
|
|
1298
|
+
id: "density-drag",
|
|
1299
|
+
interaction: "control-drag",
|
|
1300
|
+
target: "render.density",
|
|
1301
|
+
values: { default: 4, max: 12, min: 1 },
|
|
1302
|
+
workload: true,
|
|
1303
|
+
},
|
|
1304
|
+
],
|
|
1305
|
+
usesCustomRenderer: false,
|
|
1306
|
+
workloadTargets: ["render.density"],
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1310
|
+
"render.mode must have a performance scenario because every visible control can affect app responsiveness.",
|
|
1311
|
+
);
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
it("allows text-output renderers without forcing a GPU strategy", () => {
|
|
1315
|
+
const config = defineToolcraftPerformance({
|
|
1316
|
+
rendererStrategy: "canvas-2d",
|
|
1317
|
+
rendererTechnique: {
|
|
1318
|
+
exportRenderer: "canvas-2d",
|
|
1319
|
+
fidelityRisks: ["glyph metrics must stay crisp at product output size"],
|
|
1320
|
+
performanceRisks: ["large glyph grids can be expensive during drag"],
|
|
1321
|
+
previewRenderer: "canvas-2d",
|
|
1322
|
+
productRepresentation: "text",
|
|
1323
|
+
rendererStrategy: "canvas-2d",
|
|
1324
|
+
rendererWorkload: "text-output",
|
|
1325
|
+
sourceRepresentation: "dom-text",
|
|
1326
|
+
whyNotAlternativeStrategies: ["webgl texture output would rasterize text too early"],
|
|
1327
|
+
},
|
|
1328
|
+
rendererWorkload: "text-output",
|
|
1329
|
+
rendererPipeline: createTextRendererPipeline(),
|
|
1330
|
+
scenarios: createTextRendererScenarios(),
|
|
1331
|
+
usesCustomRenderer: true,
|
|
1332
|
+
workloadTargets: ["render.density"],
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
it("requires animated custom renderers to test viewport dragging during animation", () => {
|
|
1339
|
+
const config = createAnimatedVectorRendererConfig({ includeViewportDrag: false });
|
|
1340
|
+
|
|
1341
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1342
|
+
"Animated custom renderers must include an animation-viewport-drag performance scenario that samples frames while physically moving the canvas viewport.",
|
|
1343
|
+
);
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
it("requires detail-heavy custom renderers to test real viewport zoom stress", () => {
|
|
1347
|
+
const config = defineToolcraftPerformance({
|
|
1348
|
+
rendererStrategy: "canvas-2d",
|
|
1349
|
+
rendererTechnique: {
|
|
1350
|
+
exportRenderer: "canvas-2d",
|
|
1351
|
+
fidelityRisks: ["glyph metrics must stay crisp at product output size"],
|
|
1352
|
+
performanceRisks: ["large glyph grids can shake or jank during canvas zoom"],
|
|
1353
|
+
previewRenderer: "canvas-2d",
|
|
1354
|
+
productRepresentation: "text",
|
|
1355
|
+
rendererStrategy: "canvas-2d",
|
|
1356
|
+
rendererWorkload: "text-output",
|
|
1357
|
+
sourceRepresentation: "dom-text",
|
|
1358
|
+
whyNotAlternativeStrategies: ["webgl texture output would rasterize text too early"],
|
|
1359
|
+
},
|
|
1360
|
+
rendererWorkload: "text-output",
|
|
1361
|
+
scenarios: createTextRendererScenarios({ includeViewportZoomStress: false }),
|
|
1362
|
+
usesCustomRenderer: true,
|
|
1363
|
+
workloadTargets: ["render.density"],
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1367
|
+
"Detail-heavy or animated custom renderers must include a viewport-zoom-stress performance scenario that uses real zoom controls while sampling frame gaps and long tasks.",
|
|
1368
|
+
);
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
it("requires detail-heavy custom renderers to prove worst-case preview or animation stress", () => {
|
|
1372
|
+
const config = defineToolcraftPerformance({
|
|
1373
|
+
rendererStrategy: "canvas-2d",
|
|
1374
|
+
rendererTechnique: {
|
|
1375
|
+
exportRenderer: "canvas-2d",
|
|
1376
|
+
fidelityRisks: ["dense texture output must stay visually stable"],
|
|
1377
|
+
layers: [
|
|
1378
|
+
{
|
|
1379
|
+
content: ["dense-pattern"],
|
|
1380
|
+
exportMode: "included",
|
|
1381
|
+
id: "dense-pattern",
|
|
1382
|
+
kind: "product-foreground",
|
|
1383
|
+
primitiveCount: "high",
|
|
1384
|
+
renderer: "canvas-2d",
|
|
1385
|
+
uiSelector: '[data-toolcraft-renderer-layer="dense-pattern"]',
|
|
743
1386
|
},
|
|
744
1387
|
{
|
|
745
1388
|
content: ["geometry"],
|
|
@@ -812,6 +1455,73 @@ describe("Toolcraft template performance contract", () => {
|
|
|
812
1455
|
);
|
|
813
1456
|
});
|
|
814
1457
|
|
|
1458
|
+
it("requires detail-heavy Canvas 2D renderers to document measured WebGL/WebGPU evaluation", () => {
|
|
1459
|
+
const config = defineToolcraftPerformance({
|
|
1460
|
+
rendererStrategy: "canvas-2d",
|
|
1461
|
+
rendererTechnique: {
|
|
1462
|
+
exportRenderer: "canvas-2d",
|
|
1463
|
+
fidelityRisks: ["dense texture output must match source media colors"],
|
|
1464
|
+
layers: [
|
|
1465
|
+
{
|
|
1466
|
+
content: ["bitmap-media", "noise"],
|
|
1467
|
+
exportMode: "included",
|
|
1468
|
+
id: "media-effect",
|
|
1469
|
+
kind: "product-foreground",
|
|
1470
|
+
primitiveCount: "high",
|
|
1471
|
+
renderer: "canvas-2d",
|
|
1472
|
+
uiSelector: '[data-toolcraft-renderer-layer="media-effect"]',
|
|
1473
|
+
},
|
|
1474
|
+
],
|
|
1475
|
+
performanceRisks: ["large uploaded media can require millions of pixel operations"],
|
|
1476
|
+
previewRenderer: "canvas-2d",
|
|
1477
|
+
productRepresentation: "pixel",
|
|
1478
|
+
rendererStrategy: "canvas-2d",
|
|
1479
|
+
rendererWorkload: "pixel-output",
|
|
1480
|
+
sourceRepresentation: "image-media",
|
|
1481
|
+
whyNotAlternativeStrategies: ["canvas is convenient"],
|
|
1482
|
+
},
|
|
1483
|
+
rendererWorkload: "pixel-output",
|
|
1484
|
+
scenarios: createTextRendererScenarios(),
|
|
1485
|
+
usesCustomRenderer: true,
|
|
1486
|
+
workloadTargets: ["render.density"],
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1490
|
+
"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.",
|
|
1491
|
+
);
|
|
1492
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1493
|
+
'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.',
|
|
1494
|
+
);
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
it("allows pixel-output Canvas 2D only with measured WebGL/WebGPU evidence", () => {
|
|
1498
|
+
const config = defineToolcraftPerformance({
|
|
1499
|
+
rendererStrategy: "canvas-2d",
|
|
1500
|
+
rendererTechnique: {
|
|
1501
|
+
exportRenderer: "canvas-2d",
|
|
1502
|
+
fidelityRisks: ["pixel output must preserve exact color stops"],
|
|
1503
|
+
performanceRisks: [
|
|
1504
|
+
"Measured WebGL stress evidence showed setup overhead exceeded cached Canvas 2D worker output for this static fixture.",
|
|
1505
|
+
],
|
|
1506
|
+
previewRenderer: "canvas-2d",
|
|
1507
|
+
productRepresentation: "pixel",
|
|
1508
|
+
rendererStrategy: "canvas-2d",
|
|
1509
|
+
rendererWorkload: "pixel-output",
|
|
1510
|
+
sourceRepresentation: "procedural-data",
|
|
1511
|
+
whyNotAlternativeStrategies: [
|
|
1512
|
+
"WebGL measured stress evidence stayed within budget but did not improve interaction latency for this renderer.",
|
|
1513
|
+
],
|
|
1514
|
+
},
|
|
1515
|
+
rendererWorkload: "pixel-output",
|
|
1516
|
+
rendererPipeline: createPixelRendererPipeline(),
|
|
1517
|
+
scenarios: createTextRendererScenarios(),
|
|
1518
|
+
usesCustomRenderer: true,
|
|
1519
|
+
workloadTargets: ["render.density"],
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
|
|
1523
|
+
});
|
|
1524
|
+
|
|
815
1525
|
it("accepts animated custom renderers with stress coverage while dragging the viewport", () => {
|
|
816
1526
|
const config = createAnimatedVectorRendererConfig({ includeViewportDrag: true });
|
|
817
1527
|
|
|
@@ -834,6 +1544,528 @@ describe("Toolcraft template performance contract", () => {
|
|
|
834
1544
|
);
|
|
835
1545
|
});
|
|
836
1546
|
|
|
1547
|
+
it("requires custom renderers to declare a render pipeline inventory", () => {
|
|
1548
|
+
const config = defineToolcraftPerformance({
|
|
1549
|
+
rendererStrategy: "canvas-2d",
|
|
1550
|
+
rendererTechnique: {
|
|
1551
|
+
exportRenderer: "canvas-2d",
|
|
1552
|
+
fidelityRisks: ["canvas output can soften edges"],
|
|
1553
|
+
performanceRisks: ["dense output can jank during drag"],
|
|
1554
|
+
previewRenderer: "canvas-2d",
|
|
1555
|
+
productRepresentation: "mixed",
|
|
1556
|
+
rendererStrategy: "canvas-2d",
|
|
1557
|
+
rendererWorkload: "simple-composition",
|
|
1558
|
+
sourceRepresentation: "procedural-data",
|
|
1559
|
+
whyNotAlternativeStrategies: [
|
|
1560
|
+
"svg cannot preserve dense procedural background output",
|
|
1561
|
+
],
|
|
1562
|
+
},
|
|
1563
|
+
rendererWorkload: "simple-composition",
|
|
1564
|
+
scenarios: createTextRendererScenarios(),
|
|
1565
|
+
usesCustomRenderer: true,
|
|
1566
|
+
workloadTargets: ["render.density"],
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1570
|
+
"Custom renderers must declare rendererPipeline so render passes, cache keys, and invalidation are machine-checkable.",
|
|
1571
|
+
);
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
it("rejects vague renderer pipeline pass dependencies", () => {
|
|
1575
|
+
const config = defineToolcraftPerformance({
|
|
1576
|
+
rendererStrategy: "canvas-2d",
|
|
1577
|
+
rendererTechnique: {
|
|
1578
|
+
exportRenderer: "canvas-2d",
|
|
1579
|
+
fidelityRisks: ["dense text output can soften at small sizes"],
|
|
1580
|
+
performanceRisks: ["text layout can be expensive"],
|
|
1581
|
+
previewRenderer: "canvas-2d",
|
|
1582
|
+
productRepresentation: "text",
|
|
1583
|
+
rendererStrategy: "canvas-2d",
|
|
1584
|
+
rendererWorkload: "text-output",
|
|
1585
|
+
sourceRepresentation: "dom-text",
|
|
1586
|
+
whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
|
|
1587
|
+
},
|
|
1588
|
+
rendererWorkload: "text-output",
|
|
1589
|
+
rendererPipeline: {
|
|
1590
|
+
interactionInvalidation: [
|
|
1591
|
+
{
|
|
1592
|
+
interaction: "control-drag",
|
|
1593
|
+
invalidates: ["text-layout"],
|
|
1594
|
+
targets: ["render.density"],
|
|
1595
|
+
},
|
|
1596
|
+
],
|
|
1597
|
+
passes: [
|
|
1598
|
+
{
|
|
1599
|
+
cacheKey: ["state"],
|
|
1600
|
+
id: "text-layout",
|
|
1601
|
+
inputs: ["state"],
|
|
1602
|
+
invalidatedBy: ["all values"],
|
|
1603
|
+
kind: "text-layout",
|
|
1604
|
+
output: "preview",
|
|
1605
|
+
quality: "full",
|
|
1606
|
+
runsOn: "main",
|
|
1607
|
+
},
|
|
1608
|
+
],
|
|
1609
|
+
},
|
|
1610
|
+
scenarios: createTextRendererScenarios(),
|
|
1611
|
+
usesCustomRenderer: true,
|
|
1612
|
+
workloadTargets: ["render.density"],
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual(
|
|
1616
|
+
expect.arrayContaining([
|
|
1617
|
+
'rendererPipeline pass "text-layout" inputs entry "state" is too vague. Name the concrete runtime target, source key, resource key, or cache key part.',
|
|
1618
|
+
'rendererPipeline pass "text-layout" invalidatedBy entry "all values" is too vague. Name the concrete runtime target, source key, resource key, or cache key part.',
|
|
1619
|
+
'rendererPipeline pass "text-layout" cacheKey entry "state" is too vague. Name the concrete runtime target, source key, resource key, or cache key part.',
|
|
1620
|
+
]),
|
|
1621
|
+
);
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
it("requires cache-sensitive renderer pipeline passes to declare cache keys", () => {
|
|
1625
|
+
const config = defineToolcraftPerformance({
|
|
1626
|
+
rendererStrategy: "canvas-2d",
|
|
1627
|
+
rendererTechnique: {
|
|
1628
|
+
exportRenderer: "canvas-2d",
|
|
1629
|
+
fidelityRisks: ["source pixels must stay crisp"],
|
|
1630
|
+
performanceRisks: ["pixel transforms can block the main thread"],
|
|
1631
|
+
previewRenderer: "canvas-2d",
|
|
1632
|
+
productRepresentation: "pixel",
|
|
1633
|
+
rendererStrategy: "canvas-2d",
|
|
1634
|
+
rendererWorkload: "pixel-output",
|
|
1635
|
+
sourceRepresentation: "image-media",
|
|
1636
|
+
whyNotAlternativeStrategies: [
|
|
1637
|
+
"WebGL/WebGPU measured performance evidence showed no improvement for this fixture",
|
|
1638
|
+
],
|
|
1639
|
+
},
|
|
1640
|
+
rendererWorkload: "pixel-output",
|
|
1641
|
+
rendererPipeline: {
|
|
1642
|
+
interactionInvalidation: [
|
|
1643
|
+
{
|
|
1644
|
+
interaction: "control-drag",
|
|
1645
|
+
invalidates: ["pixel-pass"],
|
|
1646
|
+
targets: ["render.density"],
|
|
1647
|
+
},
|
|
1648
|
+
],
|
|
1649
|
+
passes: [
|
|
1650
|
+
{
|
|
1651
|
+
id: "pixel-pass",
|
|
1652
|
+
inputs: ["source.image", "render.density"],
|
|
1653
|
+
invalidatedBy: ["source.image", "render.density"],
|
|
1654
|
+
kind: "pixel-transform",
|
|
1655
|
+
output: "preview",
|
|
1656
|
+
quality: "retina",
|
|
1657
|
+
runsOn: "worker",
|
|
1658
|
+
},
|
|
1659
|
+
],
|
|
1660
|
+
},
|
|
1661
|
+
scenarios: createTextRendererScenarios(),
|
|
1662
|
+
usesCustomRenderer: true,
|
|
1663
|
+
workloadTargets: ["render.density"],
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1667
|
+
'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.',
|
|
1668
|
+
);
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
it("requires composite renderer pipeline passes to declare cache keys", () => {
|
|
1672
|
+
const config = defineToolcraftPerformance({
|
|
1673
|
+
rendererStrategy: "svg",
|
|
1674
|
+
rendererTechnique: {
|
|
1675
|
+
exportRenderer: "svg",
|
|
1676
|
+
fidelityRisks: ["animated composite must preserve vector output"],
|
|
1677
|
+
performanceRisks: ["compositing can be rebuilt too often"],
|
|
1678
|
+
previewRenderer: "svg",
|
|
1679
|
+
productRepresentation: "vector",
|
|
1680
|
+
rendererStrategy: "svg",
|
|
1681
|
+
rendererWorkload: "vector-output",
|
|
1682
|
+
sourceRepresentation: "procedural-data",
|
|
1683
|
+
whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
|
|
1684
|
+
},
|
|
1685
|
+
rendererWorkload: "vector-output",
|
|
1686
|
+
rendererPipeline: {
|
|
1687
|
+
...createAnimatedVectorRendererPipeline(),
|
|
1688
|
+
passes: createAnimatedVectorRendererPipeline().passes.map((pass) =>
|
|
1689
|
+
pass.id === "animation-composite"
|
|
1690
|
+
? {
|
|
1691
|
+
id: pass.id,
|
|
1692
|
+
inputs: pass.inputs,
|
|
1693
|
+
invalidatedBy: pass.invalidatedBy,
|
|
1694
|
+
kind: pass.kind,
|
|
1695
|
+
output: pass.output,
|
|
1696
|
+
quality: pass.quality,
|
|
1697
|
+
runsOn: pass.runsOn,
|
|
1698
|
+
}
|
|
1699
|
+
: pass,
|
|
1700
|
+
),
|
|
1701
|
+
},
|
|
1702
|
+
scenarios: createTextRendererScenarios(),
|
|
1703
|
+
usesCustomRenderer: true,
|
|
1704
|
+
workloadTargets: ["render.density"],
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1708
|
+
'rendererPipeline pass "animation-composite" is a cache-sensitive composite pass and must declare cacheKey so tests can reject full recomputation on every control change.',
|
|
1709
|
+
);
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
it("rejects high-frequency viewport interactions that invalidate expensive passes", () => {
|
|
1713
|
+
const config = defineToolcraftPerformance({
|
|
1714
|
+
rendererStrategy: "canvas-2d",
|
|
1715
|
+
rendererTechnique: {
|
|
1716
|
+
exportRenderer: "canvas-2d",
|
|
1717
|
+
fidelityRisks: ["dense background must stay stable while zooming"],
|
|
1718
|
+
layers: [
|
|
1719
|
+
{
|
|
1720
|
+
content: ["dense-pattern"],
|
|
1721
|
+
exportMode: "included",
|
|
1722
|
+
id: "dense-pattern",
|
|
1723
|
+
kind: "background",
|
|
1724
|
+
primitiveCount: "high",
|
|
1725
|
+
renderer: "canvas-2d",
|
|
1726
|
+
},
|
|
1727
|
+
],
|
|
1728
|
+
performanceRisks: [
|
|
1729
|
+
"WebGL measured stress evidence was reviewed before keeping dense background on Canvas 2D",
|
|
1730
|
+
],
|
|
1731
|
+
previewRenderer: "canvas-2d",
|
|
1732
|
+
productRepresentation: "pixel",
|
|
1733
|
+
rendererStrategy: "canvas-2d",
|
|
1734
|
+
rendererWorkload: "pixel-output",
|
|
1735
|
+
sourceRepresentation: "procedural-data",
|
|
1736
|
+
whyNotAlternativeStrategies: [
|
|
1737
|
+
"WebGL stress evidence showed shader setup overhead exceeded static cache reuse",
|
|
1738
|
+
],
|
|
1739
|
+
},
|
|
1740
|
+
rendererWorkload: "pixel-output",
|
|
1741
|
+
rendererPipeline: {
|
|
1742
|
+
interactionInvalidation: [
|
|
1743
|
+
{
|
|
1744
|
+
interaction: "viewport-zoom",
|
|
1745
|
+
invalidates: ["dense-background"],
|
|
1746
|
+
mustNotInvalidate: ["dense-background"],
|
|
1747
|
+
targets: ["canvas.viewport"],
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
interaction: "control-drag",
|
|
1751
|
+
invalidates: ["dense-background"],
|
|
1752
|
+
targets: ["render.density"],
|
|
1753
|
+
},
|
|
1754
|
+
],
|
|
1755
|
+
passes: [
|
|
1756
|
+
{
|
|
1757
|
+
cacheKey: ["render.density", "canvas.size", "canvas.renderScale"],
|
|
1758
|
+
id: "dense-background",
|
|
1759
|
+
inputs: ["render.density", "canvas.size", "canvas.renderScale"],
|
|
1760
|
+
invalidatedBy: ["render.density", "canvas.size", "canvas.renderScale"],
|
|
1761
|
+
kind: "rasterize",
|
|
1762
|
+
output: "preview",
|
|
1763
|
+
quality: "retina",
|
|
1764
|
+
runsOn: "worker",
|
|
1765
|
+
},
|
|
1766
|
+
],
|
|
1767
|
+
},
|
|
1768
|
+
scenarios: createTextRendererScenarios(),
|
|
1769
|
+
usesCustomRenderer: true,
|
|
1770
|
+
workloadTargets: ["render.density"],
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual(
|
|
1774
|
+
expect.arrayContaining([
|
|
1775
|
+
'rendererPipeline viewport-zoom cannot both invalidate and mustNotInvalidate pass "dense-background".',
|
|
1776
|
+
'rendererPipeline viewport-zoom must not invalidate expensive pass "dense-background" (rasterize). Move viewport work to transforms/uniforms or explain it through a cheaper pass.',
|
|
1777
|
+
]),
|
|
1778
|
+
);
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
it("requires media decoding pipelines to include media-import coverage", () => {
|
|
1782
|
+
const config = defineToolcraftPerformance({
|
|
1783
|
+
rendererStrategy: "webgl",
|
|
1784
|
+
rendererTechnique: {
|
|
1785
|
+
exportRenderer: "webgl",
|
|
1786
|
+
fidelityRisks: ["large source media must preserve color"],
|
|
1787
|
+
performanceRisks: ["source decode can be expensive"],
|
|
1788
|
+
previewRenderer: "webgl",
|
|
1789
|
+
productRepresentation: "pixel",
|
|
1790
|
+
rendererStrategy: "webgl",
|
|
1791
|
+
rendererWorkload: "pixel-output",
|
|
1792
|
+
sourceRepresentation: "image-media",
|
|
1793
|
+
whyNotAlternativeStrategies: ["canvas-2d would run per-pixel work on the main thread"],
|
|
1794
|
+
},
|
|
1795
|
+
rendererWorkload: "pixel-output",
|
|
1796
|
+
rendererPipeline: createMediaRendererPipeline(),
|
|
1797
|
+
scenarios: createTextRendererScenarios(),
|
|
1798
|
+
usesCustomRenderer: true,
|
|
1799
|
+
workloadTargets: ["render.density"],
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1803
|
+
'rendererPipeline pass "source-decode" decodes media, so performance scenarios must include media-import coverage.',
|
|
1804
|
+
);
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
it("requires workload targets to appear in renderer pipeline invalidation targets", () => {
|
|
1808
|
+
const config = defineToolcraftPerformance({
|
|
1809
|
+
rendererStrategy: "canvas-2d",
|
|
1810
|
+
rendererTechnique: {
|
|
1811
|
+
exportRenderer: "canvas-2d",
|
|
1812
|
+
fidelityRisks: ["text output must stay crisp"],
|
|
1813
|
+
performanceRisks: ["density changes can relayout text"],
|
|
1814
|
+
previewRenderer: "canvas-2d",
|
|
1815
|
+
productRepresentation: "text",
|
|
1816
|
+
rendererStrategy: "canvas-2d",
|
|
1817
|
+
rendererWorkload: "text-output",
|
|
1818
|
+
sourceRepresentation: "dom-text",
|
|
1819
|
+
whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
|
|
1820
|
+
},
|
|
1821
|
+
rendererWorkload: "text-output",
|
|
1822
|
+
rendererPipeline: {
|
|
1823
|
+
...createTextRendererPipeline(),
|
|
1824
|
+
interactionInvalidation: [
|
|
1825
|
+
{
|
|
1826
|
+
interaction: "control-change",
|
|
1827
|
+
invalidates: ["text-layout"],
|
|
1828
|
+
targets: ["render.mode"],
|
|
1829
|
+
},
|
|
1830
|
+
],
|
|
1831
|
+
},
|
|
1832
|
+
scenarios: createTextRendererScenarios(),
|
|
1833
|
+
usesCustomRenderer: true,
|
|
1834
|
+
workloadTargets: ["render.density"],
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1838
|
+
"Performance workload target render.density must appear in rendererPipeline interactionInvalidation targets.",
|
|
1839
|
+
);
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
it("requires performance scenario interactions to appear in renderer pipeline invalidation", () => {
|
|
1843
|
+
const config = defineToolcraftPerformance({
|
|
1844
|
+
rendererStrategy: "canvas-2d",
|
|
1845
|
+
rendererTechnique: {
|
|
1846
|
+
exportRenderer: "canvas-2d",
|
|
1847
|
+
fidelityRisks: ["text output must stay crisp"],
|
|
1848
|
+
performanceRisks: ["density changes can relayout text"],
|
|
1849
|
+
previewRenderer: "canvas-2d",
|
|
1850
|
+
productRepresentation: "text",
|
|
1851
|
+
rendererStrategy: "canvas-2d",
|
|
1852
|
+
rendererWorkload: "text-output",
|
|
1853
|
+
sourceRepresentation: "dom-text",
|
|
1854
|
+
whyNotAlternativeStrategies: ["dom output cannot preserve export composition"],
|
|
1855
|
+
},
|
|
1856
|
+
rendererWorkload: "text-output",
|
|
1857
|
+
rendererPipeline: {
|
|
1858
|
+
...createTextRendererPipeline(),
|
|
1859
|
+
interactionInvalidation: [
|
|
1860
|
+
{
|
|
1861
|
+
interaction: "control-drag",
|
|
1862
|
+
invalidates: ["text-layout"],
|
|
1863
|
+
targets: ["render.density"],
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
interaction: "control-change",
|
|
1867
|
+
invalidates: ["text-layout"],
|
|
1868
|
+
targets: ["render.mode"],
|
|
1869
|
+
},
|
|
1870
|
+
],
|
|
1871
|
+
},
|
|
1872
|
+
scenarios: createTextRendererScenarios(),
|
|
1873
|
+
usesCustomRenderer: true,
|
|
1874
|
+
workloadTargets: ["render.density"],
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1877
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1878
|
+
"Performance scenario viewport-zoom-stress exercises viewport-zoom, so rendererPipeline.interactionInvalidation must declare that interaction.",
|
|
1879
|
+
);
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
it("requires timeline playback scenarios to appear in renderer pipeline invalidation", () => {
|
|
1883
|
+
const config = defineToolcraftPerformance({
|
|
1884
|
+
rendererStrategy: "svg",
|
|
1885
|
+
rendererTechnique: {
|
|
1886
|
+
exportRenderer: "svg",
|
|
1887
|
+
fidelityRisks: ["timeline playback must preserve vector motion"],
|
|
1888
|
+
performanceRisks: ["timeline playback can jank at high density"],
|
|
1889
|
+
previewRenderer: "svg",
|
|
1890
|
+
productRepresentation: "vector",
|
|
1891
|
+
rendererStrategy: "svg",
|
|
1892
|
+
rendererWorkload: "vector-output",
|
|
1893
|
+
sourceRepresentation: "procedural-data",
|
|
1894
|
+
whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
|
|
1895
|
+
},
|
|
1896
|
+
rendererWorkload: "vector-output",
|
|
1897
|
+
rendererPipeline: {
|
|
1898
|
+
...createAnimatedVectorRendererPipeline(),
|
|
1899
|
+
interactionInvalidation: createAnimatedVectorRendererPipeline().interactionInvalidation.filter(
|
|
1900
|
+
(entry) => entry.interaction !== "timeline-playback",
|
|
1901
|
+
),
|
|
1902
|
+
},
|
|
1903
|
+
scenarios: [
|
|
1904
|
+
...createTextRendererScenarios(),
|
|
1905
|
+
{
|
|
1906
|
+
automated: true,
|
|
1907
|
+
automatedTestName: "perf: timeline playback stays smooth",
|
|
1908
|
+
browser: true,
|
|
1909
|
+
browserTestName: "browser perf: timeline playback stays smooth",
|
|
1910
|
+
budget: { maxFrameGapMs: 80, maxLongTaskMs: 80 },
|
|
1911
|
+
expectedObservable: "Timeline playback advances without blocking frames.",
|
|
1912
|
+
fixture: "timeline playback fixture",
|
|
1913
|
+
id: "timeline-playback",
|
|
1914
|
+
interaction: "timeline-playback",
|
|
1915
|
+
stress: true,
|
|
1916
|
+
stressFixture: createCombinedRendererStressFixture(),
|
|
1917
|
+
target: "timeline.currentTime",
|
|
1918
|
+
uiSelector: '[data-slot="timeline-playback-handle"]',
|
|
1919
|
+
workload: false,
|
|
1920
|
+
},
|
|
1921
|
+
],
|
|
1922
|
+
usesCustomRenderer: true,
|
|
1923
|
+
workloadTargets: ["render.density"],
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
1927
|
+
"Performance scenario timeline-playback exercises timeline-playback, so rendererPipeline.interactionInvalidation must declare that interaction.",
|
|
1928
|
+
);
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
it("validates timeline scrub scenarios through renderer pipeline invalidation", () => {
|
|
1932
|
+
const config = defineToolcraftPerformance({
|
|
1933
|
+
rendererStrategy: "svg",
|
|
1934
|
+
rendererTechnique: {
|
|
1935
|
+
exportRenderer: "svg",
|
|
1936
|
+
fidelityRisks: ["timeline scrub must preserve vector motion"],
|
|
1937
|
+
performanceRisks: ["timeline scrub can jank at high density"],
|
|
1938
|
+
previewRenderer: "svg",
|
|
1939
|
+
productRepresentation: "vector",
|
|
1940
|
+
rendererStrategy: "svg",
|
|
1941
|
+
rendererWorkload: "vector-output",
|
|
1942
|
+
sourceRepresentation: "procedural-data",
|
|
1943
|
+
whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
|
|
1944
|
+
},
|
|
1945
|
+
rendererWorkload: "vector-output",
|
|
1946
|
+
rendererPipeline: createAnimatedVectorRendererPipeline(),
|
|
1947
|
+
scenarios: [
|
|
1948
|
+
...createTextRendererScenarios(),
|
|
1949
|
+
{
|
|
1950
|
+
automated: true,
|
|
1951
|
+
automatedTestName: "perf: timeline scrub stays responsive",
|
|
1952
|
+
browser: true,
|
|
1953
|
+
browserTestName: "browser perf: timeline scrub stays responsive",
|
|
1954
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500, maxLongTaskMs: 80 },
|
|
1955
|
+
expectedObservable: "Timeline scrub updates output without blocking frames.",
|
|
1956
|
+
fixture: "timeline scrub fixture",
|
|
1957
|
+
id: "timeline-scrub",
|
|
1958
|
+
interaction: "timeline-scrub",
|
|
1959
|
+
stress: true,
|
|
1960
|
+
stressFixture: createCombinedRendererStressFixture(),
|
|
1961
|
+
target: "timeline.currentTime",
|
|
1962
|
+
uiSelector: '[data-slot="timeline-playback-handle"]',
|
|
1963
|
+
workload: false,
|
|
1964
|
+
},
|
|
1965
|
+
],
|
|
1966
|
+
usesCustomRenderer: true,
|
|
1967
|
+
workloadTargets: ["render.density"],
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1973
|
+
it("requires timeline performance scenarios to name the real timeline UI", () => {
|
|
1974
|
+
const config = defineToolcraftPerformance({
|
|
1975
|
+
rendererStrategy: "svg",
|
|
1976
|
+
rendererTechnique: {
|
|
1977
|
+
exportRenderer: "svg",
|
|
1978
|
+
fidelityRisks: ["timeline scrub must preserve vector motion"],
|
|
1979
|
+
performanceRisks: ["timeline scrub can jank at high density"],
|
|
1980
|
+
previewRenderer: "svg",
|
|
1981
|
+
productRepresentation: "vector",
|
|
1982
|
+
rendererStrategy: "svg",
|
|
1983
|
+
rendererWorkload: "vector-output",
|
|
1984
|
+
sourceRepresentation: "procedural-data",
|
|
1985
|
+
whyNotAlternativeStrategies: ["svg preserves semantic vector output"],
|
|
1986
|
+
},
|
|
1987
|
+
rendererWorkload: "vector-output",
|
|
1988
|
+
rendererPipeline: createAnimatedVectorRendererPipeline(),
|
|
1989
|
+
scenarios: [
|
|
1990
|
+
...createTextRendererScenarios(),
|
|
1991
|
+
{
|
|
1992
|
+
automated: true,
|
|
1993
|
+
automatedTestName: "perf: timeline scrub stays responsive",
|
|
1994
|
+
browser: true,
|
|
1995
|
+
browserTestName: "browser perf: timeline scrub stays responsive",
|
|
1996
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500, maxLongTaskMs: 80 },
|
|
1997
|
+
expectedObservable: "Timeline scrub updates output without blocking frames.",
|
|
1998
|
+
fixture: "timeline scrub fixture",
|
|
1999
|
+
id: "timeline-scrub",
|
|
2000
|
+
interaction: "timeline-scrub",
|
|
2001
|
+
stress: true,
|
|
2002
|
+
stressFixture: createCombinedRendererStressFixture(),
|
|
2003
|
+
target: "timeline.currentTime",
|
|
2004
|
+
workload: false,
|
|
2005
|
+
},
|
|
2006
|
+
],
|
|
2007
|
+
usesCustomRenderer: true,
|
|
2008
|
+
workloadTargets: ["render.density"],
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toContain(
|
|
2012
|
+
"timeline-scrub timeline-scrub scenario must declare controlLabel or uiSelector for its real browser interaction.",
|
|
2013
|
+
);
|
|
2014
|
+
});
|
|
2015
|
+
|
|
2016
|
+
it("validates mask drag scenarios through renderer pipeline invalidation", () => {
|
|
2017
|
+
const config = defineToolcraftPerformance({
|
|
2018
|
+
rendererStrategy: "svg",
|
|
2019
|
+
rendererTechnique: {
|
|
2020
|
+
exportRenderer: "svg",
|
|
2021
|
+
fidelityRisks: ["mask handles must stay aligned with vector output"],
|
|
2022
|
+
performanceRisks: ["dragging a mask handle can rebuild output too often"],
|
|
2023
|
+
previewRenderer: "svg",
|
|
2024
|
+
productRepresentation: "vector",
|
|
2025
|
+
rendererStrategy: "svg",
|
|
2026
|
+
rendererWorkload: "vector-output",
|
|
2027
|
+
sourceRepresentation: "procedural-data",
|
|
2028
|
+
whyNotAlternativeStrategies: ["svg preserves semantic handle geometry"],
|
|
2029
|
+
},
|
|
2030
|
+
rendererWorkload: "vector-output",
|
|
2031
|
+
rendererPipeline: {
|
|
2032
|
+
...createAnimatedVectorRendererPipeline(),
|
|
2033
|
+
interactionInvalidation: [
|
|
2034
|
+
...createAnimatedVectorRendererPipeline().interactionInvalidation,
|
|
2035
|
+
{
|
|
2036
|
+
interaction: "mask-drag",
|
|
2037
|
+
invalidates: ["animation-composite"],
|
|
2038
|
+
mustNotInvalidate: ["vector-build"],
|
|
2039
|
+
targets: ["mask.anchor"],
|
|
2040
|
+
},
|
|
2041
|
+
],
|
|
2042
|
+
},
|
|
2043
|
+
scenarios: [
|
|
2044
|
+
...createTextRendererScenarios(),
|
|
2045
|
+
{
|
|
2046
|
+
automated: true,
|
|
2047
|
+
automatedTestName: "perf: mask drag stays responsive",
|
|
2048
|
+
browser: true,
|
|
2049
|
+
browserTestName: "browser perf: mask drag stays responsive",
|
|
2050
|
+
budget: { maxFrameGapMs: 80, maxInteractionMs: 500, maxLongTaskMs: 80 },
|
|
2051
|
+
expectedObservable: "Dragging the mask handle updates output without blocking frames.",
|
|
2052
|
+
fixture: "mask drag fixture",
|
|
2053
|
+
id: "mask-drag",
|
|
2054
|
+
interaction: "mask-drag",
|
|
2055
|
+
stress: true,
|
|
2056
|
+
stressFixture: createCombinedRendererStressFixture(),
|
|
2057
|
+
target: "mask.anchor",
|
|
2058
|
+
uiSelector: '[data-toolcraft-canvas-handle][data-testid="mask-anchor"]',
|
|
2059
|
+
workload: false,
|
|
2060
|
+
},
|
|
2061
|
+
],
|
|
2062
|
+
usesCustomRenderer: true,
|
|
2063
|
+
workloadTargets: ["render.density"],
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual([]);
|
|
2067
|
+
});
|
|
2068
|
+
|
|
837
2069
|
it("rejects renderer technique mismatches and missing explanations", () => {
|
|
838
2070
|
const config = defineToolcraftPerformance({
|
|
839
2071
|
rendererStrategy: "canvas-2d",
|
|
@@ -909,9 +2141,11 @@ describe("Toolcraft template performance contract", () => {
|
|
|
909
2141
|
sourceRepresentation: "procedural-data",
|
|
910
2142
|
whyNotAlternativeStrategies: [
|
|
911
2143
|
"rendering thousands of background dots as DOM nodes would be expensive",
|
|
2144
|
+
"WebGL/WebGPU stress evidence is not needed yet because measured canvas-2d preview budgets pass for this dense static background.",
|
|
912
2145
|
],
|
|
913
2146
|
},
|
|
914
2147
|
rendererWorkload: "simple-composition",
|
|
2148
|
+
rendererPipeline: createDenseCanvasPipeline(),
|
|
915
2149
|
scenarios: createTextRendererScenarios(),
|
|
916
2150
|
usesCustomRenderer: true,
|
|
917
2151
|
workloadTargets: ["render.density"],
|