@viewscript/renderer 0.1.0-202605140639
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/dist/ast/types.d.ts +403 -0
- package/dist/ast/types.js +33 -0
- package/dist/compiler/chunk-splitter.d.ts +98 -0
- package/dist/compiler/chunk-splitter.js +361 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +17 -0
- package/dist/rasterizer/__tests__/error-distribution.test.d.ts +7 -0
- package/dist/rasterizer/__tests__/error-distribution.test.js +322 -0
- package/dist/rasterizer/canvas-mapper.d.ts +280 -0
- package/dist/rasterizer/canvas-mapper.js +414 -0
- package/dist/rasterizer/error-distribution.d.ts +143 -0
- package/dist/rasterizer/error-distribution.js +231 -0
- package/dist/rasterizer/gradient-mapper.d.ts +223 -0
- package/dist/rasterizer/gradient-mapper.js +352 -0
- package/dist/rasterizer/topology-rounding.d.ts +151 -0
- package/dist/rasterizer/topology-rounding.js +347 -0
- package/dist/runtime/__tests__/event-backpressure.test.d.ts +10 -0
- package/dist/runtime/__tests__/event-backpressure.test.js +190 -0
- package/dist/runtime/event-backpressure.d.ts +393 -0
- package/dist/runtime/event-backpressure.js +458 -0
- package/dist/runtime/render-loop.d.ts +277 -0
- package/dist/runtime/render-loop.js +435 -0
- package/dist/runtime/wasm-resource-manager.d.ts +122 -0
- package/dist/runtime/wasm-resource-manager.js +253 -0
- package/dist/runtime/wgpu-renderer-adapter.d.ts +168 -0
- package/dist/runtime/wgpu-renderer-adapter.js +230 -0
- package/dist/semantic/__tests__/semantic-translator.test.d.ts +4 -0
- package/dist/semantic/__tests__/semantic-translator.test.js +203 -0
- package/dist/semantic/semantic-translator.d.ts +229 -0
- package/dist/semantic/semantic-translator.js +398 -0
- package/package.json +28 -0
- package/playwright-report/data/0bafe4e0863f0e244bba68a838f73241f8f2efaa.md +226 -0
- package/playwright-report/data/9281aca8abfb06c6cecb35d5ddd13d61f8c752d8.md +226 -0
- package/playwright-report/index.html +90 -0
- package/playwright.config.ts +160 -0
- package/screenshot-chrome.png +0 -0
- package/screenshots/visual-demo-verification.png +0 -0
- package/screenshots/visual-demo.png +0 -0
- package/src/ast/types.ts +473 -0
- package/src/compiler/chunk-splitter.ts +534 -0
- package/src/index.ts +62 -0
- package/src/rasterizer/__tests__/error-distribution.test.ts +382 -0
- package/src/rasterizer/canvas-mapper.ts +677 -0
- package/src/rasterizer/error-distribution.ts +344 -0
- package/src/rasterizer/gradient-mapper.ts +563 -0
- package/src/rasterizer/topology-rounding.ts +499 -0
- package/src/runtime/__tests__/event-backpressure.test.ts +254 -0
- package/src/runtime/event-backpressure.ts +622 -0
- package/src/runtime/render-loop.ts +660 -0
- package/src/runtime/wasm-resource-manager.ts +349 -0
- package/src/runtime/wgpu-renderer-adapter.ts +318 -0
- package/src/semantic/__tests__/semantic-translator.test.ts +263 -0
- package/src/semantic/semantic-translator.ts +637 -0
- package/test-results/.last-run.json +4 -0
- package/tests/e2e/async-race.spec.ts +612 -0
- package/tests/e2e/bilayer-sync.spec.ts +405 -0
- package/tests/e2e/failures/.gitkeep +0 -0
- package/tests/e2e/fullstack.spec.ts +681 -0
- package/tests/e2e/g1-continuity.spec.ts +703 -0
- package/tests/e2e/golden/.gitkeep +0 -0
- package/tests/e2e/golden/conic-color-wheel.raw +0 -0
- package/tests/e2e/golden/conic-color-wheel.sha256 +1 -0
- package/tests/e2e/golden/conic-rotated.raw +0 -0
- package/tests/e2e/golden/conic-rotated.sha256 +1 -0
- package/tests/e2e/golden/linear-45deg.raw +0 -0
- package/tests/e2e/golden/linear-45deg.sha256 +1 -0
- package/tests/e2e/golden/linear-horizontal.raw +0 -0
- package/tests/e2e/golden/linear-horizontal.sha256 +1 -0
- package/tests/e2e/golden/linear-multi-stop.raw +0 -0
- package/tests/e2e/golden/linear-multi-stop.sha256 +1 -0
- package/tests/e2e/golden/radial-circle-center.raw +0 -0
- package/tests/e2e/golden/radial-circle-center.sha256 +1 -0
- package/tests/e2e/golden/radial-offset.raw +0 -0
- package/tests/e2e/golden/radial-offset.sha256 +1 -0
- package/tests/e2e/golden/tile-mirror.raw +0 -0
- package/tests/e2e/golden/tile-mirror.sha256 +1 -0
- package/tests/e2e/golden/tile-repeat.raw +0 -0
- package/tests/e2e/golden/tile-repeat.sha256 +1 -0
- package/tests/e2e/gradient-animation.spec.ts +606 -0
- package/tests/e2e/memory-stability.spec.ts +396 -0
- package/tests/e2e/path-topology.spec.ts +674 -0
- package/tests/e2e/performance-profile.spec.ts +501 -0
- package/tests/e2e/screenshot.spec.ts +60 -0
- package/tests/e2e/test-harness.html +1005 -0
- package/tests/e2e/text-layout.spec.ts +451 -0
- package/tests/e2e/visual-demo.html +340 -0
- package/tests/e2e/visual-regression.spec.ts +335 -0
- package/tsconfig.json +12 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Layout E2E Tests (Phase 10)
|
|
3
|
+
*
|
|
4
|
+
* This module verifies that text entities with Q→P dimension bridging
|
|
5
|
+
* correctly constrain their containing elements.
|
|
6
|
+
*
|
|
7
|
+
* ## The Problem: Constraint-Based Text Layout
|
|
8
|
+
*
|
|
9
|
+
* Text dimensions are Q-dimension (non-deterministic, font-dependent).
|
|
10
|
+
* P-dimension constraints need exact rational values. The bridge:
|
|
11
|
+
*
|
|
12
|
+
* 1. Renderer measures text using wgpu renderer/DOM → W, H (pixels)
|
|
13
|
+
* 2. CLI receives: `vsc update-metrics --id=N --width=W --height=H`
|
|
14
|
+
* 3. P-dimension solver updates bounding box constraints
|
|
15
|
+
* 4. Containing elements (buttons, etc.) resize accordingly
|
|
16
|
+
*
|
|
17
|
+
* ## Test Strategy
|
|
18
|
+
*
|
|
19
|
+
* 1. Create a text entity via CLI
|
|
20
|
+
* 2. Simulate Renderer measurement
|
|
21
|
+
* 3. Update metrics via CLI
|
|
22
|
+
* 4. Verify containing button resizes to fit text
|
|
23
|
+
*
|
|
24
|
+
* ## Font Determinism
|
|
25
|
+
*
|
|
26
|
+
* To ensure deterministic tests, we use a locally bundled monospace font.
|
|
27
|
+
* This eliminates variations from system fonts or network-loaded fonts.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { test, expect } from '@playwright/test';
|
|
31
|
+
import { execSync } from 'child_process';
|
|
32
|
+
import * as path from 'path';
|
|
33
|
+
import * as fs from 'fs';
|
|
34
|
+
import * as os from 'os';
|
|
35
|
+
import { fileURLToPath } from 'url';
|
|
36
|
+
|
|
37
|
+
// ESM-compatible __dirname replacement
|
|
38
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
39
|
+
const __dirname = path.dirname(__filename);
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Test Configuration
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Monospace font metrics for deterministic testing.
|
|
47
|
+
* Using a standard monospace where each character has equal width.
|
|
48
|
+
*/
|
|
49
|
+
const MONOSPACE_CHAR_WIDTH = 10; // pixels per character at 16px font size
|
|
50
|
+
const MONOSPACE_LINE_HEIGHT = 20; // pixels per line at 16px font size
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Button padding around text.
|
|
54
|
+
*/
|
|
55
|
+
const BUTTON_PADDING = 8;
|
|
56
|
+
|
|
57
|
+
// =============================================================================
|
|
58
|
+
// CLI Helper
|
|
59
|
+
// =============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Execute a VSC CLI command and return the parsed result.
|
|
63
|
+
*/
|
|
64
|
+
function vsc(args: string[], cwd: string): { exitCode: number; output: unknown; error?: string } {
|
|
65
|
+
const vscPath = path.resolve(__dirname, '../../../../target/debug/vsc');
|
|
66
|
+
const fullCommand = `${vscPath} ${args.join(' ')}`;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const stdout = execSync(fullCommand, {
|
|
70
|
+
cwd,
|
|
71
|
+
encoding: 'utf-8',
|
|
72
|
+
env: {
|
|
73
|
+
...process.env,
|
|
74
|
+
VS_FIXED_TIME: '1', // Deterministic timestamps
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
exitCode: 0,
|
|
80
|
+
output: JSON.parse(stdout),
|
|
81
|
+
};
|
|
82
|
+
} catch (error: unknown) {
|
|
83
|
+
if (error && typeof error === 'object' && 'status' in error) {
|
|
84
|
+
const execError = error as { status: number; stdout?: string; stderr?: string };
|
|
85
|
+
return {
|
|
86
|
+
exitCode: execError.status || 1,
|
|
87
|
+
output: execError.stdout ? JSON.parse(execError.stdout) : null,
|
|
88
|
+
error: execError.stderr || 'Command failed',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Simulate text measurement (deterministic for monospace).
|
|
97
|
+
*/
|
|
98
|
+
function measureTextMonospace(content: string, fontSize: number): { width: number; height: number } {
|
|
99
|
+
const charWidth = (fontSize / 16) * MONOSPACE_CHAR_WIDTH;
|
|
100
|
+
const lineHeight = (fontSize / 16) * MONOSPACE_LINE_HEIGHT;
|
|
101
|
+
const lines = content.split('\n');
|
|
102
|
+
|
|
103
|
+
const maxLineLength = Math.max(...lines.map(l => l.length));
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
width: Math.ceil(maxLineLength * charWidth),
|
|
107
|
+
height: Math.ceil(lines.length * lineHeight),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// Text Layout Tests
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
test.describe('Text Layout: Q→P Dimension Bridge', () => {
|
|
116
|
+
let testDir: string;
|
|
117
|
+
|
|
118
|
+
test.beforeEach(async () => {
|
|
119
|
+
// Create a temporary directory for each test
|
|
120
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vsc-text-layout-'));
|
|
121
|
+
|
|
122
|
+
// Initialize a ViewScript project
|
|
123
|
+
const initResult = vsc(['init', '--name=text-layout-test'], testDir);
|
|
124
|
+
expect(initResult.exitCode).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test.afterEach(async () => {
|
|
128
|
+
// Clean up
|
|
129
|
+
if (testDir && fs.existsSync(testDir)) {
|
|
130
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Test: Text entity creation generates 4 corner control points
|
|
136
|
+
*/
|
|
137
|
+
test('add-entity --entity-type=text creates text with 4 corner control points', async () => {
|
|
138
|
+
const result = vsc([
|
|
139
|
+
'add-entity',
|
|
140
|
+
'--entity-type=text',
|
|
141
|
+
'--content="Hello, World!"',
|
|
142
|
+
'--font-family="monospace"',
|
|
143
|
+
'--font-size=16',
|
|
144
|
+
'--x=0',
|
|
145
|
+
'--y=0',
|
|
146
|
+
], testDir);
|
|
147
|
+
|
|
148
|
+
expect(result.exitCode).toBe(0);
|
|
149
|
+
|
|
150
|
+
const output = result.output as {
|
|
151
|
+
status: string;
|
|
152
|
+
entity_type: string;
|
|
153
|
+
entity_id: number;
|
|
154
|
+
corner_tl: number;
|
|
155
|
+
corner_tr: number;
|
|
156
|
+
corner_bl: number;
|
|
157
|
+
corner_br: number;
|
|
158
|
+
metrics_pending: boolean;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
expect(output.status).toBe('success');
|
|
162
|
+
expect(output.entity_type).toBe('text');
|
|
163
|
+
expect(output.entity_id).toBeDefined();
|
|
164
|
+
expect(output.corner_tl).toBe(output.entity_id + 1);
|
|
165
|
+
expect(output.corner_tr).toBe(output.entity_id + 2);
|
|
166
|
+
expect(output.corner_bl).toBe(output.entity_id + 3);
|
|
167
|
+
expect(output.corner_br).toBe(output.entity_id + 4);
|
|
168
|
+
expect(output.metrics_pending).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Test: update-metrics adds bounding box constraints
|
|
173
|
+
*/
|
|
174
|
+
test('update-metrics adds width and height constraints', async () => {
|
|
175
|
+
// Create text entity
|
|
176
|
+
const addResult = vsc([
|
|
177
|
+
'add-entity',
|
|
178
|
+
'--entity-type=text',
|
|
179
|
+
'--content="Test"',
|
|
180
|
+
'--font-family="monospace"',
|
|
181
|
+
'--font-size=16',
|
|
182
|
+
], testDir);
|
|
183
|
+
|
|
184
|
+
expect(addResult.exitCode).toBe(0);
|
|
185
|
+
const textId = (addResult.output as { entity_id: number }).entity_id;
|
|
186
|
+
|
|
187
|
+
// Measure text (simulated)
|
|
188
|
+
const metrics = measureTextMonospace('Test', 16);
|
|
189
|
+
|
|
190
|
+
// Update metrics
|
|
191
|
+
const updateResult = vsc([
|
|
192
|
+
'update-metrics',
|
|
193
|
+
`--id=${textId}`,
|
|
194
|
+
`--width=${metrics.width}`,
|
|
195
|
+
`--height=${metrics.height}`,
|
|
196
|
+
], testDir);
|
|
197
|
+
|
|
198
|
+
expect(updateResult.exitCode).toBe(0);
|
|
199
|
+
|
|
200
|
+
const output = updateResult.output as {
|
|
201
|
+
status: string;
|
|
202
|
+
constraints_added: number;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
expect(output.status).toBe('success');
|
|
206
|
+
// 8 constraints: 2 width, 2 height, 4 alignment
|
|
207
|
+
expect(output.constraints_added).toBe(8);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Test: Button containing text resizes based on text metrics
|
|
212
|
+
*
|
|
213
|
+
* Scenario:
|
|
214
|
+
* 1. Create text entity "Click Me"
|
|
215
|
+
* 2. Update metrics (simulated measurement)
|
|
216
|
+
* 3. Create button constraints: button.width = text.width + 2*padding
|
|
217
|
+
* 4. Verify button width matches expected value
|
|
218
|
+
*/
|
|
219
|
+
test('button width is constrained by text width plus padding', async () => {
|
|
220
|
+
const textContent = 'Click Me';
|
|
221
|
+
|
|
222
|
+
// Step 1: Create text entity
|
|
223
|
+
const addResult = vsc([
|
|
224
|
+
'add-entity',
|
|
225
|
+
'--entity-type=text',
|
|
226
|
+
`--content="${textContent}"`,
|
|
227
|
+
'--font-family="monospace"',
|
|
228
|
+
'--font-size=16',
|
|
229
|
+
'--x=100',
|
|
230
|
+
'--y=100',
|
|
231
|
+
], testDir);
|
|
232
|
+
|
|
233
|
+
expect(addResult.exitCode).toBe(0);
|
|
234
|
+
const addOutput = addResult.output as {
|
|
235
|
+
entity_id: number;
|
|
236
|
+
corner_tl: number;
|
|
237
|
+
corner_tr: number;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// Step 2: Measure and update metrics
|
|
241
|
+
const metrics = measureTextMonospace(textContent, 16);
|
|
242
|
+
|
|
243
|
+
const updateResult = vsc([
|
|
244
|
+
'update-metrics',
|
|
245
|
+
`--id=${addOutput.entity_id}`,
|
|
246
|
+
`--width=${metrics.width}`,
|
|
247
|
+
`--height=${metrics.height}`,
|
|
248
|
+
], testDir);
|
|
249
|
+
|
|
250
|
+
expect(updateResult.exitCode).toBe(0);
|
|
251
|
+
|
|
252
|
+
// Step 3: Create button entity and constrain it to text
|
|
253
|
+
// Button width = TR.x - TL.x + 2*padding
|
|
254
|
+
// This is: (TL.x + text_width) - TL.x + 2*padding = text_width + 2*padding
|
|
255
|
+
|
|
256
|
+
// For this test, we verify the constraint graph was updated correctly
|
|
257
|
+
// by reading the buildinfo and checking the constraints exist
|
|
258
|
+
const buildInfoPath = path.join(testDir, '.vsbuildinfo');
|
|
259
|
+
const buildInfo = JSON.parse(fs.readFileSync(buildInfoPath, 'utf-8'));
|
|
260
|
+
|
|
261
|
+
// Verify text entity entry exists with correct metrics
|
|
262
|
+
expect(buildInfo.text_entities).toBeDefined();
|
|
263
|
+
expect(buildInfo.text_entities.length).toBe(1);
|
|
264
|
+
|
|
265
|
+
const textEntry = buildInfo.text_entities[0];
|
|
266
|
+
expect(textEntry.metrics_resolved).toBe(true);
|
|
267
|
+
expect(textEntry.measured_width).toBe(`${metrics.width}/1`);
|
|
268
|
+
expect(textEntry.measured_height).toBe(`${metrics.height}/1`);
|
|
269
|
+
|
|
270
|
+
// Verify constraints were added
|
|
271
|
+
// Initial: 2 constraints for TL position
|
|
272
|
+
// Metrics: 8 constraints for bounding box
|
|
273
|
+
const constraintCount = buildInfo.operations.filter(
|
|
274
|
+
(op: { op_type: string }) => op.op_type === 'add'
|
|
275
|
+
).length;
|
|
276
|
+
|
|
277
|
+
expect(constraintCount).toBe(2 + 8);
|
|
278
|
+
|
|
279
|
+
// Calculate expected button width
|
|
280
|
+
const expectedButtonWidth = metrics.width + 2 * BUTTON_PADDING;
|
|
281
|
+
|
|
282
|
+
// For this test, we just verify the text width + padding calculation
|
|
283
|
+
// In a full E2E test with the renderer, we would verify the visual output
|
|
284
|
+
expect(expectedButtonWidth).toBe(textContent.length * MONOSPACE_CHAR_WIDTH + 2 * BUTTON_PADDING);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Test: Multi-line text height is correctly calculated
|
|
289
|
+
*/
|
|
290
|
+
test('multi-line text height is sum of line heights', async () => {
|
|
291
|
+
const textContent = 'Line 1\\nLine 2\\nLine 3';
|
|
292
|
+
|
|
293
|
+
const addResult = vsc([
|
|
294
|
+
'add-entity',
|
|
295
|
+
'--entity-type=text',
|
|
296
|
+
`--content="${textContent}"`,
|
|
297
|
+
'--font-family="monospace"',
|
|
298
|
+
'--font-size=16',
|
|
299
|
+
], testDir);
|
|
300
|
+
|
|
301
|
+
expect(addResult.exitCode).toBe(0);
|
|
302
|
+
const textId = (addResult.output as { entity_id: number }).entity_id;
|
|
303
|
+
|
|
304
|
+
// Measure with 3 lines
|
|
305
|
+
const metrics = measureTextMonospace('Line 1\nLine 2\nLine 3', 16);
|
|
306
|
+
expect(metrics.height).toBe(3 * MONOSPACE_LINE_HEIGHT);
|
|
307
|
+
|
|
308
|
+
const updateResult = vsc([
|
|
309
|
+
'update-metrics',
|
|
310
|
+
`--id=${textId}`,
|
|
311
|
+
`--width=${metrics.width}`,
|
|
312
|
+
`--height=${metrics.height}`,
|
|
313
|
+
], testDir);
|
|
314
|
+
|
|
315
|
+
expect(updateResult.exitCode).toBe(0);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Test: Font size scaling affects metrics proportionally
|
|
320
|
+
*/
|
|
321
|
+
test('font size 32 doubles text dimensions compared to font size 16', async () => {
|
|
322
|
+
const textContent = 'Scale Test';
|
|
323
|
+
|
|
324
|
+
// Create two text entities with different font sizes
|
|
325
|
+
const add16 = vsc([
|
|
326
|
+
'add-entity',
|
|
327
|
+
'--entity-type=text',
|
|
328
|
+
`--content="${textContent}"`,
|
|
329
|
+
'--font-family="monospace"',
|
|
330
|
+
'--font-size=16',
|
|
331
|
+
], testDir);
|
|
332
|
+
|
|
333
|
+
expect(add16.exitCode).toBe(0);
|
|
334
|
+
const id16 = (add16.output as { entity_id: number }).entity_id;
|
|
335
|
+
|
|
336
|
+
const add32 = vsc([
|
|
337
|
+
'add-entity',
|
|
338
|
+
'--entity-type=text',
|
|
339
|
+
`--content="${textContent}"`,
|
|
340
|
+
'--font-family="monospace"',
|
|
341
|
+
'--font-size=32',
|
|
342
|
+
], testDir);
|
|
343
|
+
|
|
344
|
+
expect(add32.exitCode).toBe(0);
|
|
345
|
+
const id32 = (add32.output as { entity_id: number }).entity_id;
|
|
346
|
+
|
|
347
|
+
// Measure both
|
|
348
|
+
const metrics16 = measureTextMonospace(textContent, 16);
|
|
349
|
+
const metrics32 = measureTextMonospace(textContent, 32);
|
|
350
|
+
|
|
351
|
+
// 32px should be exactly 2x the 16px dimensions
|
|
352
|
+
expect(metrics32.width).toBe(metrics16.width * 2);
|
|
353
|
+
expect(metrics32.height).toBe(metrics16.height * 2);
|
|
354
|
+
|
|
355
|
+
// Update metrics for both
|
|
356
|
+
const update16 = vsc([
|
|
357
|
+
'update-metrics',
|
|
358
|
+
`--id=${id16}`,
|
|
359
|
+
`--width=${metrics16.width}`,
|
|
360
|
+
`--height=${metrics16.height}`,
|
|
361
|
+
], testDir);
|
|
362
|
+
expect(update16.exitCode).toBe(0);
|
|
363
|
+
|
|
364
|
+
const update32 = vsc([
|
|
365
|
+
'update-metrics',
|
|
366
|
+
`--id=${id32}`,
|
|
367
|
+
`--width=${metrics32.width}`,
|
|
368
|
+
`--height=${metrics32.height}`,
|
|
369
|
+
], testDir);
|
|
370
|
+
expect(update32.exitCode).toBe(0);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Test: update-metrics for non-existent entity returns error
|
|
375
|
+
*/
|
|
376
|
+
test('update-metrics for non-existent entity fails gracefully', async () => {
|
|
377
|
+
const result = vsc([
|
|
378
|
+
'update-metrics',
|
|
379
|
+
'--id=99999',
|
|
380
|
+
'--width=100',
|
|
381
|
+
'--height=20',
|
|
382
|
+
], testDir);
|
|
383
|
+
|
|
384
|
+
expect(result.exitCode).toBe(1);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Test: Invalid width/height values are rejected
|
|
389
|
+
*/
|
|
390
|
+
test('update-metrics with invalid values is rejected', async () => {
|
|
391
|
+
// Create a text entity first
|
|
392
|
+
const addResult = vsc([
|
|
393
|
+
'add-entity',
|
|
394
|
+
'--entity-type=text',
|
|
395
|
+
'--content="Test"',
|
|
396
|
+
], testDir);
|
|
397
|
+
|
|
398
|
+
expect(addResult.exitCode).toBe(0);
|
|
399
|
+
const textId = (addResult.output as { entity_id: number }).entity_id;
|
|
400
|
+
|
|
401
|
+
// Try to update with invalid width
|
|
402
|
+
const result = vsc([
|
|
403
|
+
'update-metrics',
|
|
404
|
+
`--id=${textId}`,
|
|
405
|
+
'--width=invalid',
|
|
406
|
+
'--height=20',
|
|
407
|
+
], testDir);
|
|
408
|
+
|
|
409
|
+
expect(result.exitCode).toBe(1);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// =============================================================================
|
|
414
|
+
// Determinism Tests (Font Loading)
|
|
415
|
+
// =============================================================================
|
|
416
|
+
|
|
417
|
+
test.describe('Text Layout: Determinism', () => {
|
|
418
|
+
let testDir: string;
|
|
419
|
+
|
|
420
|
+
test.beforeEach(async () => {
|
|
421
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vsc-text-determinism-'));
|
|
422
|
+
vsc(['init', '--name=determinism-test'], testDir);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test.afterEach(async () => {
|
|
426
|
+
if (testDir && fs.existsSync(testDir)) {
|
|
427
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Test: Same content produces same measurements
|
|
433
|
+
*
|
|
434
|
+
* This validates that our simulated monospace measurement is deterministic.
|
|
435
|
+
* In production, font loading async issues can cause non-determinism.
|
|
436
|
+
*/
|
|
437
|
+
test('identical text produces identical measurements across runs', async () => {
|
|
438
|
+
const textContent = 'Determinism Test String';
|
|
439
|
+
|
|
440
|
+
// Run measurement 3 times
|
|
441
|
+
const measurements = Array.from({ length: 3 }, () =>
|
|
442
|
+
measureTextMonospace(textContent, 16)
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
// All measurements should be identical
|
|
446
|
+
expect(measurements[0].width).toBe(measurements[1].width);
|
|
447
|
+
expect(measurements[0].width).toBe(measurements[2].width);
|
|
448
|
+
expect(measurements[0].height).toBe(measurements[1].height);
|
|
449
|
+
expect(measurements[0].height).toBe(measurements[2].height);
|
|
450
|
+
});
|
|
451
|
+
});
|