Extract default familiar window lifecycle seam
This commit is contained in:
parent
5caf9b1c39
commit
430ccdb661
6 changed files with 256 additions and 60 deletions
|
|
@ -41,6 +41,7 @@ const behaviorTests = [
|
|||
".test-dist/tests/settings-tts-group-split.test.js",
|
||||
".test-dist/tests/default-familiar-external-show.test.js",
|
||||
".test-dist/tests/default-familiar-controller-motion.test.js",
|
||||
".test-dist/tests/default-familiar-controller-window.test.js",
|
||||
".test-dist/tests/app-state-preferences.test.js",
|
||||
".test-dist/tests/app-state-analytics.test.js",
|
||||
".test-dist/tests/app-state-familiar-records.test.js",
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@ const leaseManagerSource = readFileSync(join(appDir, "src", "lease-manager.ts"),
|
|||
const defaultPetControllerSource = readFileSync(join(appDir, "src", "default-familiar-controller.ts"), "utf8");
|
||||
const defaultPetControllerDisplaySource = readFileSync(join(appDir, "src", "default-familiar-controller-display.ts"), "utf8");
|
||||
const defaultPetControllerMotionSource = readFileSync(join(appDir, "src", "default-familiar-controller-motion.ts"), "utf8");
|
||||
const defaultPetControllerWindowSource = readFileSync(join(appDir, "src", "default-familiar-controller-window.ts"), "utf8");
|
||||
const agentPetControllerSourceForLogging = readFileSync(join(appDir, "src", "agent-familiar-controller.ts"), "utf8");
|
||||
const mappingDoc = readFileSync(join(repoRoot, "docs", "mapping.md"), "utf8");
|
||||
const controlCenterInternalUiSource = [
|
||||
|
|
@ -363,15 +364,18 @@ assert.match(petPreloadSource, /familiaros:familiar-hit-test/, "familiar preload
|
|||
assert.match(petPreloadSource, /familiaros:familiar-ready/, "familiar preload must report readiness after installing mouse handlers.");
|
||||
assert.match(petPreloadSource, /familiaros:familiar-drag-start/, "familiar preload must start controlled familiar dragging from the sprite.");
|
||||
assert.match(petPreloadSource, /familiaros:familiar-open-prompt/, "familiar preload must request the floating prompt window on double-click.");
|
||||
assert.match(defaultPetControllerSource, /powerMonitor\.on\("resume", recoverDefaultPetWindowAfterResume\)/, "default familiar must recover mouse interop after Windows sleep or resume.");
|
||||
assert.match(defaultPetControllerSource, /recoverDefaultPetMouseInterop\("display-change"\)/, "default familiar must recover mouse interop after monitor topology changes.");
|
||||
assert.match(defaultPetControllerSource, /from "\.\/default-familiar-controller-display(?:\.js)?"/, "default familiar controller must compose the extracted display seam.");
|
||||
assert.match(defaultPetControllerSource, /from "\.\/default-familiar-controller-motion(?:\.js)?"/, "default familiar controller must compose the extracted motion seam.");
|
||||
assert.match(defaultPetControllerSource, /from "\.\/default-familiar-controller-window(?:\.js)?"/, "default familiar controller must compose the extracted window seam.");
|
||||
assert.match(defaultPetControllerDisplaySource, /export function setDefaultPetTransientDisplay/, "default familiar display seam must export transient display handling.");
|
||||
assert.match(defaultPetControllerDisplaySource, /export function setDefaultPetStatusBadge/, "default familiar display seam must export status badge handling.");
|
||||
assert.match(defaultPetControllerDisplaySource, /export function getDefaultPetDismissToken/, "default familiar display seam must export dismiss token shaping.");
|
||||
assert.match(defaultPetControllerMotionSource, /export async function moveDefaultPetWindowBy/, "default familiar motion seam must export bounded plugin window movement.");
|
||||
assert.match(defaultPetControllerMotionSource, /export function getPetMovementBlockedReason/, "default familiar motion seam must export movement block-state resolution.");
|
||||
assert.match(defaultPetControllerWindowSource, /export function loadDefaultPetWindowContent/, "default familiar window seam must export content reload handling.");
|
||||
assert.match(defaultPetControllerWindowSource, /export function installDefaultPetWindowDisplayHandlers/, "default familiar window seam must export display and resume handler installation.");
|
||||
assert.match(defaultPetControllerWindowSource, /powerMonitor\.on\("resume", recoverAfterResume\)/, "default familiar must recover mouse interop after Windows sleep or resume.");
|
||||
assert.match(defaultPetControllerWindowSource, /recoverMouseInterop\("display-change"\)/, "default familiar must recover mouse interop after monitor topology changes.");
|
||||
assert.match(controlCenterInternalUiSource, /recoverDefaultPetMouseInterop\("default-familiar-changed"\)/, "changing default familiar must recover mouse interop for dragging without app restart.");
|
||||
assert.match(petWindowPluginMenuSource, /function installPetContextMenu/, "familiar windows must install a native right-click context menu.");
|
||||
assert.match(petWindowPluginMenuSource, /webContents\.on\("context-menu"/, "familiar context menu must be handled in the Electron main process.");
|
||||
|
|
|
|||
|
|
@ -246,9 +246,10 @@ plugin-service-local-support.ts → plugin-local-loader.ts validates selected fo
|
|||
- `familiar-window-renderer-bridge.ts`: Extracted renderer IPC helpers for bubble layout updates, reaction state, sprite overrides, audio playback, and TTS
|
||||
- `familiar-window-render.ts`: Shared familiar-window CSS/sprite render shell plus stage markup and bubble seam re-exports
|
||||
- `familiar-window-render-bubbles.ts`: Extracted familiar-window bubble/plugin HUD markup, status badge, and HTML escaping helpers
|
||||
- `default-familiar-controller.ts`: Default familiar visibility, position persistence, transient reactions, status badges, logging, and motion-shell orchestration
|
||||
- `default-familiar-controller.ts`: Default familiar visibility, position persistence, transient reactions, status badges, logging, and display/motion/window-shell orchestration
|
||||
- `default-familiar-controller-display.ts`: Extracted transient display timers, status badge lifetimes, and dismiss-token shaping
|
||||
- `default-familiar-controller-motion.ts`: Extracted default familiar movement blocking and animated reposition helpers
|
||||
- `default-familiar-controller-window.ts`: Extracted default familiar content reload, hide/destroy lifecycle, display handler wiring, bounds recovery, and resume/topology mouse-interop helpers
|
||||
- `agent-familiar-controller.ts`: Lease-triggered familiar windows, dismissal tracking, transient displays, status badges, logging
|
||||
- `plugins-view-presentation.tsx`: Extracted Control Center plugins route iconography, label maps, permission tones, and primary/status badge helpers
|
||||
- `plugins-view-state.ts`: Extracted Control Center plugins route state, selection/filter derivation, and install/update/load actions
|
||||
|
|
|
|||
154
apps/desktop/src/default-familiar-controller-window.ts
Normal file
154
apps/desktop/src/default-familiar-controller-window.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import { powerMonitor, screen, type BrowserWindow } from "electron";
|
||||
|
||||
import { resetDefaultPetPosition, setDefaultPetPosition } from "./app-state.js";
|
||||
import { defaultPetWindowSize, getDefaultPetInitialPosition } from "./display.js";
|
||||
import {
|
||||
getSafeDefaultPetPosition,
|
||||
loadDefaultPetContent,
|
||||
readWindowPosition,
|
||||
recoverPetMouseInterop,
|
||||
type PetPluginBubbles,
|
||||
type PetStatusBadgeReaction,
|
||||
type PetTransientDisplay,
|
||||
} from "./familiar-window.js";
|
||||
import { debug, info } from "./logger.js";
|
||||
|
||||
export interface DefaultPetWindowContentSnapshot {
|
||||
readonly paused: boolean;
|
||||
readonly display: PetTransientDisplay | null;
|
||||
readonly badge: PetStatusBadgeReaction | null;
|
||||
readonly dismissToken?: string;
|
||||
readonly pluginBubbles: PetPluginBubbles | null;
|
||||
}
|
||||
|
||||
export function loadDefaultPetWindowContent(
|
||||
window: BrowserWindow,
|
||||
snapshot: DefaultPetWindowContentSnapshot,
|
||||
): void {
|
||||
void loadDefaultPetContent(
|
||||
window,
|
||||
snapshot.paused,
|
||||
snapshot.display,
|
||||
snapshot.badge,
|
||||
snapshot.dismissToken,
|
||||
snapshot.pluginBubbles,
|
||||
);
|
||||
}
|
||||
|
||||
export function hideManagedDefaultPetWindow(
|
||||
window: BrowserWindow | null,
|
||||
petId: string | undefined,
|
||||
): void {
|
||||
if (!window || window.isDestroyed()) {
|
||||
debug("familiar.default", "hide skipped", { reason: "no-window" });
|
||||
return;
|
||||
}
|
||||
|
||||
info("familiar.default", "hide requested", {
|
||||
windowId: window.id,
|
||||
position: readWindowPosition(window),
|
||||
petId,
|
||||
});
|
||||
setDefaultPetPosition(readWindowPosition(window));
|
||||
window.hide();
|
||||
}
|
||||
|
||||
export function recoverManagedDefaultPetMouseInterop(
|
||||
window: BrowserWindow | null,
|
||||
reason: string,
|
||||
petId: string | undefined,
|
||||
): void {
|
||||
if (!window || window.isDestroyed()) {
|
||||
debug("familiar.default", "mouse interop recovery skipped", {
|
||||
reason,
|
||||
skippedReason: "no-window",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
debug("familiar.default", "mouse interop recovery requested", {
|
||||
windowId: window.id,
|
||||
reason,
|
||||
petId,
|
||||
});
|
||||
recoverPetMouseInterop(window, reason);
|
||||
}
|
||||
|
||||
export function destroyManagedDefaultPetWindow(
|
||||
window: BrowserWindow | null,
|
||||
petId: string | undefined,
|
||||
clearDisplayTimers: () => void,
|
||||
): BrowserWindow | null {
|
||||
clearDisplayTimers();
|
||||
|
||||
if (!window || window.isDestroyed()) {
|
||||
debug("familiar.default", "destroy skipped", { reason: "no-window" });
|
||||
return null;
|
||||
}
|
||||
|
||||
info("familiar.default", "destroy requested", {
|
||||
windowId: window.id,
|
||||
position: readWindowPosition(window),
|
||||
petId,
|
||||
});
|
||||
setDefaultPetPosition(readWindowPosition(window));
|
||||
window.setIgnoreMouseEvents(false);
|
||||
window.destroy();
|
||||
return null;
|
||||
}
|
||||
|
||||
export function installDefaultPetWindowDisplayHandlers(
|
||||
reclampWindow: () => void,
|
||||
recoverAfterResume: () => void,
|
||||
): void {
|
||||
screen.on("display-added", reclampWindow);
|
||||
screen.on("display-removed", reclampWindow);
|
||||
screen.on("display-metrics-changed", reclampWindow);
|
||||
powerMonitor.on("resume", recoverAfterResume);
|
||||
}
|
||||
|
||||
export function getManagedDefaultPetWindowBounds(
|
||||
window: BrowserWindow | null,
|
||||
): Electron.Rectangle | null {
|
||||
if (!window || window.isDestroyed()) return null;
|
||||
return window.getBounds();
|
||||
}
|
||||
|
||||
export function reclampManagedDefaultPetWindow(
|
||||
window: BrowserWindow | null,
|
||||
recoverMouseInterop: (reason: string) => void,
|
||||
): void {
|
||||
if (!window || window.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const safePosition = readWindowPosition(window);
|
||||
info("familiar.default", "reclamp position", {
|
||||
windowId: window.id,
|
||||
position: safePosition,
|
||||
});
|
||||
window.setPosition(safePosition.x, safePosition.y, false);
|
||||
setDefaultPetPosition(safePosition);
|
||||
recoverMouseInterop("display-change");
|
||||
}
|
||||
|
||||
export function recoverManagedDefaultPetWindowAfterResume(
|
||||
recoverMouseInterop: (reason: string) => void,
|
||||
scheduleTimeout: typeof setTimeout = setTimeout,
|
||||
): void {
|
||||
recoverMouseInterop("power-resume");
|
||||
scheduleTimeout(() => recoverMouseInterop("power-resume+500ms"), 500).unref?.();
|
||||
}
|
||||
|
||||
export function resetManagedDefaultPetWindowToInitialPosition(
|
||||
window: BrowserWindow | null,
|
||||
): void {
|
||||
const safePosition = getSafeDefaultPetPosition(
|
||||
getDefaultPetInitialPosition(defaultPetWindowSize),
|
||||
);
|
||||
resetDefaultPetPosition(safePosition);
|
||||
|
||||
if (window && !window.isDestroyed()) {
|
||||
window.setPosition(safePosition.x, safePosition.y, false);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { BrowserWindow, powerMonitor, screen } from "electron";
|
||||
import { BrowserWindow } from "electron";
|
||||
|
||||
import { getAppStateSnapshot, getDefaultPetPosition, resetDefaultPetPosition, setDefaultPetPosition, updatePreferences } from "./app-state.js";
|
||||
import { shouldShowDefaultPetForExternalEvent } from "./app-state-core.js";
|
||||
|
|
@ -11,10 +11,21 @@ import {
|
|||
setDefaultPetTransientDisplay,
|
||||
} from "./default-familiar-controller-display.js";
|
||||
import { getPetMovementBlockedReason, moveDefaultPetWindowBy } from "./default-familiar-controller-motion.js";
|
||||
import {
|
||||
destroyManagedDefaultPetWindow,
|
||||
getManagedDefaultPetWindowBounds,
|
||||
hideManagedDefaultPetWindow,
|
||||
installDefaultPetWindowDisplayHandlers,
|
||||
loadDefaultPetWindowContent,
|
||||
reclampManagedDefaultPetWindow,
|
||||
recoverManagedDefaultPetMouseInterop,
|
||||
recoverManagedDefaultPetWindowAfterResume,
|
||||
resetManagedDefaultPetWindowToInitialPosition,
|
||||
} from "./default-familiar-controller-window.js";
|
||||
import { defaultPetWindowSize, getDefaultPetInitialPosition } from "./display.js";
|
||||
import { debug, info } from "./logger.js";
|
||||
import { type FamiliarOSReaction } from "./local-ipc-protocol.js";
|
||||
import { createDefaultPetWindow, getSafeDefaultPetPosition, isPetWindowDragging, loadDefaultPetContent, readWindowPosition, recoverPetMouseInterop, type PetPluginBubbles, type PetTransientDisplay } from "./familiar-window.js";
|
||||
import { createDefaultPetWindow, getSafeDefaultPetPosition, isPetWindowDragging, loadDefaultPetContent, readWindowPosition, type PetPluginBubbles, type PetTransientDisplay } from "./familiar-window.js";
|
||||
import { PetBubbleArbiter, type ActiveBubble, type PetBubbleSink } from "./plugin-bubble-arbiter.js";
|
||||
import { publishPluginPetEvent } from "./plugin-events-source.js";
|
||||
|
||||
|
|
@ -78,15 +89,10 @@ function showDefaultPetWindow(source: "user" | "external-event"): void {
|
|||
|
||||
export function hideDefaultPet(): void {
|
||||
updatePreferences({ openDefaultPetOnLaunch: false });
|
||||
|
||||
if (!defaultPetWindow || defaultPetWindow.isDestroyed()) {
|
||||
debug("familiar.default", "hide skipped", { reason: "no-window" });
|
||||
return;
|
||||
}
|
||||
|
||||
info("familiar.default", "hide requested", { windowId: defaultPetWindow.id, position: readWindowPosition(defaultPetWindow), petId: getAppStateSnapshot().preferences.defaultPetId });
|
||||
setDefaultPetPosition(readWindowPosition(defaultPetWindow));
|
||||
defaultPetWindow.hide();
|
||||
hideManagedDefaultPetWindow(
|
||||
defaultPetWindow,
|
||||
getAppStateSnapshot().preferences.defaultPetId,
|
||||
);
|
||||
}
|
||||
|
||||
export function isDefaultPetVisible(): boolean {
|
||||
|
|
@ -101,7 +107,13 @@ export function setDefaultPetPaused(nextPaused: boolean): void {
|
|||
return;
|
||||
}
|
||||
|
||||
void loadDefaultPetContent(defaultPetWindow, paused, displayState.transientDisplay, displayState.statusBadge, getCurrentDismissToken(), getDefaultPetPluginBubbles());
|
||||
loadDefaultPetWindowContent(defaultPetWindow, {
|
||||
paused,
|
||||
display: displayState.transientDisplay,
|
||||
badge: displayState.statusBadge,
|
||||
dismissToken: getCurrentDismissToken(),
|
||||
pluginBubbles: getDefaultPetPluginBubbles(),
|
||||
});
|
||||
}
|
||||
|
||||
export function getDefaultPetPaused(): boolean {
|
||||
|
|
@ -119,17 +131,21 @@ export function refreshDefaultPetContent(): void {
|
|||
}
|
||||
|
||||
debug("familiar.default", "refresh content", { windowId: defaultPetWindow.id, paused, hasDisplay: Boolean(displayState.transientDisplay), badge: displayState.statusBadge, petId: getAppStateSnapshot().preferences.defaultPetId });
|
||||
void loadDefaultPetContent(defaultPetWindow, paused, displayState.transientDisplay, displayState.statusBadge, getCurrentDismissToken(), getDefaultPetPluginBubbles());
|
||||
loadDefaultPetWindowContent(defaultPetWindow, {
|
||||
paused,
|
||||
display: displayState.transientDisplay,
|
||||
badge: displayState.statusBadge,
|
||||
dismissToken: getCurrentDismissToken(),
|
||||
pluginBubbles: getDefaultPetPluginBubbles(),
|
||||
});
|
||||
}
|
||||
|
||||
export function recoverDefaultPetMouseInterop(reason: string): void {
|
||||
if (!defaultPetWindow || defaultPetWindow.isDestroyed()) {
|
||||
debug("familiar.default", "mouse interop recovery skipped", { reason, skippedReason: "no-window" });
|
||||
return;
|
||||
}
|
||||
|
||||
debug("familiar.default", "mouse interop recovery requested", { windowId: defaultPetWindow.id, reason, petId: getAppStateSnapshot().preferences.defaultPetId });
|
||||
recoverPetMouseInterop(defaultPetWindow, reason);
|
||||
recoverManagedDefaultPetMouseInterop(
|
||||
defaultPetWindow,
|
||||
reason,
|
||||
getAppStateSnapshot().preferences.defaultPetId,
|
||||
);
|
||||
}
|
||||
|
||||
export function applyExternalPetReaction(reaction: FamiliarOSReaction, options: PetReactionOptions = {}): { readonly shown: boolean; readonly reason?: string } {
|
||||
|
|
@ -194,33 +210,23 @@ export function applyExternalPetMoveToHome(): Promise<{ readonly moved: boolean;
|
|||
}
|
||||
|
||||
export function destroyDefaultPet(): void {
|
||||
clearDefaultPetDisplayTimers();
|
||||
|
||||
if (!defaultPetWindow || defaultPetWindow.isDestroyed()) {
|
||||
debug("familiar.default", "destroy skipped", { reason: "no-window" });
|
||||
defaultPetWindow = null;
|
||||
return;
|
||||
}
|
||||
|
||||
info("familiar.default", "destroy requested", { windowId: defaultPetWindow.id, position: readWindowPosition(defaultPetWindow), petId: getAppStateSnapshot().preferences.defaultPetId });
|
||||
setDefaultPetPosition(readWindowPosition(defaultPetWindow));
|
||||
const window = defaultPetWindow;
|
||||
defaultPetWindow = null;
|
||||
window.setIgnoreMouseEvents(false);
|
||||
window.destroy();
|
||||
defaultPetWindow = destroyManagedDefaultPetWindow(
|
||||
defaultPetWindow,
|
||||
getAppStateSnapshot().preferences.defaultPetId,
|
||||
clearDefaultPetDisplayTimers,
|
||||
);
|
||||
}
|
||||
|
||||
export function installDefaultPetDisplayHandlers(options: { readonly onPromptRequested?: () => void } = {}): void {
|
||||
promptRequestHandler = options.onPromptRequested;
|
||||
screen.on("display-added", reclampDefaultPetWindow);
|
||||
screen.on("display-removed", reclampDefaultPetWindow);
|
||||
screen.on("display-metrics-changed", reclampDefaultPetWindow);
|
||||
powerMonitor.on("resume", recoverDefaultPetWindowAfterResume);
|
||||
installDefaultPetWindowDisplayHandlers(
|
||||
reclampDefaultPetWindow,
|
||||
recoverDefaultPetWindowAfterResume,
|
||||
);
|
||||
}
|
||||
|
||||
export function getDefaultPetWindowBounds(): Electron.Rectangle | null {
|
||||
if (!defaultPetWindow || defaultPetWindow.isDestroyed()) return null;
|
||||
return defaultPetWindow.getBounds();
|
||||
return getManagedDefaultPetWindowBounds(defaultPetWindow);
|
||||
}
|
||||
|
||||
function handleBubbleDismissed(dismissToken: string): void {
|
||||
|
|
@ -350,20 +356,11 @@ function getCurrentDismissToken(): string | undefined {
|
|||
}
|
||||
|
||||
function reclampDefaultPetWindow(): void {
|
||||
if (!defaultPetWindow || defaultPetWindow.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const safePosition = readWindowPosition(defaultPetWindow);
|
||||
info("familiar.default", "reclamp position", { windowId: defaultPetWindow.id, position: safePosition });
|
||||
defaultPetWindow.setPosition(safePosition.x, safePosition.y, false);
|
||||
setDefaultPetPosition(safePosition);
|
||||
recoverDefaultPetMouseInterop("display-change");
|
||||
reclampManagedDefaultPetWindow(defaultPetWindow, recoverDefaultPetMouseInterop);
|
||||
}
|
||||
|
||||
function recoverDefaultPetWindowAfterResume(): void {
|
||||
recoverDefaultPetMouseInterop("power-resume");
|
||||
setTimeout(() => recoverDefaultPetMouseInterop("power-resume+500ms"), 500).unref?.();
|
||||
recoverManagedDefaultPetWindowAfterResume(recoverDefaultPetMouseInterop);
|
||||
}
|
||||
|
||||
export function shouldOpenDefaultPetOnLaunch(): boolean {
|
||||
|
|
@ -371,12 +368,7 @@ export function shouldOpenDefaultPetOnLaunch(): boolean {
|
|||
}
|
||||
|
||||
export function resetDefaultPetToInitialPosition(): void {
|
||||
const safePosition = getSafeDefaultPetPosition(getDefaultPetInitialPosition(defaultPetWindowSize));
|
||||
resetDefaultPetPosition(safePosition);
|
||||
|
||||
if (defaultPetWindow && !defaultPetWindow.isDestroyed()) {
|
||||
defaultPetWindow.setPosition(safePosition.x, safePosition.y, false);
|
||||
}
|
||||
resetManagedDefaultPetWindowToInitialPosition(defaultPetWindow);
|
||||
}
|
||||
|
||||
function getDefaultPetDisplayDeps() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const controllerSource = readFileSync(resolve(desktopRoot, "src/default-familiar-controller.ts"), "utf8");
|
||||
const controllerWindowSource = readFileSync(resolve(desktopRoot, "src/default-familiar-controller-window.ts"), "utf8");
|
||||
const getManagedDefaultPetWindowBounds = extractFunction(controllerWindowSource, "getManagedDefaultPetWindowBounds");
|
||||
const recoverManagedDefaultPetWindowAfterResume = extractFunction(controllerWindowSource, "recoverManagedDefaultPetWindowAfterResume");
|
||||
|
||||
assert.match(controllerSource, /from "\.\/default-familiar-controller-window(?:\.js)?"/, "default familiar controller must compose the extracted window seam.");
|
||||
assert.match(controllerWindowSource, /export function loadDefaultPetWindowContent/, "default familiar window seam must export content reload handling.");
|
||||
assert.match(controllerWindowSource, /export function installDefaultPetWindowDisplayHandlers/, "default familiar window seam must export display and resume handler installation.");
|
||||
assert.match(controllerWindowSource, /powerMonitor\.on\("resume", recoverAfterResume\)/, "default familiar window seam must recover mouse interop after system resume.");
|
||||
assert.match(controllerWindowSource, /recoverMouseInterop\("display-change"\)/, "default familiar window seam must recover mouse interop after monitor topology changes.");
|
||||
assert.match(getManagedDefaultPetWindowBounds, /if \(!window \|\| window\.isDestroyed\(\)\) return null;/, "window bounds seam must guard absent or destroyed windows.");
|
||||
assert.match(getManagedDefaultPetWindowBounds, /return window\.getBounds\(\);/, "window bounds seam must proxy BrowserWindow bounds for active familiars.");
|
||||
assert.match(recoverManagedDefaultPetWindowAfterResume, /recoverMouseInterop\("power-resume"\)/, "resume recovery seam must trigger an immediate mouse interop refresh.");
|
||||
assert.match(recoverManagedDefaultPetWindowAfterResume, /recoverMouseInterop\("power-resume\+500ms"\)/, "resume recovery seam must trigger the delayed follow-up refresh.");
|
||||
assert.match(recoverManagedDefaultPetWindowAfterResume, /scheduleTimeout\(\(\) => recoverMouseInterop\("power-resume\+500ms"\), 500\)\.unref\?\.\(\);/, "resume recovery seam must unref the delayed follow-up timer when available.");
|
||||
|
||||
console.error("Default familiar window seam validation passed.");
|
||||
|
||||
function extractFunction(source: string, functionName: string): string {
|
||||
const declaration = `function ${functionName}`;
|
||||
const start = source.indexOf(declaration);
|
||||
assert.notEqual(start, -1, `missing function ${functionName}`);
|
||||
|
||||
const bodyStart = source.indexOf("{\n", start);
|
||||
assert.notEqual(bodyStart, -1, `missing body for ${functionName}`);
|
||||
|
||||
let depth = 0;
|
||||
for (let index = bodyStart; index < source.length; index += 1) {
|
||||
const char = source[index];
|
||||
if (char === "{") depth += 1;
|
||||
if (char === "}") {
|
||||
depth -= 1;
|
||||
if (depth === 0) return source.slice(start, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
assert.fail(`unterminated function ${functionName}`);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue