Skip to content
71 changes: 71 additions & 0 deletions apps/desktop/src/preview/Manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2789,6 +2789,77 @@ describe("PreviewManager", () => {
),
);

effectIt.effect("keeps frame capture closed when a replacement is destroyed during cleanup", () =>
withManager((manager) =>
Effect.gen(function* () {
const windowEvents = new Map<string, () => void>();
const host = makeTestHostWebContents();
const capturePage = vi.fn(async () => ({
toJPEG: () => Buffer.from("recording-frame"),
getSize: () => ({ width: 1280, height: 720 }),
}));
const guest = makeTestPreviewWebContents(capturePage, 42, host);
fromId.mockReturnValue(guest);
yield* manager.createTab("tab_shutdown");
yield* manager.registerWebview("tab_shutdown", 42);
yield* manager.setMainWindow({
isDestroyed: () => false,
once: (event: string, listener: () => void) => windowEvents.set(event, listener),
webContents: { setBackgroundThrottling: vi.fn() },
} as never);
yield* manager.startRecording("tab_shutdown");
const pip = makeTestPictureInPictureWindow();
browserWindowConstructor.mockImplementation(function () {
return pip.pictureInPictureWindow;
});
yield* manager.openPictureInPicture("tab_shutdown");
const capturesBeforeClose = capturePage.mock.calls.length;
const cleanupStarted = yield* Deferred.make<void>();
const releaseCleanup = yield* Deferred.make<void>();
yield* manager.subscribeStateChanges((_tabId, state) =>
state.pictureInPicture
? Effect.void
: Deferred.succeed(cleanupStarted, undefined).pipe(
Effect.andThen(Deferred.await(releaseCleanup)),
),
);

windowEvents.get("closed")!();
yield* Deferred.await(cleanupStarted);
let destroyed = false;
const replacement = yield* manager
.setMainWindow({
isDestroyed: () => destroyed,
once: vi.fn(),
webContents: { setBackgroundThrottling: vi.fn() },
} as never)
.pipe(Effect.forkChild({ startImmediately: true }));
try {
expect(replacement.pollUnsafe()).toBeUndefined();
destroyed = true;
} finally {
yield* Deferred.succeed(releaseCleanup, undefined);
}
yield* Fiber.join(replacement);

const restart = yield* Effect.exit(manager.startRecording("tab_shutdown"));
expect(Exit.isFailure(restart)).toBe(true);
if (Exit.isFailure(restart)) {
expect(Option.getOrThrow(Cause.findErrorOption(restart.cause))).toMatchObject({
_tag: "PreviewMainWindowClosedError",
tabId: "tab_shutdown",
});
}
expect(capturePage).toHaveBeenCalledTimes(capturesBeforeClose);
expect(pip.pictureInPictureWindow.isDestroyed()).toBe(true);
expect(guest.setBackgroundThrottling.mock.calls).toEqual([[false], [true]]);
const grants: Array<{ video?: unknown }> = [];
host.displayMediaHandler()?.({ frame: host.mainFrame }, (value) => grants.push(value));
expect(grants).toEqual([{}]);
}),
),
);

