[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
120
components/terminal/terminalTabVisibilityRecovery.test.ts
Normal file
120
components/terminal/terminalTabVisibilityRecovery.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
const source = readFileSync(new URL('./useTerminalEffects.ts', import.meta.url), 'utf8');
|
||||
|
||||
test('clears committed layout state when a terminal pane hides', () => {
|
||||
assert.match(source, /if \(isVisible\) return;[\s\S]*lastCommittedVisibleLayoutKeyRef\.current = null/);
|
||||
assert.match(source, /lastWebglRecoveryLayoutKeyRef\.current = null/);
|
||||
});
|
||||
|
||||
test('forces full recovery when a terminal pane becomes visible again', () => {
|
||||
assert.match(source, /const becameVisible = isVisible && !wasVisibleRef\.current/);
|
||||
assert.match(source, /recoverTerminalAfterBecomeVisible\(\)/);
|
||||
assert.match(source, /nudgeAlternateScreenRedraw\(term\)/);
|
||||
assert.match(source, /syncPtySizeAfterLayout/);
|
||||
});
|
||||
|
||||
test('layout recovery refit also syncs PTY size for full-screen TUIs', () => {
|
||||
assert.match(source, /runImmediateRefit\(\{ force: true, repeatOnNextFrame: false \}\);\s*finishLayoutRecoveryAfterFit\(\)/);
|
||||
assert.match(source, /finishLayoutRecoveryAfterFit/);
|
||||
});
|
||||
|
||||
test('tab-switch suppression does not consume the visible recovery pass', () => {
|
||||
assert.match(source, /const becameVisible = isVisible && !wasVisibleRef\.current/);
|
||||
assert.match(
|
||||
source,
|
||||
/if \(!isVisible\) \{\s*wasVisibleRef\.current = false;\s*syncOutputPressureVisibility\(\);\s*return;\s*\}[\s\S]*if \(splitResizeActive\) return;[\s\S]*wasVisibleRef\.current = true;[\s\S]*recoverTerminalAfterBecomeVisible\(\)/,
|
||||
);
|
||||
assert.doesNotMatch(source, /wasVisibleRef\.current = isVisible;\s*if \(!isVisible \|\| isResizing\) return/);
|
||||
});
|
||||
|
||||
test('immediate visibility recovery does not wait for the next animation frame', () => {
|
||||
assert.match(source, /safeFit\(\{ force, requireVisible: true, immediate: true \}\)/);
|
||||
assert.match(source, /safeFit\(\{ force: true, requireVisible: true, immediate: true \}\)/);
|
||||
});
|
||||
|
||||
test('visible tab recovery reuses a cached fit when the container size is unchanged', () => {
|
||||
assert.match(source, /const currentContainerSizeAlreadyFit = \(\) => \{/);
|
||||
assert.match(
|
||||
source,
|
||||
/if \(currentContainerSizeAlreadyFit\(\)\) \{\s*finishLayoutRecovery\(\);\s*flushPendingOutputScroll\(\);\s*commitVisibleLayout\(\);\s*return;\s*\}/,
|
||||
);
|
||||
});
|
||||
|
||||
test('short unchanged tab reveals skip the recovery work entirely', () => {
|
||||
const recoverIndex = source.indexOf('const recoverTerminalAfterBecomeVisible = () => {');
|
||||
const fastPathIndex = source.indexOf('getHiddenDurationMs() < CSS_ONLY_TAB_REVEAL_MAX_HIDDEN_MS', recoverIndex);
|
||||
const webglRecoveryIndex = source.indexOf('xtermRuntimeRef.current?.ensureWebglRenderer();', recoverIndex);
|
||||
|
||||
assert.ok(recoverIndex >= 0);
|
||||
assert.ok(fastPathIndex > recoverIndex);
|
||||
assert.ok(webglRecoveryIndex > fastPathIndex);
|
||||
assert.match(
|
||||
source.slice(fastPathIndex, webglRecoveryIndex),
|
||||
/currentContainerSizeAlreadyFit\(\)[\s\S]*lastWebglRecoveryLayoutKeyRef\.current = paneLayoutKey;[\s\S]*commitVisibleLayout\(\);[\s\S]*return;/,
|
||||
);
|
||||
});
|
||||
|
||||
test('short unchanged tab reveals still flush pending hidden-output scroll', () => {
|
||||
const recoverIndex = source.indexOf('const recoverTerminalAfterBecomeVisible = () => {');
|
||||
const fastPathIndex = source.indexOf('getHiddenDurationMs() < CSS_ONLY_TAB_REVEAL_MAX_HIDDEN_MS', recoverIndex);
|
||||
const webglRecoveryIndex = source.indexOf('xtermRuntimeRef.current?.ensureWebglRenderer();', recoverIndex);
|
||||
const delayedSkipIndex = source.indexOf('lastWebglRecoveryLayoutKeyRef.current === paneLayoutKey', webglRecoveryIndex);
|
||||
const delayedTimerIndex = source.indexOf('const timer = setTimeout', delayedSkipIndex);
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/const flushPendingOutputScroll = \(\) => \{[\s\S]*pendingOutputScrollRef\.current[\s\S]*scrollToBottom\(\)[\s\S]*pendingOutputScrollRef\.current = false;/,
|
||||
);
|
||||
assert.match(
|
||||
source.slice(fastPathIndex, webglRecoveryIndex),
|
||||
/flushPendingOutputScroll\(\);[\s\S]*commitVisibleLayout\(\);[\s\S]*return;/,
|
||||
);
|
||||
assert.match(
|
||||
source.slice(delayedSkipIndex, delayedTimerIndex),
|
||||
/flushPendingOutputScroll\(\);\s*return;/,
|
||||
);
|
||||
});
|
||||
|
||||
test('visible tab recovery drains hidden terminal writes before any fast-path return', () => {
|
||||
const recoverIndex = source.indexOf('const recoverTerminalAfterBecomeVisible = () => {');
|
||||
const fastPathIndex = source.indexOf('getHiddenDurationMs() < CSS_ONLY_TAB_REVEAL_MAX_HIDDEN_MS', recoverIndex);
|
||||
const flushCallIndex = source.indexOf('flushTerminalWritesAfterBecomeVisible();', recoverIndex);
|
||||
|
||||
assert.ok(recoverIndex >= 0);
|
||||
assert.ok(fastPathIndex > recoverIndex);
|
||||
assert.ok(flushCallIndex > recoverIndex && flushCallIndex < fastPathIndex);
|
||||
});
|
||||
|
||||
test('visible split-pane recovery no longer defers background panes', () => {
|
||||
const effectIndex = source.indexOf('const becameVisible = isVisible && !wasVisibleRef.current');
|
||||
const effectEnd = source.indexOf('}, [isVisible, paneLayoutKey, splitResizeActive]);', effectIndex);
|
||||
const effectSource = source.slice(effectIndex, effectEnd);
|
||||
|
||||
assert.ok(effectIndex >= 0);
|
||||
assert.ok(effectEnd > effectIndex);
|
||||
assert.match(
|
||||
source,
|
||||
/if \(becameVisible\) \{\s*recoverTerminalAfterBecomeVisible\(\);\s*return;\s*\}/,
|
||||
);
|
||||
assert.doesNotMatch(effectSource, /\binWorkspace\b/);
|
||||
assert.doesNotMatch(effectSource, /\bisFocusMode\b/);
|
||||
assert.doesNotMatch(effectSource, /\bisFocused\b/);
|
||||
assert.doesNotMatch(source, /shouldRefitImmediatelyOnShow/);
|
||||
assert.doesNotMatch(source, /shouldRecoverWebglOnShow/);
|
||||
assert.doesNotMatch(source, /const runDeferred = \(\) => \{/);
|
||||
assert.doesNotMatch(source, /scheduleLayoutRecoveryRefit\(\[120, 350\]\)/);
|
||||
});
|
||||
|
||||
test('immediate tab recovery marks webgl recovery to skip the delayed duplicate pass', () => {
|
||||
assert.match(
|
||||
source,
|
||||
/const recoverTerminalAfterBecomeVisible = \(\) => \{[\s\S]*xtermRuntimeRef\.current\?\.clearTextureAtlas\(\);\s*lastWebglRecoveryLayoutKeyRef\.current = paneLayoutKey;/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/lastWebglRecoveryLayoutKeyRef\.current === paneLayoutKey\s*&& hiddenMs < CSS_ONLY_TAB_REVEAL_MAX_HIDDEN_MS/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user