Files
NetMesh/components/terminal/terminalTabVisibilityRecovery.test.ts

121 lines
5.9 KiB
TypeScript
Raw Normal View History

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/,
);
});