@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.
Files changed (89) hide show
  1. package/dist/ast/types.d.ts +403 -0
  2. package/dist/ast/types.js +33 -0
  3. package/dist/compiler/chunk-splitter.d.ts +98 -0
  4. package/dist/compiler/chunk-splitter.js +361 -0
  5. package/dist/index.d.ts +55 -0
  6. package/dist/index.js +17 -0
  7. package/dist/rasterizer/__tests__/error-distribution.test.d.ts +7 -0
  8. package/dist/rasterizer/__tests__/error-distribution.test.js +322 -0
  9. package/dist/rasterizer/canvas-mapper.d.ts +280 -0
  10. package/dist/rasterizer/canvas-mapper.js +414 -0
  11. package/dist/rasterizer/error-distribution.d.ts +143 -0
  12. package/dist/rasterizer/error-distribution.js +231 -0
  13. package/dist/rasterizer/gradient-mapper.d.ts +223 -0
  14. package/dist/rasterizer/gradient-mapper.js +352 -0
  15. package/dist/rasterizer/topology-rounding.d.ts +151 -0
  16. package/dist/rasterizer/topology-rounding.js +347 -0
  17. package/dist/runtime/__tests__/event-backpressure.test.d.ts +10 -0
  18. package/dist/runtime/__tests__/event-backpressure.test.js +190 -0
  19. package/dist/runtime/event-backpressure.d.ts +393 -0
  20. package/dist/runtime/event-backpressure.js +458 -0
  21. package/dist/runtime/render-loop.d.ts +277 -0
  22. package/dist/runtime/render-loop.js +435 -0
  23. package/dist/runtime/wasm-resource-manager.d.ts +122 -0
  24. package/dist/runtime/wasm-resource-manager.js +253 -0
  25. package/dist/runtime/wgpu-renderer-adapter.d.ts +168 -0
  26. package/dist/runtime/wgpu-renderer-adapter.js +230 -0
  27. package/dist/semantic/__tests__/semantic-translator.test.d.ts +4 -0
  28. package/dist/semantic/__tests__/semantic-translator.test.js +203 -0
  29. package/dist/semantic/semantic-translator.d.ts +229 -0
  30. package/dist/semantic/semantic-translator.js +398 -0
  31. package/package.json +28 -0
  32. package/playwright-report/data/0bafe4e0863f0e244bba68a838f73241f8f2efaa.md +226 -0
  33. package/playwright-report/data/9281aca8abfb06c6cecb35d5ddd13d61f8c752d8.md +226 -0
  34. package/playwright-report/index.html +90 -0
  35. package/playwright.config.ts +160 -0
  36. package/screenshot-chrome.png +0 -0
  37. package/screenshots/visual-demo-verification.png +0 -0
  38. package/screenshots/visual-demo.png +0 -0
  39. package/src/ast/types.ts +473 -0
  40. package/src/compiler/chunk-splitter.ts +534 -0
  41. package/src/index.ts +62 -0
  42. package/src/rasterizer/__tests__/error-distribution.test.ts +382 -0
  43. package/src/rasterizer/canvas-mapper.ts +677 -0
  44. package/src/rasterizer/error-distribution.ts +344 -0
  45. package/src/rasterizer/gradient-mapper.ts +563 -0
  46. package/src/rasterizer/topology-rounding.ts +499 -0
  47. package/src/runtime/__tests__/event-backpressure.test.ts +254 -0
  48. package/src/runtime/event-backpressure.ts +622 -0
  49. package/src/runtime/render-loop.ts +660 -0
  50. package/src/runtime/wasm-resource-manager.ts +349 -0
  51. package/src/runtime/wgpu-renderer-adapter.ts +318 -0
  52. package/src/semantic/__tests__/semantic-translator.test.ts +263 -0
  53. package/src/semantic/semantic-translator.ts +637 -0
  54. package/test-results/.last-run.json +4 -0
  55. package/tests/e2e/async-race.spec.ts +612 -0
  56. package/tests/e2e/bilayer-sync.spec.ts +405 -0
  57. package/tests/e2e/failures/.gitkeep +0 -0
  58. package/tests/e2e/fullstack.spec.ts +681 -0
  59. package/tests/e2e/g1-continuity.spec.ts +703 -0
  60. package/tests/e2e/golden/.gitkeep +0 -0
  61. package/tests/e2e/golden/conic-color-wheel.raw +0 -0
  62. package/tests/e2e/golden/conic-color-wheel.sha256 +1 -0
  63. package/tests/e2e/golden/conic-rotated.raw +0 -0
  64. package/tests/e2e/golden/conic-rotated.sha256 +1 -0
  65. package/tests/e2e/golden/linear-45deg.raw +0 -0
  66. package/tests/e2e/golden/linear-45deg.sha256 +1 -0
  67. package/tests/e2e/golden/linear-horizontal.raw +0 -0
  68. package/tests/e2e/golden/linear-horizontal.sha256 +1 -0
  69. package/tests/e2e/golden/linear-multi-stop.raw +0 -0
  70. package/tests/e2e/golden/linear-multi-stop.sha256 +1 -0
  71. package/tests/e2e/golden/radial-circle-center.raw +0 -0
  72. package/tests/e2e/golden/radial-circle-center.sha256 +1 -0
  73. package/tests/e2e/golden/radial-offset.raw +0 -0
  74. package/tests/e2e/golden/radial-offset.sha256 +1 -0
  75. package/tests/e2e/golden/tile-mirror.raw +0 -0
  76. package/tests/e2e/golden/tile-mirror.sha256 +1 -0
  77. package/tests/e2e/golden/tile-repeat.raw +0 -0
  78. package/tests/e2e/golden/tile-repeat.sha256 +1 -0
  79. package/tests/e2e/gradient-animation.spec.ts +606 -0
  80. package/tests/e2e/memory-stability.spec.ts +396 -0
  81. package/tests/e2e/path-topology.spec.ts +674 -0
  82. package/tests/e2e/performance-profile.spec.ts +501 -0
  83. package/tests/e2e/screenshot.spec.ts +60 -0
  84. package/tests/e2e/test-harness.html +1005 -0
  85. package/tests/e2e/text-layout.spec.ts +451 -0
  86. package/tests/e2e/visual-demo.html +340 -0
  87. package/tests/e2e/visual-regression.spec.ts +335 -0
  88. package/tsconfig.json +12 -0
  89. 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
+ });