Files
NetMesh/components/terminal/useTerminalHibernateEffect.visibility.test.ts

129 lines
4.5 KiB
TypeScript
Raw Normal View History

import assert from "node:assert/strict";
import test from "node:test";
import { readFileSync } from "node:fs";
import React from "react";
import { act, create, type ReactTestRenderer } from "react-test-renderer";
import { removePaneVisible } from "./paneVisibilityStore.ts";
import { useTerminalHibernateEffect } from "./useTerminalHibernateEffect.ts";
test("hibernate effect keeps isVisibleRef current even when hibernate is disabled", () => {
const source = readFileSync(
new URL("./useTerminalHibernateEffect.ts", import.meta.url),
"utf8",
);
// Visibility sync must not early-return when hibernate is off; otherwise
// solo tab switches leave write/recovery paths on a stale isVisibleRef.
assert.doesNotMatch(
source,
/if \(!hibernateEnabled\) \{\s*clearHibernateTimer\(\);[\s\S]*return \(\) => \{\s*unsubscribeDisabled\(\);\s*\};\s*\}/,
);
assert.match(source, /isVisibleRef\.current = visible;/);
assert.match(
source,
/if \(hibernateEnabled\) \{\s*scheduleHibernate\(\);\s*\}/,
);
});
test("disabling hibernate wakes already soft-hidden or hibernated panes", () => {
const source = readFileSync(
new URL("./useTerminalHibernateEffect.ts", import.meta.url),
"utf8",
);
assert.match(
source,
/if \(!hibernateEnabled\) \{[\s\S]*?clearHibernateTimer\(\);[\s\S]*?if \(hibernatedRef\.current \|\| softHiddenRef\.current\) \{\s*tryWake\(\);\s*\}/,
);
// Must not early-return before visibility sync after the disable wake path.
const disableWakeIndex = source.indexOf("Turning hibernate off must wake");
const applyVisibilityCallIndex = source.indexOf(
"applyVisibility(resolveVisible());",
disableWakeIndex,
);
assert.ok(disableWakeIndex !== -1);
assert.ok(applyVisibilityCallIndex !== -1);
assert.ok(applyVisibilityCallIndex > disableWakeIndex);
});
test("soft-hidden wake keeps its marker until the runtime has resumed", () => {
const source = readFileSync(
new URL("./useTerminalHibernateEffect.ts", import.meta.url),
"utf8",
);
const softWakeBranch = source.match(
/if \(softHiddenRef\.current\) \{([\s\S]*?)return;\s*\}/,
)?.[1] ?? "";
assert.match(softWakeBranch, /onSoftHideWakeRef\.current\(\)/);
assert.doesNotMatch(softWakeBranch, /softHiddenRef\.current\s*=\s*false/);
});
test("hidden disconnected remote terminals release their retained runtime", async (t) => {
const sessionId = "disconnected-hidden-hibernate-test";
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
const previousActEnvironment = Object.getOwnPropertyDescriptor(
globalThis,
"IS_REACT_ACT_ENVIRONMENT",
);
Object.defineProperty(globalThis, "window", {
configurable: true,
value: {
setTimeout,
clearTimeout,
},
});
Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", {
configurable: true,
value: true,
});
t.after(() => {
removePaneVisible(sessionId);
if (previousWindow) Object.defineProperty(globalThis, "window", previousWindow);
else Reflect.deleteProperty(globalThis, "window");
if (previousActEnvironment) {
Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", previousActEnvironment);
} else {
Reflect.deleteProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT");
}
});
let hibernateCalls = 0;
let renderer: ReactTestRenderer | null = null;
const ref = <T>(current: T) => ({ current });
function Probe() {
useTerminalHibernateEffect({
sessionId,
isVisible: false,
isVisibleRef: ref(false),
getSessionConnectedRef: ref(() => false),
status: "disconnected",
isSearchOpen: false,
hibernateEnabled: true,
hibernateDelayMs: 5,
fileTransferActive: false,
hibernatedRef: ref(false),
softHiddenRef: ref(false),
hibernatePendingBufferRef: ref(""),
hibernateSnapshotRef: ref(""),
hibernateViewportSnapshotRef: ref(""),
hibernateScrollbackSnapshotRef: ref(""),
hibernateContextSnapshotRef: ref(""),
hibernateContextViewportSnapshotRef: ref(""),
hibernateContextScrollbackSnapshotRef: ref(""),
hibernateAlternateScreenRef: ref(false),
hasRuntimeRef: ref(true),
onHibernate: () => { hibernateCalls += 1; },
onSoftHideWake: () => {},
onWake: () => true,
});
return null;
}
await act(async () => { renderer = create(React.createElement(Probe)); });
await act(async () => { await new Promise((resolve) => setTimeout(resolve, 20)); });
assert.equal(hibernateCalls, 1);
await act(async () => renderer!.unmount());
});