@opentui/core 0.2.15 → 0.3.0

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/testing.js CHANGED
@@ -2,13 +2,8 @@
2
2
  import {
3
3
  ANSI,
4
4
  CliRenderer,
5
- TreeSitterClient,
6
- calculateRenderGeometry,
7
- resolveRenderLib
8
- } from "./index-3fq5hq97.js";
9
-
10
- // src/testing/test-renderer.ts
11
- import { Readable, Writable } from "stream";
5
+ TreeSitterClient
6
+ } from "./index-081xws23.js";
12
7
 
13
8
  // src/testing/mock-keys.ts
14
9
  import { Buffer as Buffer2 } from "buffer";
@@ -455,14 +450,14 @@ function createMockMouse(renderer) {
455
450
  };
456
451
  }
457
452
 
458
- // src/testing/test-renderer.ts
459
- var decoder = new TextDecoder;
453
+ // src/testing/test-streams.ts
454
+ import { Readable, Writable } from "stream";
460
455
 
461
456
  class TestWriteStream extends Writable {
462
457
  isTTY = true;
463
458
  columns;
464
459
  rows;
465
- constructor(columns, rows) {
460
+ constructor(columns = 80, rows = 24) {
466
461
  super();
467
462
  this.columns = columns;
468
463
  this.rows = rows;
@@ -474,6 +469,115 @@ class TestWriteStream extends Writable {
474
469
  return 24;
475
470
  }
476
471
  }
472
+ function createTestStdin() {
473
+ return new Readable({ read() {} });
474
+ }
475
+ function createTestStdout(columns = 80, rows = 24) {
476
+ return new TestWriteStream(columns, rows);
477
+ }
478
+
479
+ // src/testing/test-renderer.ts
480
+ var decoder = new TextDecoder;
481
+ var DEFAULT_MAX_PASSES = 20;
482
+ var DEFAULT_MAX_VISUAL_IDLE_FRAMES = 20;
483
+ var DEFAULT_QUIET_FRAMES = 1;
484
+ async function drainImmediateWork() {
485
+ await Promise.resolve();
486
+ await new Promise((resolve) => process.nextTick(resolve));
487
+ await Promise.resolve();
488
+ }
489
+ function normalizePositiveInteger(value, fallback) {
490
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
491
+ return fallback;
492
+ }
493
+ return Math.floor(value);
494
+ }
495
+ function createWaitError(renderer, message, frame) {
496
+ const stats = renderer.getStats();
497
+ const scheduler = renderer.getSchedulerState();
498
+ const details = [
499
+ message,
500
+ `frameId: ${renderer.frameId}`,
501
+ `nativeFrameCount: ${stats.nativeFrameCount}`,
502
+ `cellsUpdated: ${stats.cellsUpdated}`,
503
+ `isRunning: ${scheduler.isRunning}`,
504
+ `isRendering: ${scheduler.isRendering}`,
505
+ `hasScheduledRender: ${scheduler.hasScheduledRender}`
506
+ ];
507
+ if (frame !== undefined) {
508
+ details.push(`lastFrame:
509
+ ${frame}`);
510
+ }
511
+ return new Error(details.join(`
512
+ `));
513
+ }
514
+
515
+ class TestExternalOutputRecorder {
516
+ commits = [];
517
+ constructor(renderer) {
518
+ renderer.on("external_output" /* EXTERNAL_OUTPUT */, this.record);
519
+ renderer.once("destroy" /* DESTROY */, () => {
520
+ renderer.off("external_output" /* EXTERNAL_OUTPUT */, this.record);
521
+ });
522
+ }
523
+ record = (event) => {
524
+ const raw = decoder.decode(event.snapshot.getRealCharBytes(false));
525
+ const rows = Array.from({ length: event.snapshot.height }, (_, index) => raw.slice(index * event.snapshot.width, (index + 1) * event.snapshot.width).trimEnd());
526
+ this.commits.push({
527
+ text: rows.join(`
528
+ `),
529
+ rows,
530
+ width: event.snapshot.width,
531
+ height: event.snapshot.height,
532
+ rowColumns: event.rowColumns,
533
+ startOnNewLine: event.startOnNewLine,
534
+ trailingNewline: event.trailingNewline
535
+ });
536
+ };
537
+ take() {
538
+ const commits = this.commits;
539
+ this.commits = [];
540
+ return commits;
541
+ }
542
+ takeText() {
543
+ return this.take().flatMap((commit) => commit.rows).join(`
544
+ `);
545
+ }
546
+ clear() {
547
+ this.commits = [];
548
+ }
549
+ }
550
+ function waitForNextFrameOrIdle(renderer) {
551
+ const scheduler = renderer.getSchedulerState();
552
+ if (!scheduler.isRunning && !scheduler.isRendering && !scheduler.hasScheduledRender) {
553
+ return Promise.resolve(null);
554
+ }
555
+ return new Promise((resolve) => {
556
+ let settled = false;
557
+ const cleanup = () => {
558
+ renderer.off("frame" /* FRAME */, onFrame);
559
+ renderer.off("destroy" /* DESTROY */, onDestroy);
560
+ };
561
+ const finish = (event) => {
562
+ if (settled)
563
+ return;
564
+ settled = true;
565
+ cleanup();
566
+ resolve(event);
567
+ };
568
+ const onFrame = (event) => {
569
+ finish(event);
570
+ };
571
+ const onDestroy = () => {
572
+ finish(null);
573
+ };
574
+ renderer.on("frame" /* FRAME */, onFrame);
575
+ renderer.once("destroy" /* DESTROY */, onDestroy);
576
+ if (!scheduler.isRunning) {
577
+ renderer.idle().then(() => finish(null));
578
+ }
579
+ });
580
+ }
477
581
  async function createTestRenderer(options) {
478
582
  const useKittyKeyboard = options.kittyKeyboard ? { events: true } : options.useKittyKeyboard;
479
583
  const renderer = await setupTestRenderer({
@@ -484,24 +588,108 @@ async function createTestRenderer(options) {
484
588
  consoleMode: options.consoleMode ?? "disabled",
485
589
  externalOutputMode: options.externalOutputMode ?? "passthrough"
486
590
  });
591
+ const externalOutput = new TestExternalOutputRecorder(renderer);
487
592
  const mockInput = createMockKeys(renderer, {
488
593
  kittyKeyboard: options.kittyKeyboard,
489
594
  otherModifiersMode: options.otherModifiersMode
490
595
  });
491
596
  const mockMouse = createMockMouse(renderer);
492
597
  const renderOnce = async () => {
598
+ const feed = renderer._feed;
599
+ if (feed?.isBackpressured()) {
600
+ await feed.idle();
601
+ }
493
602
  await renderer.loop();
494
603
  };
604
+ const captureCharFrame = () => {
605
+ const currentBuffer = renderer.currentRenderBuffer;
606
+ const frameBytes = currentBuffer.getRealCharBytes(true);
607
+ return decoder.decode(frameBytes);
608
+ };
609
+ const waitForVisualIdle = async (waitOptions = {}) => {
610
+ const maxFrames = normalizePositiveInteger(waitOptions.maxFrames, DEFAULT_MAX_VISUAL_IDLE_FRAMES);
611
+ const quietFrames = normalizePositiveInteger(waitOptions.quietFrames, DEFAULT_QUIET_FRAMES);
612
+ let consecutiveQuietFrames = 0;
613
+ for (let frame = 0;frame < maxFrames; frame++) {
614
+ await drainImmediateWork();
615
+ const scheduler2 = renderer.getSchedulerState();
616
+ if (!scheduler2.isRunning && !scheduler2.isRendering && !scheduler2.hasScheduledRender) {
617
+ return;
618
+ }
619
+ const event = await waitForNextFrameOrIdle(renderer);
620
+ if (!event) {
621
+ return;
622
+ }
623
+ if (renderer.getNativeStats().cellsUpdated === 0) {
624
+ consecutiveQuietFrames++;
625
+ if (consecutiveQuietFrames >= quietFrames) {
626
+ return;
627
+ }
628
+ } else {
629
+ consecutiveQuietFrames = 0;
630
+ }
631
+ }
632
+ await drainImmediateWork();
633
+ const scheduler = renderer.getSchedulerState();
634
+ if (!scheduler.isRunning && !scheduler.isRendering && !scheduler.hasScheduledRender) {
635
+ return;
636
+ }
637
+ throw createWaitError(renderer, `Timed out waiting for visual idle after ${maxFrames} frames`);
638
+ };
639
+ const flush = async (flushOptions = {}) => {
640
+ await waitForVisualIdle({ maxFrames: normalizePositiveInteger(flushOptions.maxPasses, DEFAULT_MAX_PASSES) });
641
+ };
642
+ const waitFor = async (predicate, waitOptions = {}) => {
643
+ const maxPasses = normalizePositiveInteger(waitOptions.maxPasses, DEFAULT_MAX_PASSES);
644
+ for (let pass = 0;pass <= maxPasses; pass++) {
645
+ await drainImmediateWork();
646
+ if (await predicate()) {
647
+ return;
648
+ }
649
+ if (pass === maxPasses) {
650
+ break;
651
+ }
652
+ const scheduler = renderer.getSchedulerState();
653
+ if (!scheduler.isRunning && !scheduler.isRendering && !scheduler.hasScheduledRender) {
654
+ break;
655
+ }
656
+ await waitForNextFrameOrIdle(renderer);
657
+ }
658
+ throw createWaitError(renderer, `Timed out waiting for predicate after ${maxPasses} passes`);
659
+ };
660
+ const waitForFrame = async (predicate, waitOptions = {}) => {
661
+ const maxPasses = normalizePositiveInteger(waitOptions.maxPasses, DEFAULT_MAX_PASSES);
662
+ let frame = captureCharFrame();
663
+ for (let pass = 0;pass <= maxPasses; pass++) {
664
+ await drainImmediateWork();
665
+ frame = captureCharFrame();
666
+ if (await predicate(frame)) {
667
+ return frame;
668
+ }
669
+ if (pass === maxPasses) {
670
+ break;
671
+ }
672
+ const scheduler = renderer.getSchedulerState();
673
+ if (!scheduler.isRunning && !scheduler.isRendering && !scheduler.hasScheduledRender) {
674
+ break;
675
+ }
676
+ await waitForNextFrameOrIdle(renderer);
677
+ }
678
+ frame = captureCharFrame();
679
+ throw createWaitError(renderer, `Timed out waiting for frame predicate after ${maxPasses} passes`, frame);
680
+ };
495
681
  return {
496
682
  renderer,
497
683
  mockInput,
498
684
  mockMouse,
499
685
  renderOnce,
500
- captureCharFrame: () => {
501
- const currentBuffer = renderer.currentRenderBuffer;
502
- const frameBytes = currentBuffer.getRealCharBytes(true);
503
- return decoder.decode(frameBytes);
504
- },
686
+ flush,
687
+ waitFor,
688
+ waitForFrame,
689
+ waitForVisualIdle,
690
+ externalOutput,
691
+ getNativeStats: () => renderer.getNativeStats(),
692
+ captureCharFrame,
505
693
  captureSpans: () => {
506
694
  const currentBuffer = renderer.currentRenderBuffer;
507
695
  const lines = currentBuffer.getSpanLines();
@@ -519,31 +707,14 @@ async function createTestRenderer(options) {
519
707
  };
520
708
  }
521
709
  async function setupTestRenderer(config) {
522
- const stdin = config.stdin || new Readable({ read() {} });
710
+ const stdin = config.stdin || createTestStdin();
523
711
  const width = config.width || config.stdout?.columns || process.stdout.columns || 80;
524
712
  const height = config.height || config.stdout?.rows || process.stdout.rows || 24;
525
- const stdout = config.stdout || new TestWriteStream(width, height);
526
- const screenMode = config.screenMode ?? "alternate-screen";
527
- const footerHeight = config.footerHeight ?? 12;
528
- const geometry = calculateRenderGeometry(screenMode, width, height, footerHeight);
529
- const ziglib = resolveRenderLib();
530
- const rendererPtr = ziglib.createRenderer(geometry.renderWidth, geometry.renderHeight, {
531
- testing: true,
532
- remote: config.remote ?? false
713
+ const stdout = config.stdout || createTestStdout(width, height);
714
+ return new CliRenderer(stdin, stdout, width, height, {
715
+ ...config,
716
+ bufferedOutput: config.bufferedOutput ?? "memory"
533
717
  });
534
- if (!rendererPtr) {
535
- throw new Error("Failed to create test renderer");
536
- }
537
- if (config.useThread === undefined) {
538
- config.useThread = true;
539
- }
540
- if (process.platform === "linux") {
541
- config.useThread = false;
542
- }
543
- ziglib.setUseThread(rendererPtr, config.useThread);
544
- const renderer = new CliRenderer(ziglib, rendererPtr, stdin, stdout, width, height, config);
545
- process.off("SIGWINCH", renderer["sigwinchHandler"]);
546
- return renderer;
547
718
  }
548
719
  // src/testing/mock-tree-sitter-client.ts
549
720
  class MockTreeSitterClient extends TreeSitterClient {
@@ -615,7 +786,8 @@ function createTerminalCapabilities(overrides = {}) {
615
786
  osc52: false,
616
787
  notifications: false,
617
788
  explicit_cursor_positioning: false,
618
- in_tmux: false,
789
+ remote: false,
790
+ multiplexer: "none",
619
791
  ...overrides,
620
792
  terminal: {
621
793
  name: "",
@@ -644,6 +816,94 @@ function createSpy() {
644
816
  spy.reset = () => calls.length = 0;
645
817
  return spy;
646
818
  }
819
+ // src/testing/manual-clock.ts
820
+ function compareTimers(left, right) {
821
+ if (left.fireAt !== right.fireAt) {
822
+ return left.fireAt - right.fireAt;
823
+ }
824
+ return left.order - right.order;
825
+ }
826
+
827
+ class ManualClock {
828
+ time = 0;
829
+ nextId = 1;
830
+ nextOrder = 0;
831
+ timers = new Map;
832
+ now() {
833
+ return this.time;
834
+ }
835
+ setTime(time) {
836
+ const targetTime = Math.floor(time);
837
+ if (targetTime >= this.time) {
838
+ this.advance(targetTime - this.time);
839
+ return;
840
+ }
841
+ this.time = targetTime;
842
+ }
843
+ setTimeout(fn, delayMs) {
844
+ return this.schedule(fn, delayMs, false);
845
+ }
846
+ clearTimeout(handle) {
847
+ this.timers.delete(Number(handle));
848
+ }
849
+ setInterval(fn, delayMs) {
850
+ return this.schedule(fn, delayMs, true);
851
+ }
852
+ clearInterval(handle) {
853
+ this.clearTimeout(handle);
854
+ }
855
+ advance(delayMs) {
856
+ const targetTime = this.time + Math.max(0, Math.floor(delayMs));
857
+ while (true) {
858
+ const nextTimer = this.peekNextTimer();
859
+ if (!nextTimer || nextTimer.fireAt > targetTime) {
860
+ break;
861
+ }
862
+ this.timers.delete(nextTimer.id);
863
+ this.time = nextTimer.fireAt;
864
+ nextTimer.fn();
865
+ if (nextTimer.repeat && !this.timers.has(nextTimer.id)) {
866
+ this.timers.set(nextTimer.id, {
867
+ ...nextTimer,
868
+ fireAt: this.time + nextTimer.delayMs,
869
+ order: this.nextOrder++
870
+ });
871
+ }
872
+ }
873
+ this.time = targetTime;
874
+ }
875
+ runAll() {
876
+ while (true) {
877
+ const nextTimer = this.peekNextTimer();
878
+ if (!nextTimer) {
879
+ return;
880
+ }
881
+ this.advance(nextTimer.fireAt - this.time);
882
+ }
883
+ }
884
+ schedule(fn, delayMs, repeat) {
885
+ const id = this.nextId++;
886
+ const normalizedDelay = Math.max(0, Math.floor(delayMs));
887
+ this.timers.set(id, {
888
+ id,
889
+ fireAt: this.time + normalizedDelay,
890
+ order: this.nextOrder++,
891
+ delayMs: normalizedDelay,
892
+ repeat,
893
+ fn
894
+ });
895
+ return id;
896
+ }
897
+ peekNextTimer() {
898
+ let nextTimer = null;
899
+ for (const timer of this.timers.values()) {
900
+ if (!nextTimer || compareTimers(timer, nextTimer) < 0) {
901
+ nextTimer = timer;
902
+ }
903
+ }
904
+ return nextTimer;
905
+ }
906
+ }
647
907
  // src/testing/test-recorder.ts
648
908
  class TestRecorder {
649
909
  renderer;
@@ -651,10 +911,14 @@ class TestRecorder {
651
911
  recording = false;
652
912
  frameNumber = 0;
653
913
  startTime = 0;
654
- originalRenderNative;
655
914
  decoder = new TextDecoder;
656
915
  recordBuffers;
657
916
  now;
917
+ onFrame = () => {
918
+ if (!this.recording)
919
+ return;
920
+ this.captureFrame();
921
+ };
658
922
  constructor(renderer, options) {
659
923
  this.renderer = renderer;
660
924
  this.recordBuffers = options?.recordBuffers || {};
@@ -668,21 +932,14 @@ class TestRecorder {
668
932
  this.frames = [];
669
933
  this.frameNumber = 0;
670
934
  this.startTime = this.now();
671
- this.originalRenderNative = this.renderer["renderNative"].bind(this.renderer);
672
- this.renderer["renderNative"] = () => {
673
- this.originalRenderNative();
674
- this.captureFrame();
675
- };
935
+ this.renderer.on("frame" /* FRAME */, this.onFrame);
676
936
  }
677
937
  stop() {
678
938
  if (!this.recording) {
679
939
  return;
680
940
  }
681
941
  this.recording = false;
682
- if (this.originalRenderNative) {
683
- this.renderer["renderNative"] = this.originalRenderNative;
684
- this.originalRenderNative = undefined;
685
- }
942
+ this.renderer.off("frame" /* FRAME */, this.onFrame);
686
943
  }
687
944
  get recordedFrames() {
688
945
  return [...this.frames];
@@ -730,8 +987,9 @@ export {
730
987
  TestRecorder,
731
988
  MouseButtons,
732
989
  MockTreeSitterClient,
990
+ ManualClock,
733
991
  KeyCodes
734
992
  };
735
993
 
736
- //# debugId=E4ECC3F7B130440664756E2164756E21
994
+ //# debugId=0BAC903EE185632E64756E2164756E21
737
995
  //# sourceMappingURL=testing.js.map