@pixel-point/toolcraft 0.0.16 → 0.0.17
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-performance-iteration-execution.test.mjs +2 -2
- package/src/generate-test-demand-only-lifecycle.mjs +14 -3
- package/src/generate-test-dependency-sandbox.mjs +42 -5
- package/src/generate-test-doc-assertions.mjs +68 -18
- package/src/generate-test-doc-assertions.test.mjs +22 -0
- package/src/generate-test-file-assertions.mjs +43 -76
- package/src/generate-test-output-assertions.mjs +95 -0
- package/src/generate-test-package-assertions.mjs +6 -0
- package/src/generate-test-performance-boundary-assertions.mjs +45 -0
- package/src/generate-test-product-fixture.mjs +1 -0
- package/src/generate-test-product-fixture.test.mjs +18 -0
- package/src/generate-test-product-iteration.mjs +1 -0
- package/src/generate-test-product-iteration.test.mjs +1 -0
- package/src/generate-test-standalone-delivery-stages.mjs +14 -6
- package/src/generate.test.mjs +14 -0
- package/templates/runtime/contracts/component-contracts.choices.test.ts +12 -9
- package/templates/runtime/contracts/component-contracts.choices.ts +10 -9
- package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +17 -2
- package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +1 -1
- package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +1 -1
- package/templates/runtime/contracts/component-contracts.runtime.ts +3 -2
- package/templates/runtime/contracts/decision-contracts.test.ts +25 -4
- package/templates/runtime/contracts/decision-contracts.ts +3 -3
- package/templates/runtime/contracts/performance-verification-policy.test.ts +59 -6
- package/templates/runtime/contracts/performance-verification-policy.ts +5 -2
- package/templates/runtime/export/artifact-download.test.ts +71 -0
- package/templates/runtime/export/artifact-download.ts +80 -0
- package/templates/runtime/export/artifact-export-request.ts +23 -0
- package/templates/runtime/export/artifact-export-settings.test.ts +75 -0
- package/templates/runtime/export/artifact-export-settings.ts +96 -0
- package/templates/runtime/export/artifact-frame-renderer.test.ts +107 -0
- package/templates/runtime/export/artifact-frame-renderer.ts +96 -0
- package/templates/runtime/export/artifact-frame-state.test.ts +54 -0
- package/templates/runtime/export/artifact-frame-state.ts +44 -0
- package/templates/runtime/export/artifact-scene-frame.test.ts +84 -0
- package/templates/runtime/export/artifact-scene-frame.ts +118 -0
- package/templates/runtime/export/export-background.ts +65 -0
- package/templates/runtime/export/export-error.ts +43 -0
- package/templates/runtime/export/export.test.ts +6 -186
- package/templates/runtime/export/image-artifact-export.test.ts +111 -0
- package/templates/runtime/export/image-artifact-export.ts +145 -0
- package/templates/runtime/export/index.ts +14 -1
- package/templates/runtime/export/product-export-renderer.test.ts +77 -0
- package/templates/runtime/export/product-export-renderer.ts +63 -0
- package/templates/runtime/export/video-artifact-export.test.ts +166 -0
- package/templates/runtime/export/video-artifact-export.ts +164 -0
- package/templates/runtime/export/video-encoding-backend.ts +124 -0
- package/templates/runtime/export/video-encoding-policy.test.ts +68 -0
- package/templates/runtime/export/video-encoding-policy.ts +86 -0
- package/templates/runtime/export/video-frame-schedule.test.ts +58 -0
- package/templates/runtime/export/video-frame-schedule.ts +42 -0
- package/templates/runtime/index.ts +1 -0
- package/templates/runtime/react/app-shell/runtime-public-api-runtime.test.tsx +291 -0
- package/templates/runtime/react/app-shell/runtime-public-api-test-support.ts +21 -0
- package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +4 -303
- package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +12 -27
- package/templates/runtime/react/app-shell/toolcraft-app.tsx +22 -4
- package/templates/runtime/react/app-shell/toolcraft-source-asset-context.test.tsx +16 -57
- package/templates/runtime/react/app-shell/toolcraft-source-asset-context.tsx +75 -434
- package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.test.ts +194 -0
- package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.ts +227 -0
- package/templates/runtime/react/app-shell/toolcraft-source-asset-provider-ownership.test.tsx +243 -0
- package/templates/runtime/react/app-shell/toolcraft-source-asset-test-support.ts +51 -0
- package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.test.ts +138 -0
- package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.ts +378 -0
- package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +7 -12
- package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +17 -83
- package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +42 -84
- package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +62 -0
- package/templates/runtime/react/panel-host/panel-host-runtime-placement.test.tsx +30 -0
- package/templates/runtime/react/panel-host/panel-host.tsx +4 -0
- package/templates/runtime/scene/scene-bounds.ts +1 -2
- package/templates/runtime/schema/canvas-render-scale.ts +62 -0
- package/templates/runtime/schema/controls-panel-section-id.test.ts +1 -1
- package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +3 -3
- package/templates/runtime/schema/define-toolcraft.setup-background.test.ts +1 -1
- package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +71 -3
- package/templates/runtime/schema/define-toolcraft.ts +1 -1
- package/templates/runtime/schema/schema-resolvers.ts +0 -56
- package/templates/runtime/schema/types.ts +4 -4
- package/templates/runtime/state/canvas-background-state.ts +6 -0
- package/templates/runtime/testing/performance-render-plan-integration.test.ts +64 -0
- package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +18 -1
- package/templates/starter/AGENTS.md +8 -7
- package/templates/starter/NOTICE.md +6 -0
- package/templates/starter/docs/toolcraft/README.md +2 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +8 -6
- package/templates/starter/docs/toolcraft/agent-worklog.md +3 -3
- package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -3
- package/templates/starter/docs/toolcraft/core/media-upload.md +2 -2
- package/templates/starter/docs/toolcraft/core/performance.md +14 -3
- package/templates/starter/docs/toolcraft/core/runtime-boundary.md +3 -1
- package/templates/starter/docs/toolcraft/core/setup-export.md +16 -12
- package/templates/starter/docs/toolcraft/core/timeline-animation.md +4 -4
- package/templates/starter/docs/toolcraft/performance.md +20 -6
- package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +2 -6
- package/templates/starter/docs/toolcraft/workflow.md +7 -2
- package/templates/starter/e2e/app-browser-control-coverage.spec.ts +33 -0
- package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +43 -1
- package/templates/starter/e2e/app-browser-render-scale-evidence.spec.ts +60 -6
- package/templates/starter/e2e/app-browser-render-scale-requirements.spec.ts +77 -1
- package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +8 -0
- package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +35 -0
- package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +4 -1
- package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +0 -1
- package/templates/starter/e2e/app-browser-shell.spec.ts +13 -5
- package/templates/starter/e2e/app-performance-path-adapters.ts +8 -1
- package/templates/starter/e2e/app-performance.spec.ts +16 -9
- package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +9 -330
- package/templates/starter/e2e/browser-infinity-canvas-unavailable-image-evidence.ts +370 -0
- package/templates/starter/e2e/browser-media-export-evidence.spec.ts +77 -0
- package/templates/starter/e2e/browser-media-export-evidence.ts +208 -0
- package/templates/starter/e2e/browser-render-scale-evidence.ts +37 -29
- package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +4 -1
- package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +43 -9
- package/templates/starter/e2e/browser-standard-timeline-evidence.ts +106 -0
- package/templates/starter/e2e/decoded-pixel-observation.spec.ts +74 -0
- package/templates/starter/e2e/decoded-pixel-observation.ts +227 -0
- package/templates/starter/e2e/export-artifact-helpers.ts +206 -25
- package/templates/starter/e2e/image-artifact-inspection.spec.ts +41 -0
- package/templates/starter/e2e/image-artifact-inspection.ts +99 -0
- package/templates/starter/e2e/performance-canvas-helpers.ts +22 -21
- package/templates/starter/e2e/performance-canvas-quality-guard.spec.ts +312 -0
- package/templates/starter/e2e/performance-canvas-quality-guard.ts +196 -0
- package/templates/starter/e2e/performance-canvas-quality-observer.ts +351 -0
- package/templates/starter/e2e/performance-control-layout-helpers.ts +2 -2
- package/templates/starter/e2e/performance-path-adapter-contract.ts +4 -4
- package/templates/starter/e2e/performance-path-helpers.spec.ts +203 -70
- package/templates/starter/e2e/performance-path-helpers.ts +210 -91
- package/templates/starter/e2e/performance-pipeline-evidence-continuity.spec.ts +76 -0
- package/templates/starter/e2e/performance-pipeline-evidence-test-fixtures.ts +11 -2
- package/templates/starter/e2e/performance-pipeline-invariants.ts +47 -4
- package/templates/starter/e2e/performance-render-scale-runner.spec.ts +140 -38
- package/templates/starter/e2e/video-artifact-inspection.spec.ts +64 -0
- package/templates/starter/e2e/video-artifact-inspection.ts +281 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/package.json +1 -0
- package/templates/starter/scripts/check-toolcraft-docs.mjs +17 -0
- package/templates/starter/scripts/toolcraft-delivery-plan-authority.test.mjs +60 -0
- package/templates/starter/scripts/toolcraft-delivery-plan-errors.test.mjs +7 -2
- package/templates/starter/scripts/toolcraft-delivery-plan.mjs +7 -3
- package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +24 -2
- package/templates/starter/scripts/toolcraft-product-boundary.mjs +12 -0
- package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +46 -0
- package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +85 -20
- package/templates/starter/scripts/toolcraft-product-evidence-import-policy.mjs +43 -0
- package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +168 -0
- package/templates/starter/scripts/toolcraft-typescript-analysis.d.mts +73 -0
- package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade-test-helpers.mjs +318 -0
- package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +155 -206
- package/templates/starter/scripts/toolcraft-verification-inventory.mjs +1 -0
- package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +51 -0
- package/templates/starter/scripts/toolcraft-vite-env-activation.test.mjs +128 -59
- package/templates/starter/src/app/acceptance/export-artifact-coverage.ts +113 -0
- package/templates/starter/src/app/acceptance/output-background-rules.ts +1 -1
- package/templates/starter/src/app/acceptance/output-export.ts +13 -2
- package/templates/starter/src/app/acceptance/types.ts +7 -0
- package/templates/starter/src/app/acceptance/validate-coverage.ts +2 -1
- package/templates/starter/src/app/starter-acceptance.export-artifact.test.ts +131 -0
- package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +14 -4
- package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +42 -1
- package/templates/starter/src/app/starter-acceptance.ts +2 -0
- package/templates/starter/src/app/starter-performance-test-utils.ts +103 -0
- package/templates/starter/src/app/starter-performance.gates.test.ts +231 -278
- package/templates/starter/src/app/starter-performance.lifecycle.test.ts +261 -0
- package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +2 -0
- package/templates/starter/src/routes/index.tsx +1 -0
- package/templates/runtime/export/export.ts +0 -166
|
@@ -1,12 +1,52 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type ToolcraftNormalizedPixelBounds = Readonly<{
|
|
2
|
+
height: number;
|
|
3
|
+
width: number;
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
}>;
|
|
7
|
+
|
|
8
|
+
export type ToolcraftBinaryArtifactInspection = Readonly<{
|
|
2
9
|
byteLength: number;
|
|
3
10
|
contentHash?: string;
|
|
4
11
|
durationMs?: number;
|
|
5
12
|
frameCount?: number;
|
|
6
13
|
height?: number;
|
|
14
|
+
kind?: "binary";
|
|
7
15
|
mediaType?: string;
|
|
8
16
|
width?: number;
|
|
9
|
-
}
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export type ToolcraftImageArtifactInspection = Readonly<{
|
|
20
|
+
byteLength: number;
|
|
21
|
+
decodedPixelHash: string;
|
|
22
|
+
height: number;
|
|
23
|
+
kind: "image";
|
|
24
|
+
mediaType: "image/jpeg" | "image/png";
|
|
25
|
+
nonBackgroundBounds: ToolcraftNormalizedPixelBounds | null;
|
|
26
|
+
width: number;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
export type ToolcraftVideoPacketTiming = Readonly<{
|
|
30
|
+
durationSeconds: number;
|
|
31
|
+
timeSeconds: number;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
export type ToolcraftVideoArtifactInspection = Readonly<{
|
|
35
|
+
byteLength: number;
|
|
36
|
+
durationMs: number;
|
|
37
|
+
frameCount: number;
|
|
38
|
+
height: number;
|
|
39
|
+
kind: "video";
|
|
40
|
+
mediaType: "video/mp4" | "video/webm";
|
|
41
|
+
packetTimings: readonly ToolcraftVideoPacketTiming[];
|
|
42
|
+
samplePixelHashes: readonly string[];
|
|
43
|
+
width: number;
|
|
44
|
+
}>;
|
|
45
|
+
|
|
46
|
+
export type ToolcraftExportArtifactInspection =
|
|
47
|
+
| ToolcraftBinaryArtifactInspection
|
|
48
|
+
| ToolcraftImageArtifactInspection
|
|
49
|
+
| ToolcraftVideoArtifactInspection;
|
|
10
50
|
|
|
11
51
|
export type ToolcraftExportArtifactInspectionResult =
|
|
12
52
|
| ToolcraftExportArtifactInspection
|
|
@@ -33,13 +73,46 @@ function assertPositiveInteger(
|
|
|
33
73
|
field: string,
|
|
34
74
|
requirementId: string,
|
|
35
75
|
): asserts value is number {
|
|
76
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Export requirement "${requirementId}" must report a positive ${field} integer.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateNormalizedPixelBounds(
|
|
84
|
+
value: unknown,
|
|
85
|
+
requirementId: string,
|
|
86
|
+
): void {
|
|
87
|
+
if (value === null) return;
|
|
88
|
+
if (!isRecord(value)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Export requirement "${requirementId}" must report normalized nonBackgroundBounds or null.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const { height, width, x, y } = value;
|
|
94
|
+
for (const [field, coordinate] of Object.entries({ height, width, x, y })) {
|
|
95
|
+
if (
|
|
96
|
+
typeof coordinate !== "number" ||
|
|
97
|
+
!Number.isFinite(coordinate) ||
|
|
98
|
+
coordinate < 0 ||
|
|
99
|
+
coordinate > 1 ||
|
|
100
|
+
((field === "height" || field === "width") && coordinate === 0)
|
|
101
|
+
) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Export requirement "${requirementId}" must report normalized nonBackgroundBounds.${field}.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
36
107
|
if (
|
|
37
|
-
typeof
|
|
38
|
-
|
|
39
|
-
|
|
108
|
+
typeof x === "number" &&
|
|
109
|
+
typeof y === "number" &&
|
|
110
|
+
typeof width === "number" &&
|
|
111
|
+
typeof height === "number" &&
|
|
112
|
+
(x + width > 1 || y + height > 1)
|
|
40
113
|
) {
|
|
41
114
|
throw new Error(
|
|
42
|
-
`Export requirement "${requirementId}" must
|
|
115
|
+
`Export requirement "${requirementId}" nonBackgroundBounds must remain inside the normalized artifact.`,
|
|
43
116
|
);
|
|
44
117
|
}
|
|
45
118
|
}
|
|
@@ -64,25 +137,12 @@ export function assertToolcraftProducedArtifact(
|
|
|
64
137
|
}
|
|
65
138
|
}
|
|
66
139
|
|
|
67
|
-
|
|
68
|
-
|
|
140
|
+
function validateInspection(
|
|
141
|
+
inspection: Record<string, unknown>,
|
|
69
142
|
requirementId: string,
|
|
70
|
-
):
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
throw new Error(
|
|
74
|
-
`Export requirement "${requirementId}" must inspect at least one artifact.`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
for (const inspection of observations) {
|
|
79
|
-
if (!isRecord(inspection) || !("byteLength" in inspection)) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
`Export requirement "${requirementId}" must return a typed artifact inspection with byteLength.`,
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
assertPositiveInteger(inspection.byteLength, "byteLength", requirementId);
|
|
143
|
+
): void {
|
|
144
|
+
assertPositiveInteger(inspection.byteLength, "byteLength", requirementId);
|
|
145
|
+
if (inspection.kind === undefined || inspection.kind === "binary") {
|
|
86
146
|
if (inspection.durationMs !== undefined) {
|
|
87
147
|
assertPositiveFiniteNumber(
|
|
88
148
|
inspection.durationMs,
|
|
@@ -105,6 +165,108 @@ export function validateToolcraftExportArtifactInspection(
|
|
|
105
165
|
);
|
|
106
166
|
}
|
|
107
167
|
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (inspection.kind === "image") {
|
|
172
|
+
assertPositiveInteger(inspection.width, "width", requirementId);
|
|
173
|
+
assertPositiveInteger(inspection.height, "height", requirementId);
|
|
174
|
+
if (
|
|
175
|
+
inspection.mediaType !== "image/png" &&
|
|
176
|
+
inspection.mediaType !== "image/jpeg"
|
|
177
|
+
) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Export requirement "${requirementId}" must report a supported image mediaType.`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (
|
|
183
|
+
typeof inspection.decodedPixelHash !== "string" ||
|
|
184
|
+
!inspection.decodedPixelHash.trim()
|
|
185
|
+
) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Export requirement "${requirementId}" must hash decoded image pixels.`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
validateNormalizedPixelBounds(
|
|
191
|
+
inspection.nonBackgroundBounds,
|
|
192
|
+
requirementId,
|
|
193
|
+
);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (inspection.kind === "video") {
|
|
198
|
+
assertPositiveInteger(inspection.width, "width", requirementId);
|
|
199
|
+
assertPositiveInteger(inspection.height, "height", requirementId);
|
|
200
|
+
assertPositiveInteger(inspection.frameCount, "frameCount", requirementId);
|
|
201
|
+
assertPositiveFiniteNumber(inspection.durationMs, "durationMs", requirementId);
|
|
202
|
+
if (
|
|
203
|
+
inspection.mediaType !== "video/mp4" &&
|
|
204
|
+
inspection.mediaType !== "video/webm"
|
|
205
|
+
) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Export requirement "${requirementId}" must report a supported video mediaType.`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (
|
|
211
|
+
!Array.isArray(inspection.packetTimings) ||
|
|
212
|
+
inspection.packetTimings.length !== inspection.frameCount
|
|
213
|
+
) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Export requirement "${requirementId}" must report every real encoded packet timing.`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
for (const [index, timing] of inspection.packetTimings.entries()) {
|
|
219
|
+
if (
|
|
220
|
+
!isRecord(timing) ||
|
|
221
|
+
typeof timing.timeSeconds !== "number" ||
|
|
222
|
+
!Number.isFinite(timing.timeSeconds) ||
|
|
223
|
+
timing.timeSeconds < 0 ||
|
|
224
|
+
typeof timing.durationSeconds !== "number" ||
|
|
225
|
+
!Number.isFinite(timing.durationSeconds) ||
|
|
226
|
+
timing.durationSeconds <= 0
|
|
227
|
+
) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Export requirement "${requirementId}" must report valid timing for encoded packet ${index}.`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (
|
|
234
|
+
!Array.isArray(inspection.samplePixelHashes) ||
|
|
235
|
+
inspection.samplePixelHashes.length === 0 ||
|
|
236
|
+
inspection.samplePixelHashes.some(
|
|
237
|
+
(hash) => typeof hash !== "string" || !hash.trim(),
|
|
238
|
+
)
|
|
239
|
+
) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Export requirement "${requirementId}" must hash decoded video samples.`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
throw new Error(
|
|
248
|
+
`Export requirement "${requirementId}" returned an unknown artifact inspection kind.`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function validateToolcraftExportArtifactInspection(
|
|
253
|
+
observation: unknown,
|
|
254
|
+
requirementId: string,
|
|
255
|
+
): ToolcraftExportArtifactInspectionResult {
|
|
256
|
+
const observations = Array.isArray(observation) ? observation : [observation];
|
|
257
|
+
if (observations.length === 0) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Export requirement "${requirementId}" must inspect at least one artifact.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const inspection of observations) {
|
|
264
|
+
if (!isRecord(inspection) || !("byteLength" in inspection)) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Export requirement "${requirementId}" must return a typed artifact inspection with byteLength.`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
validateInspection(inspection, requirementId);
|
|
108
270
|
}
|
|
109
271
|
|
|
110
272
|
return observation as ToolcraftExportArtifactInspectionResult;
|
|
@@ -115,12 +277,31 @@ export function getToolcraftSemanticArtifactSignature(
|
|
|
115
277
|
requirementId: string,
|
|
116
278
|
): string {
|
|
117
279
|
validateToolcraftExportArtifactInspection(inspection, requirementId);
|
|
280
|
+
if (inspection.kind === "image") {
|
|
281
|
+
return JSON.stringify({
|
|
282
|
+
decodedPixelHash: inspection.decodedPixelHash,
|
|
283
|
+
height: inspection.height,
|
|
284
|
+
mediaType: inspection.mediaType,
|
|
285
|
+
nonBackgroundBounds: inspection.nonBackgroundBounds,
|
|
286
|
+
width: inspection.width,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (inspection.kind === "video") {
|
|
290
|
+
return JSON.stringify({
|
|
291
|
+
durationMs: inspection.durationMs,
|
|
292
|
+
frameCount: inspection.frameCount,
|
|
293
|
+
height: inspection.height,
|
|
294
|
+
mediaType: inspection.mediaType,
|
|
295
|
+
packetTimings: inspection.packetTimings,
|
|
296
|
+
samplePixelHashes: inspection.samplePixelHashes,
|
|
297
|
+
width: inspection.width,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
118
300
|
if (!inspection.contentHash?.trim()) {
|
|
119
301
|
throw new Error(
|
|
120
302
|
`Export requirement "${requirementId}" must report contentHash from decoded product output for semantic comparison.`,
|
|
121
303
|
);
|
|
122
304
|
}
|
|
123
|
-
|
|
124
305
|
return JSON.stringify({
|
|
125
306
|
contentHash: inspection.contentHash,
|
|
126
307
|
durationMs: inspection.durationMs,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { expect, test, type Download } from "@playwright/test";
|
|
4
|
+
|
|
5
|
+
import { inspectToolcraftImageDownload } from "./image-artifact-inspection";
|
|
6
|
+
|
|
7
|
+
test("image inspection hashes decoded pixels and reports product bounds", async ({
|
|
8
|
+
page,
|
|
9
|
+
}, testInfo) => {
|
|
10
|
+
await page.goto("/");
|
|
11
|
+
const base64 = await page.evaluate(() => {
|
|
12
|
+
const canvas = document.createElement("canvas");
|
|
13
|
+
canvas.width = 64;
|
|
14
|
+
canvas.height = 32;
|
|
15
|
+
const context = canvas.getContext("2d");
|
|
16
|
+
if (!context) throw new Error("Missing test canvas context.");
|
|
17
|
+
context.fillStyle = "#000000";
|
|
18
|
+
context.fillRect(0, 0, 64, 32);
|
|
19
|
+
context.fillStyle = "#FF0000";
|
|
20
|
+
context.fillRect(16, 8, 32, 16);
|
|
21
|
+
return canvas.toDataURL("image/png").split(",")[1]!;
|
|
22
|
+
});
|
|
23
|
+
const artifactPath = testInfo.outputPath("decoded-fixture.png");
|
|
24
|
+
await fs.writeFile(artifactPath, Buffer.from(base64, "base64"));
|
|
25
|
+
const download = { path: async () => artifactPath } as Download;
|
|
26
|
+
|
|
27
|
+
const result = await inspectToolcraftImageDownload({
|
|
28
|
+
backgroundRgba: [0, 0, 0, 255],
|
|
29
|
+
download,
|
|
30
|
+
page,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(result.inspection).toMatchObject({
|
|
34
|
+
height: 32,
|
|
35
|
+
kind: "image",
|
|
36
|
+
mediaType: "image/png",
|
|
37
|
+
nonBackgroundBounds: { height: 0.5, width: 0.5, x: 0.25, y: 0.25 },
|
|
38
|
+
width: 64,
|
|
39
|
+
});
|
|
40
|
+
expect(result.inspection.decodedPixelHash).toMatch(/^[a-f0-9]{64}$/u);
|
|
41
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import type { Download, Page } from "@playwright/test";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
observeToolcraftDecodedPixels,
|
|
7
|
+
type ToolcraftDecodedPixelObservation,
|
|
8
|
+
} from "./decoded-pixel-observation";
|
|
9
|
+
import type { ToolcraftImageArtifactInspection } from "./export-artifact-helpers";
|
|
10
|
+
|
|
11
|
+
export type ToolcraftInspectedImageArtifact = Readonly<{
|
|
12
|
+
inspection: ToolcraftImageArtifactInspection;
|
|
13
|
+
observation: ToolcraftDecodedPixelObservation;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
function detectImageMediaType(
|
|
17
|
+
bytes: Uint8Array,
|
|
18
|
+
): "image/jpeg" | "image/png" {
|
|
19
|
+
if (
|
|
20
|
+
bytes.length >= 8 &&
|
|
21
|
+
bytes[0] === 0x89 &&
|
|
22
|
+
bytes[1] === 0x50 &&
|
|
23
|
+
bytes[2] === 0x4e &&
|
|
24
|
+
bytes[3] === 0x47
|
|
25
|
+
) {
|
|
26
|
+
return "image/png";
|
|
27
|
+
}
|
|
28
|
+
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8) {
|
|
29
|
+
return "image/jpeg";
|
|
30
|
+
}
|
|
31
|
+
throw new Error("Downloaded artifact is not a decodable PNG or JPEG image.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function inspectToolcraftImageDownload({
|
|
35
|
+
backgroundRgba,
|
|
36
|
+
download,
|
|
37
|
+
page,
|
|
38
|
+
}: Readonly<{
|
|
39
|
+
backgroundRgba: readonly [number, number, number, number];
|
|
40
|
+
download: Download;
|
|
41
|
+
page: Page;
|
|
42
|
+
}>): Promise<ToolcraftInspectedImageArtifact> {
|
|
43
|
+
const downloadPath = await download.path();
|
|
44
|
+
if (!downloadPath) {
|
|
45
|
+
throw new Error("Playwright did not expose the downloaded image path.");
|
|
46
|
+
}
|
|
47
|
+
const bytes = await fs.readFile(downloadPath);
|
|
48
|
+
if (bytes.byteLength === 0) {
|
|
49
|
+
throw new Error("Downloaded image artifact is empty.");
|
|
50
|
+
}
|
|
51
|
+
const mediaType = detectImageMediaType(bytes);
|
|
52
|
+
const decoded = await page.evaluate(
|
|
53
|
+
async ({ base64, mediaType: decodedMediaType }) => {
|
|
54
|
+
const binary = atob(base64);
|
|
55
|
+
const data = Uint8Array.from(binary, (character) =>
|
|
56
|
+
character.charCodeAt(0),
|
|
57
|
+
);
|
|
58
|
+
const bitmap = await createImageBitmap(
|
|
59
|
+
new Blob([data], { type: decodedMediaType }),
|
|
60
|
+
);
|
|
61
|
+
try {
|
|
62
|
+
const canvas = document.createElement("canvas");
|
|
63
|
+
canvas.width = 64;
|
|
64
|
+
canvas.height = 64;
|
|
65
|
+
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
66
|
+
if (!context) throw new Error("Unable to inspect exported image pixels.");
|
|
67
|
+
context.imageSmoothingEnabled = false;
|
|
68
|
+
context.drawImage(bitmap, 0, 0, 64, 64);
|
|
69
|
+
return {
|
|
70
|
+
height: bitmap.height,
|
|
71
|
+
pixels: Array.from(context.getImageData(0, 0, 64, 64).data),
|
|
72
|
+
width: bitmap.width,
|
|
73
|
+
};
|
|
74
|
+
} finally {
|
|
75
|
+
bitmap.close();
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{ base64: bytes.toString("base64"), mediaType },
|
|
79
|
+
);
|
|
80
|
+
const observation = await observeToolcraftDecodedPixels({
|
|
81
|
+
backgroundRgba,
|
|
82
|
+
pixels: Uint8ClampedArray.from(decoded.pixels),
|
|
83
|
+
sourceHeight: 64,
|
|
84
|
+
sourceWidth: 64,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
inspection: Object.freeze({
|
|
89
|
+
byteLength: bytes.byteLength,
|
|
90
|
+
decodedPixelHash: observation.decodedPixelHash,
|
|
91
|
+
height: decoded.height,
|
|
92
|
+
kind: "image",
|
|
93
|
+
mediaType,
|
|
94
|
+
nonBackgroundBounds: observation.nonBackgroundBounds,
|
|
95
|
+
width: decoded.width,
|
|
96
|
+
}),
|
|
97
|
+
observation,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -38,22 +38,22 @@ export async function dragToolcraftCanvasViewport(
|
|
|
38
38
|
|
|
39
39
|
export async function zoomToolcraftCanvasViewport(
|
|
40
40
|
page: Page,
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
options: {
|
|
42
|
+
direction: "in" | "out";
|
|
43
|
+
pathId?: string;
|
|
44
|
+
repetitions?: number;
|
|
45
|
+
},
|
|
43
46
|
): Promise<void> {
|
|
44
47
|
const zoomIn = page.getByRole("button", { name: "Zoom in" });
|
|
45
48
|
const zoomOut = page.getByRole("button", { name: "Zoom out" });
|
|
49
|
+
const control = options.direction === "in" ? zoomIn : zoomOut;
|
|
50
|
+
await expect(
|
|
51
|
+
control,
|
|
52
|
+
`Toolcraft zoom-${options.direction} control should be visible`,
|
|
53
|
+
).toBeVisible();
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
for (let index = 0; index < repetitions; index += 1) {
|
|
51
|
-
await zoomIn.click();
|
|
52
|
-
await waitForToolcraftAnimationFrames(page, 2);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (let index = 0; index < repetitions; index += 1) {
|
|
56
|
-
await zoomOut.click();
|
|
55
|
+
for (let index = 0; index < (options.repetitions ?? 1); index += 1) {
|
|
56
|
+
await control.click();
|
|
57
57
|
await waitForToolcraftAnimationFrames(page, 2);
|
|
58
58
|
}
|
|
59
59
|
if (options.pathId) {
|
|
@@ -70,17 +70,18 @@ export async function readToolcraftCanvasViewport(page: Page): Promise<{
|
|
|
70
70
|
zoom: number;
|
|
71
71
|
}> {
|
|
72
72
|
return page.evaluate(() => {
|
|
73
|
-
const
|
|
74
|
-
const style = canvas ? window.getComputedStyle(canvas) : null;
|
|
75
|
-
const zoomText =
|
|
76
|
-
canvas?.getAttribute("data-canvas-zoom") ??
|
|
77
|
-
style?.getPropertyValue("--canvas-zoom") ??
|
|
78
|
-
"1";
|
|
73
|
+
const world = document.querySelector("[data-toolcraft-canvas-world]");
|
|
79
74
|
|
|
80
75
|
return {
|
|
81
|
-
offsetX: Number(
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
offsetX: Number(
|
|
77
|
+
world?.getAttribute("data-toolcraft-canvas-offset-x") ?? 0,
|
|
78
|
+
),
|
|
79
|
+
offsetY: Number(
|
|
80
|
+
world?.getAttribute("data-toolcraft-canvas-offset-y") ?? 0,
|
|
81
|
+
),
|
|
82
|
+
zoom: Number.parseFloat(
|
|
83
|
+
world?.getAttribute("data-toolcraft-canvas-zoom") ?? "100",
|
|
84
|
+
) || 100,
|
|
84
85
|
};
|
|
85
86
|
});
|
|
86
87
|
}
|