effectIt.effect("ignores close events from replaced main windows", () =>
withManager((manager) =>
Effect.gen(function* () {
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/preview/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,8 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
}
yield* SynchronizedRef.modifyEffect(frameCaptureSessionsRef, (sessions) =>
Effect.gen(function* () {
// Cleanup or lock acquisition can outlive the replacement window.
if (window.isDestroyed()) return [undefined, sessions] as const;
if (sessions.size > 0) {
yield* setWindowBackgroundThrottling(window, false);
}
Expand Down
145 changes: 143 additions & 2 deletions apps/desktop/src/window/DesktopWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { assert, describe, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import { DesktopSnapShotId } from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import * as Logger from "effect/Logger";
Expand Down Expand Up @@ -227,6 +228,9 @@ function layerTest(input: {
readonly beforeMainWindowBoundsUpdate?: (
bounds: DesktopAppSettings.DesktopWindowBounds,
) => Effect.Effect<void>;
readonly setPreviewMainWindow?: (window: Electron.BrowserWindow) => Effect.Effect<void>;
readonly destroyAll?: Effect.Effect<void>;
readonly onWindowCreate?: Effect.Effect<void>;
readonly openedExternalUrls?: unknown[];
readonly copiedTexts?: string[];
readonly onPopupTemplate?: (input: ElectronMenu.ElectronMenuTemplateInput) => Effect.Effect<void>;
Expand Down Expand Up @@ -275,6 +279,7 @@ function layerTest(input: {
input.createdWindowOptions?.push(options);
}).pipe(
Effect.andThen(Ref.update(input.createCount, (count) => count + 1)),
Effect.andThen(input.onWindowCreate ?? Effect.void),
Effect.as(input.window),
),
main: Ref.get(input.mainWindow),
Expand All @@ -288,7 +293,7 @@ function layerTest(input: {
prepareReveal: () => Effect.succeed(false),
reveal: (window) => Effect.sync(() => input.onReveal?.(window)),
sendAll: () => Effect.void,
destroyAll: Effect.void,
destroyAll: input.destroyAll ?? Effect.void,
syncAllAppearance: (sync) => sync(input.window),
} satisfies ElectronWindow.ElectronWindow["Service"]);

Expand Down Expand Up @@ -333,7 +338,7 @@ function layerTest(input: {
}),
Layer.mock(PreviewManager.PreviewManager)({
getBrowserSession: () => Effect.succeed({} as Electron.Session),
setMainWindow: () => Effect.void,
setMainWindow: input.setPreviewMainWindow ?? (() => Effect.void),
prepareWebview: () => Effect.void,
isBrowserPartition: (partition) => partition.startsWith("persist:t3code-preview-"),
getBrowserPartition: () => Effect.succeed("persist:t3code-preview-test"),
Expand All @@ -344,6 +349,7 @@ function layerTest(input: {
}),
),
),
Layer.merge(DesktopState.layer),
);
}

Expand Down Expand Up @@ -425,6 +431,7 @@ const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | n
DesktopAppSettings.layerTest(),
layerDesktopClientSettings,
layerDesktopServerExposure,
DesktopState.layer,
layerElectronApp,
layerElectronMenu,
Layer.succeed(ElectronShell.ElectronShell, {
Expand Down Expand Up @@ -924,6 +931,140 @@ describe("DesktopWindow", () => {
}),
);

it.effect("does not open main or splash windows once quitting has started", () =>
Effect.gen(function* () {
const fake = makeFakeBrowserWindow();
const createCount = yield* Ref.make(0);
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
yield* Effect.gen(function* () {
const windows = yield* DesktopWindow.DesktopWindow;
const state = yield* DesktopState.DesktopState;
yield* Ref.set(state.quitting, true);

yield* windows.showConnectingSplash;
yield* windows.handleBackendReady(new URL("http://127.0.0.1:3773"));
yield* windows.activate;
yield* windows.dispatchMenuAction("open-settings");
for (const creation of [
windows.createMain,
windows.ensureMain,
windows.revealOrCreateMain,
]) {
assert.isTrue(Exit.isFailure(yield* Effect.exit(creation)));
}

assert.equal(yield* Ref.get(createCount), 0);
assert.isTrue(Option.isNone(yield* Ref.get(mainWindow)));
assert.equal(fake.send.mock.calls.length, 0);
}).pipe(Effect.provide(layerTest({ window: fake.window, createCount, mainWindow })));
}),
);

it.effect.each(["main", "splash"] as const)(
"destroys a %s window when quit races native creation without loading or revealing it",
(kind) =>
Effect.gen(function* () {
const fake = makeFakeBrowserWindow();
const createCount = yield* Ref.make(0);
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const creationStarted = yield* Deferred.make<void>();
const releaseCreation = yield* Deferred.make<void>();
const reveal = vi.fn();
const layer = layerTest({
window: fake.window,
createCount,
mainWindow,
onWindowCreate: Deferred.succeed(creationStarted, undefined).pipe(
Effect.andThen(Deferred.await(releaseCreation)),
),
destroyAll: Effect.sync(() => fake.isDestroyed.mockReturnValue(true)),
onReveal: reveal,
});

yield* Effect.gen(function* () {
const windows = yield* DesktopWindow.DesktopWindow;
const state = yield* DesktopState.DesktopState;
const fiber = yield* (
kind === "splash"
? windows.showConnectingSplash
: windows.handleBackendReady(new URL("http://127.0.0.1:3773"))
).pipe(Effect.forkChild({ startImmediately: true }));
yield* Deferred.await(creationStarted);
yield* Ref.set(state.quitting, true);
yield* Deferred.succeed(releaseCreation, undefined);
assert.isTrue(Exit.isSuccess(yield* Fiber.await(fiber)));
assert.equal(yield* Ref.get(createCount), 1);
assert.isTrue(fake.window.isDestroyed());
assert.isTrue(Option.isNone(yield* Ref.get(mainWindow)));
assert.equal(fake.loadURL.mock.calls.length, 0);
assert.equal(fake.openDevTools.mock.calls.length, 0);
assert.equal(reveal.mock.calls.length, 0);
}).pipe(Effect.provide(layer));
}),
);

it.effect.each(["backend ready", "activation", "renderer event"] as const)(
"quietly cancels %s when quit races preview cleanup",
(entry) =>
Effect.gen(function* () {
const fake = makeFakeBrowserWindow();
const createCount = yield* Ref.make(0);
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const previewStarted = yield* Deferred.make<void>();
const releasePreview = yield* Deferred.make<void>();
const reveal = vi.fn();
const warnings: unknown[] = [];
const logger = Logger.make(({ logLevel, message }) => {
if (logLevel === "Warn" || logLevel === "Error") warnings.push(message);
});
const layer = layerTest({
window: fake.window,
createCount,
mainWindow,
desktopSettings: {
...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
localEnvironmentEnabled: false,
},
setPreviewMainWindow: () =>
Deferred.succeed(previewStarted, undefined).pipe(
Effect.andThen(Deferred.await(releasePreview)),
),
destroyAll: Effect.sync(() => fake.isDestroyed.mockReturnValue(true)),
onReveal: reveal,
});

yield* Effect.gen(function* () {
const windows = yield* DesktopWindow.DesktopWindow;
const state = yield* DesktopState.DesktopState;
const creation =
entry === "backend ready"
? windows.handleBackendReady(new URL("http://127.0.0.1:3773"))
: entry === "activation"
? windows.activate
: windows.dispatchMenuAction("open-settings");
const fiber = yield* creation.pipe(Effect.forkChild({ startImmediately: true }));
yield* Deferred.await(previewStarted);
yield* Ref.set(state.quitting, true);
// Cover both an already-destroyed window and one shutdown has yet to destroy.
fake.isDestroyed.mockReturnValue(entry !== "backend ready");
yield* Deferred.succeed(releasePreview, undefined);

assert.isTrue(Exit.isSuccess(yield* Fiber.await(fiber)));
assert.isTrue(fake.window.isDestroyed());
assert.isTrue(Option.isNone(yield* Ref.get(mainWindow)));
assert.equal(fake.loadURL.mock.calls.length, 0);
assert.equal(fake.openDevTools.mock.calls.length, 0);
assert.equal(fake.send.mock.calls.length, 0);
assert.equal(reveal.mock.calls.length, 0);
assert.deepEqual(warnings, []);
}).pipe(
Effect.provide(
Layer.mergeAll(layer, Logger.layer([logger], { mergeWithExisting: false })),
),
);
}),
);

it.effect("restores the persisted maximized state", () =>
Effect.gen(function* () {
const fakeWindow = makeFakeBrowserWindow();
Expand Down
Loading
Loading