@vybestack/llxprt-ui 0.7.0-nightly.251214.7c8736a50 → 0.7.0-nightly.251215.66bf0bc39

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/logos/.gitkeep ADDED
@@ -0,0 +1 @@
1
+
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vybestack/llxprt-ui",
3
- "version": "0.7.0-nightly.251214.7c8736a50",
3
+ "version": "0.7.0-nightly.251215.66bf0bc39",
4
4
  "description": "Experimental terminal UI for llxprt-code",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -35,13 +35,13 @@
35
35
  "bun": ">=1.2.0"
36
36
  },
37
37
  "dependencies": {
38
- "@vybestack/opentui-core": "^0.1.61",
39
- "@vybestack/opentui-react": "^0.1.61",
38
+ "@vybestack/opentui-core": "^0.1.62",
39
+ "@vybestack/opentui-react": "^0.1.62",
40
40
  "clipboardy": "^4.0.0",
41
41
  "react": "^19.2.1"
42
42
  },
43
43
  "peerDependencies": {
44
- "@vybestack/llxprt-code-core": "^0.7.0-nightly.251214.7c8736a50"
44
+ "@vybestack/llxprt-code-core": "^0.7.0-nightly.251215.66bf0bc39"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@eslint/js": "^9.39.1",
@@ -0,0 +1,618 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { createTestRenderer } from '@vybestack/opentui-core/testing';
4
+ import {
5
+ BoxRenderable,
6
+ ImageRenderable,
7
+ type CellMetrics,
8
+ } from '@vybestack/opentui-core';
9
+ import { TextRenderable } from '@vybestack/opentui-core';
10
+
11
+ interface SharpInstance {
12
+ png: () => SharpInstance;
13
+ ensureAlpha: () => SharpInstance;
14
+ raw: () => SharpInstance;
15
+ toBuffer: () => Promise<Buffer>;
16
+ metadata: () => Promise<{ width?: number; height?: number }>;
17
+ }
18
+
19
+ type SharpFactory = (input: unknown) => SharpInstance;
20
+
21
+ async function getSharp(): Promise<SharpFactory> {
22
+ const sharpModule: unknown = await import('sharp');
23
+ const moduleCandidate = sharpModule as { default?: unknown };
24
+ const sharpExport =
25
+ typeof moduleCandidate.default === 'function'
26
+ ? moduleCandidate.default
27
+ : sharpModule;
28
+
29
+ if (typeof sharpExport !== 'function') {
30
+ throw new Error('sharp import did not resolve to a callable function');
31
+ }
32
+
33
+ return sharpExport as SharpFactory;
34
+ }
35
+
36
+ function parseFirstItermImagePayload(writes: string[]): {
37
+ row1: number;
38
+ col1: number;
39
+ width: number;
40
+ height: number;
41
+ widthUnit: 'px' | 'cells';
42
+ heightUnit: 'px' | 'cells';
43
+ preserveAspectRatio: number;
44
+ png: Buffer;
45
+ } {
46
+ const output = writes.join('');
47
+
48
+ const oscPrefix = '\u001b]1337;File=inline=1;';
49
+ const oscStart = output.indexOf(oscPrefix);
50
+ if (oscStart === -1) {
51
+ throw new Error('No iTerm2 inline image sequence found in renderer output');
52
+ }
53
+
54
+ const belIndex = output.indexOf('\u0007', oscStart);
55
+ if (belIndex === -1) {
56
+ throw new Error(
57
+ 'Found iTerm2 image prefix but did not find terminating BEL',
58
+ );
59
+ }
60
+
61
+ const moveStart = output.lastIndexOf('\u001b[', oscStart);
62
+ const moveEnd = output.lastIndexOf('H', oscStart);
63
+ if (moveStart === -1 || moveEnd === -1 || moveStart >= moveEnd) {
64
+ throw new Error(
65
+ 'Failed to locate cursor movement sequence preceding iTerm2 payload',
66
+ );
67
+ }
68
+
69
+ const movePayload = output.slice(moveStart + 2, moveEnd);
70
+ const [rowStr, colStr] = movePayload.split(';');
71
+ const row1 = Number(rowStr);
72
+ const col1 = Number(colStr);
73
+
74
+ const oscPayload = output.slice(oscStart + 2, belIndex);
75
+ const oscMatch =
76
+ /^1337;File=inline=1;width=(\d+)(px)?;height=(\d+)(px)?;preserveAspectRatio=(\d+):([A-Za-z0-9+/=]+)$/.exec(
77
+ oscPayload,
78
+ );
79
+ if (!oscMatch) {
80
+ throw new Error('Failed to parse iTerm2 inline image payload');
81
+ }
82
+
83
+ const width = Number(oscMatch[1]);
84
+ const widthUnit: 'px' | 'cells' = oscMatch[2] === 'px' ? 'px' : 'cells';
85
+ const height = Number(oscMatch[3]);
86
+ const heightUnit: 'px' | 'cells' = oscMatch[4] === 'px' ? 'px' : 'cells';
87
+ const preserveAspectRatio = Number(oscMatch[5]);
88
+ const png = Buffer.from(oscMatch[6], 'base64');
89
+
90
+ if (
91
+ !Number.isFinite(row1) ||
92
+ !Number.isFinite(col1) ||
93
+ !Number.isFinite(width) ||
94
+ !Number.isFinite(height)
95
+ ) {
96
+ throw new Error('Failed to parse iTerm2 image move/size from output');
97
+ }
98
+
99
+ if (!Number.isFinite(preserveAspectRatio)) {
100
+ throw new Error(
101
+ 'Failed to parse preserveAspectRatio from iTerm2 image payload',
102
+ );
103
+ }
104
+
105
+ return {
106
+ row1,
107
+ col1,
108
+ width,
109
+ height,
110
+ widthUnit,
111
+ heightUnit,
112
+ preserveAspectRatio,
113
+ png,
114
+ };
115
+ }
116
+
117
+ async function assertContainPaddingPreservesAlpha(): Promise<void> {
118
+ const { renderer, renderOnce } = await createTestRenderer({
119
+ width: 80,
120
+ height: 24,
121
+ });
122
+
123
+ const writes: string[] = [];
124
+ const testHarness = renderer as unknown as {
125
+ _graphicsSupport: { protocol: 'iterm2' };
126
+ getCellMetrics: () => CellMetrics | null;
127
+ writeOut: (chunk: string) => boolean;
128
+ };
129
+ testHarness._graphicsSupport = { protocol: 'iterm2' };
130
+ testHarness.getCellMetrics = () => ({ pxPerCellX: 10, pxPerCellY: 20 });
131
+ testHarness.writeOut = (chunk: string) => {
132
+ writes.push(chunk);
133
+ return true;
134
+ };
135
+
136
+ const sharp = await getSharp();
137
+ const input = await sharp({
138
+ create: {
139
+ width: 100,
140
+ height: 50,
141
+ channels: 4,
142
+ background: { r: 255, g: 0, b: 0, alpha: 1 },
143
+ },
144
+ })
145
+ .png()
146
+ .toBuffer();
147
+
148
+ const bg = '#fafafa';
149
+ const image = new ImageRenderable(renderer, {
150
+ id: 'padding-bg-test',
151
+ src: input,
152
+ fit: 'contain',
153
+ pixelWidth: 50,
154
+ pixelHeight: 50,
155
+ backgroundColor: bg,
156
+ alignSelf: 'flex-start',
157
+ });
158
+
159
+ renderer.root.add(image);
160
+ await renderOnce();
161
+ renderer.destroy();
162
+
163
+ const { png, width, height, widthUnit, heightUnit, preserveAspectRatio } =
164
+ parseFirstItermImagePayload(writes);
165
+ if (widthUnit !== 'cells' || heightUnit !== 'cells') {
166
+ throw new Error(
167
+ `Expected iTerm2 image sizing in cells, got widthUnit=${widthUnit} heightUnit=${heightUnit}`,
168
+ );
169
+ }
170
+ if (preserveAspectRatio !== 0) {
171
+ throw new Error(
172
+ `Expected iTerm2 preserveAspectRatio=0 (renderer pre-sizes images), got ${preserveAspectRatio}`,
173
+ );
174
+ }
175
+
176
+ const expectedWidthPx = Math.max(1, Math.floor(width * 10));
177
+ const expectedHeightPx = Math.max(1, Math.floor(height * 20));
178
+
179
+ const meta = await sharp(png).metadata();
180
+ const widthPx = meta.width;
181
+ const heightPx = meta.height;
182
+ if (widthPx == null || heightPx == null) {
183
+ throw new Error('Failed to read PNG metadata from iTerm2 payload');
184
+ }
185
+ if (widthPx !== expectedWidthPx || heightPx !== expectedHeightPx) {
186
+ throw new Error(
187
+ `Unexpected resized PNG dimensions: expected ${expectedWidthPx}x${expectedHeightPx}, got ${widthPx}x${heightPx}`,
188
+ );
189
+ }
190
+
191
+ const raw = await sharp(png).ensureAlpha().raw().toBuffer();
192
+ const stride = widthPx * 4;
193
+ const corners = [
194
+ { x: 0, y: 0 },
195
+ { x: widthPx - 1, y: 0 },
196
+ { x: 0, y: heightPx - 1 },
197
+ { x: widthPx - 1, y: heightPx - 1 },
198
+ ];
199
+
200
+ for (const { x, y } of corners) {
201
+ const idx = y * stride + x * 4;
202
+ const a = raw[idx + 3];
203
+ if (a !== 0) {
204
+ throw new Error(
205
+ `Contain padding corner alpha mismatch at (${x},${y}): expected alpha=0, got alpha=${a}`,
206
+ );
207
+ }
208
+ }
209
+
210
+ // Sanity check: the center pixel should still be opaque from the source content.
211
+ const centerIdx =
212
+ Math.floor(heightPx / 2) * stride + Math.floor(widthPx / 2) * 4;
213
+ const centerAlpha = raw[centerIdx + 3];
214
+ if (centerAlpha === 0) {
215
+ throw new Error(
216
+ `Expected contain-resized PNG center pixel to be opaque, got alpha=${centerAlpha}`,
217
+ );
218
+ }
219
+ }
220
+
221
+ async function assertHeaderImagePayloadMatchesLayout(): Promise<void> {
222
+ const { renderer, renderOnce } = await createTestRenderer({
223
+ width: 80,
224
+ height: 24,
225
+ });
226
+
227
+ const writes: string[] = [];
228
+ const testHarness = renderer as unknown as {
229
+ _graphicsSupport: { protocol: 'iterm2' };
230
+ getCellMetrics: () => CellMetrics | null;
231
+ writeOut: (chunk: string) => boolean;
232
+ };
233
+ testHarness._graphicsSupport = { protocol: 'iterm2' };
234
+ testHarness.getCellMetrics = () => ({ pxPerCellX: 10, pxPerCellY: 20 });
235
+ testHarness.writeOut = (chunk: string) => {
236
+ writes.push(chunk);
237
+ return true;
238
+ };
239
+
240
+ const bg = '#101010';
241
+ const header = new BoxRenderable(renderer, {
242
+ id: 'header',
243
+ flexDirection: 'row',
244
+ alignItems: 'center',
245
+ justifyContent: 'flex-start',
246
+ border: true,
247
+ height: 3,
248
+ minHeight: 3,
249
+ maxHeight: 3,
250
+ backgroundColor: bg,
251
+ });
252
+
253
+ const logoPath = fileURLToPath(new URL('../llxprt.png', import.meta.url));
254
+ const logo = readFileSync(logoPath);
255
+ const aspectRatio = 415 / 260;
256
+
257
+ const image = new ImageRenderable(renderer, {
258
+ id: 'logo',
259
+ src: logo,
260
+ height: 1,
261
+ aspectRatio,
262
+ fit: 'contain',
263
+ backgroundColor: bg,
264
+ });
265
+ header.add(image);
266
+ renderer.root.add(header);
267
+
268
+ await renderOnce();
269
+ renderer.destroy();
270
+
271
+ const {
272
+ row1,
273
+ col1,
274
+ width,
275
+ height,
276
+ widthUnit,
277
+ heightUnit,
278
+ preserveAspectRatio,
279
+ png,
280
+ } = parseFirstItermImagePayload(writes);
281
+ if (widthUnit !== 'cells' || heightUnit !== 'cells') {
282
+ throw new Error(
283
+ `Expected iTerm2 image sizing in cells, got widthUnit=${widthUnit} heightUnit=${heightUnit}`,
284
+ );
285
+ }
286
+ if (preserveAspectRatio !== 0) {
287
+ throw new Error(
288
+ `Expected iTerm2 preserveAspectRatio=0 (renderer pre-sizes images), got ${preserveAspectRatio}`,
289
+ );
290
+ }
291
+
292
+ if (row1 !== image.y + 1 || col1 !== image.x + 1) {
293
+ throw new Error(
294
+ `Expected iTerm2 image cursor to match layout. got row=${row1} col=${col1}, expected row=${image.y + 1} col=${image.x + 1}`,
295
+ );
296
+ }
297
+
298
+ if (width !== image.width || height !== image.height) {
299
+ throw new Error(
300
+ `Expected iTerm2 image cell size to match layout. got ${width}x${height}, expected ${image.width}x${image.height}`,
301
+ );
302
+ }
303
+
304
+ const sharp = await getSharp();
305
+ const meta = await sharp(png).metadata();
306
+ if (meta.width == null || meta.height == null) {
307
+ throw new Error('Failed to read PNG metadata from iTerm2 payload');
308
+ }
309
+
310
+ const expectedWidthPx = width * 10;
311
+ const expectedHeightPx = height * 20;
312
+ if (meta.width !== expectedWidthPx || meta.height !== expectedHeightPx) {
313
+ throw new Error(
314
+ `Unexpected resized PNG dimensions: expected ${expectedWidthPx}x${expectedHeightPx}, got ${meta.width}x${meta.height}`,
315
+ );
316
+ }
317
+ }
318
+
319
+ async function assertMovedImageClearsPreviousInlineArea(): Promise<void> {
320
+ const { renderer, renderOnce } = await createTestRenderer({
321
+ width: 40,
322
+ height: 12,
323
+ });
324
+
325
+ const writes: string[] = [];
326
+ const testHarness = renderer as unknown as {
327
+ _graphicsSupport: { protocol: 'iterm2' };
328
+ getCellMetrics: () => CellMetrics | null;
329
+ writeOut: (chunk: string) => boolean;
330
+ };
331
+ testHarness._graphicsSupport = { protocol: 'iterm2' };
332
+ testHarness.writeOut = (chunk: string) => {
333
+ writes.push(chunk);
334
+ return true;
335
+ };
336
+
337
+ // Fixed-size container so justifyContent:center shifts the image when its measured width changes.
338
+ const container = new BoxRenderable(renderer, {
339
+ id: 'container',
340
+ flexDirection: 'row',
341
+ alignItems: 'center',
342
+ justifyContent: 'center',
343
+ height: 6,
344
+ width: 20,
345
+ });
346
+
347
+ const bg = '#fafafa';
348
+ const image = new ImageRenderable(renderer, {
349
+ id: 'move-test-image',
350
+ src: Buffer.from(
351
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P8z/D/PwAHggJ/Pq2uGAAAAABJRU5ErkJggg==',
352
+ 'base64',
353
+ ),
354
+ pixelWidth: 40,
355
+ pixelHeight: 40,
356
+ backgroundColor: bg,
357
+ alignSelf: 'flex-start',
358
+ });
359
+
360
+ container.add(image);
361
+ renderer.root.add(container);
362
+
363
+ // First render: no metrics, ImageRenderable falls back to 9x20 => width=5 cells for 40px.
364
+ testHarness.getCellMetrics = () => null;
365
+ await renderOnce();
366
+ const first = parseFirstItermImagePayload(writes);
367
+ const firstCursor = { row1: first.row1, col1: first.col1 };
368
+ writes.length = 0;
369
+
370
+ // Second render: metrics change to 20x40 => width=2 cells for 40px, shifting X due to centering.
371
+ testHarness.getCellMetrics = () => ({ pxPerCellX: 20, pxPerCellY: 40 });
372
+ renderer.emit('pixelResolution', { width: 800, height: 480 });
373
+ await renderOnce();
374
+ renderer.destroy();
375
+
376
+ const second = parseFirstItermImagePayload(writes);
377
+ const secondCursor = { row1: second.row1, col1: second.col1 };
378
+
379
+ if (
380
+ firstCursor.row1 === secondCursor.row1 &&
381
+ firstCursor.col1 === secondCursor.col1
382
+ ) {
383
+ throw new Error(
384
+ 'Expected image cursor position to change between renders, but it did not',
385
+ );
386
+ }
387
+
388
+ const output = writes.join('');
389
+ const clearAtOld = `\u001b[${firstCursor.row1};${firstCursor.col1}H\u001b[0m`;
390
+ const clearIndex = output.indexOf(clearAtOld);
391
+ if (clearIndex === -1) {
392
+ throw new Error(
393
+ 'Expected iTerm2 inline clear sequence at previous cursor position, but none was found',
394
+ );
395
+ }
396
+
397
+ const newMove = `\u001b[${secondCursor.row1};${secondCursor.col1}H\u001b]1337;File=inline=1;`;
398
+ const newIndex = output.indexOf(newMove);
399
+ if (newIndex === -1) {
400
+ throw new Error(
401
+ 'Expected iTerm2 inline image sequence for second render, but none was found',
402
+ );
403
+ }
404
+
405
+ if (clearIndex > newIndex) {
406
+ throw new Error(
407
+ 'Expected inline clear to occur before the moved image is drawn',
408
+ );
409
+ }
410
+ }
411
+
412
+ async function assertResizedImageClearsPreviousInlineArea(): Promise<void> {
413
+ const { renderer, renderOnce } = await createTestRenderer({
414
+ width: 20,
415
+ height: 10,
416
+ });
417
+
418
+ const writes: string[] = [];
419
+ const testHarness = renderer as unknown as {
420
+ _graphicsSupport: { protocol: 'iterm2' };
421
+ getCellMetrics: () => CellMetrics | null;
422
+ writeOut: (chunk: string) => boolean;
423
+ };
424
+ testHarness._graphicsSupport = { protocol: 'iterm2' };
425
+ testHarness.getCellMetrics = () => ({ pxPerCellX: 10, pxPerCellY: 20 });
426
+ testHarness.writeOut = (chunk: string) => {
427
+ writes.push(chunk);
428
+ return true;
429
+ };
430
+
431
+ const bg = '#fafafa';
432
+ const image = new ImageRenderable(renderer, {
433
+ id: 'resize-test-image',
434
+ src: Buffer.from(
435
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P8z/D/PwAHggJ/Pq2uGAAAAABJRU5ErkJggg==',
436
+ 'base64',
437
+ ),
438
+ width: 5,
439
+ height: 2,
440
+ backgroundColor: bg,
441
+ alignSelf: 'flex-start',
442
+ });
443
+
444
+ renderer.root.add(image);
445
+
446
+ await renderOnce();
447
+ const first = parseFirstItermImagePayload(writes);
448
+ const firstCursor = { row1: first.row1, col1: first.col1 };
449
+ writes.length = 0;
450
+
451
+ // Redraw at the same cell position, but with a different pixel backing size
452
+ // (simulating a pixel-resolution/cell-metrics change).
453
+ testHarness.getCellMetrics = () => ({ pxPerCellX: 11, pxPerCellY: 21 });
454
+ renderer.emit('pixelResolution', { width: 220, height: 210 });
455
+
456
+ await renderOnce();
457
+ renderer.destroy();
458
+
459
+ const second = parseFirstItermImagePayload(writes);
460
+ const secondCursor = { row1: second.row1, col1: second.col1 };
461
+
462
+ if (
463
+ firstCursor.row1 !== secondCursor.row1 ||
464
+ firstCursor.col1 !== secondCursor.col1
465
+ ) {
466
+ throw new Error(
467
+ 'Expected image cursor position to remain the same between resize renders, but it changed',
468
+ );
469
+ }
470
+
471
+ const output = writes.join('');
472
+ const clearAt = `\u001b[${firstCursor.row1};${firstCursor.col1}H\u001b[0m`;
473
+ const clearIndex = output.indexOf(clearAt);
474
+ if (clearIndex === -1) {
475
+ throw new Error(
476
+ 'Expected iTerm2 inline clear sequence before resized redraw, but none was found',
477
+ );
478
+ }
479
+
480
+ const newMove = `\u001b[${secondCursor.row1};${secondCursor.col1}H\u001b]1337;File=inline=1;`;
481
+ const newIndex = output.indexOf(newMove);
482
+ if (newIndex === -1) {
483
+ throw new Error(
484
+ 'Expected iTerm2 inline image sequence for resized render, but none was found',
485
+ );
486
+ }
487
+
488
+ if (clearIndex > newIndex) {
489
+ throw new Error(
490
+ 'Expected inline clear to occur before the resized image is drawn',
491
+ );
492
+ }
493
+ }
494
+
495
+ async function assertHeaderLayoutBehavesLikeImg(): Promise<void> {
496
+ const { renderer, renderOnce } = await createTestRenderer({
497
+ width: 80,
498
+ height: 24,
499
+ });
500
+
501
+ const testHarness = renderer as unknown as {
502
+ getCellMetrics: () => CellMetrics | null;
503
+ _graphicsSupport: { protocol: 'none' };
504
+ };
505
+ testHarness.getCellMetrics = () => ({ pxPerCellX: 10, pxPerCellY: 20 });
506
+ testHarness._graphicsSupport = { protocol: 'none' };
507
+
508
+ const bg = '#101010';
509
+ const header = new BoxRenderable(renderer, {
510
+ id: 'header',
511
+ flexDirection: 'row',
512
+ alignItems: 'center',
513
+ justifyContent: 'flex-start',
514
+ border: true,
515
+ height: 3,
516
+ minHeight: 3,
517
+ maxHeight: 3,
518
+ backgroundColor: bg,
519
+ });
520
+
521
+ const aspectRatio = 415 / 260;
522
+ const image = new ImageRenderable(renderer, {
523
+ id: 'logo',
524
+ src: Buffer.from(
525
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P8z/D/PwAHggJ/Pq2uGAAAAABJRU5ErkJggg==',
526
+ 'base64',
527
+ ),
528
+ height: 1,
529
+ aspectRatio,
530
+ backgroundColor: bg,
531
+ });
532
+
533
+ const text = new TextRenderable(renderer, {
534
+ id: 'title',
535
+ content: "LLxprt Code - I'm here to help",
536
+ marginLeft: 1,
537
+ });
538
+
539
+ header.add(image);
540
+ header.add(text);
541
+ renderer.root.add(header);
542
+
543
+ await renderOnce();
544
+ renderer.destroy();
545
+
546
+ if (header.height !== 3) {
547
+ throw new Error(`Expected header height=3, got ${header.height}`);
548
+ }
549
+
550
+ if (image.height !== 1) {
551
+ throw new Error(`Expected image height=1 cell, got ${image.height}`);
552
+ }
553
+
554
+ if (image.width !== 4) {
555
+ throw new Error(`Expected image width=4 cells, got ${image.width}`);
556
+ }
557
+
558
+ if (text.x < image.x + image.width) {
559
+ throw new Error(
560
+ `Expected text to start after image. image ends at x=${image.x + image.width}, text starts at x=${text.x}`,
561
+ );
562
+ }
563
+
564
+ if (text.y !== image.y) {
565
+ throw new Error(
566
+ `Expected image and text to share y (vertical centering). image.y=${image.y}, text.y=${text.y}`,
567
+ );
568
+ }
569
+ }
570
+
571
+ async function main(): Promise<void> {
572
+ const failures: { name: string; error: string }[] = [];
573
+
574
+ const tests: { name: string; run: () => Promise<void> }[] = [
575
+ {
576
+ name: 'contain-padding-alpha',
577
+ run: assertContainPaddingPreservesAlpha,
578
+ },
579
+ {
580
+ name: 'header-image-payload-matches-layout',
581
+ run: assertHeaderImagePayloadMatchesLayout,
582
+ },
583
+ {
584
+ name: 'move-clears-previous-inline',
585
+ run: assertMovedImageClearsPreviousInlineArea,
586
+ },
587
+ {
588
+ name: 'redraw-clears-previous-inline',
589
+ run: assertResizedImageClearsPreviousInlineArea,
590
+ },
591
+ {
592
+ name: 'header-layout-behaves-like-img',
593
+ run: assertHeaderLayoutBehavesLikeImg,
594
+ },
595
+ ];
596
+
597
+ for (const t of tests) {
598
+ try {
599
+ await t.run();
600
+ } catch (err) {
601
+ failures.push({
602
+ name: t.name,
603
+ error: err instanceof Error ? err.message : String(err),
604
+ });
605
+ }
606
+ }
607
+
608
+ if (failures.length > 0) {
609
+ process.stderr.write(
610
+ `${JSON.stringify({ ok: false, failures }, null, 2)}\n`,
611
+ );
612
+ process.exit(1);
613
+ }
614
+
615
+ process.stdout.write(`${JSON.stringify({ ok: true })}\n`);
616
+ }
617
+
618
+ await main();