Skip to content

Commit 83044d9

Browse files
committed
chore: merge upstream preview test fix
2 parents 1dda3cf + 31be3e9 commit 83044d9

329 files changed

Lines changed: 14962 additions & 1163 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dist-electron/
3333
.showcase/
3434
apps/mobile/.showcase/
3535
apps/mobile/.generated/
36+
apps/mobile/.android-cache/
3637
/.generated/
3738
artifacts/app-store/screenshots/
3839
.github/pr-assets/

‎apps/desktop/src/preview/Manager.test.ts‎

Lines changed: 139 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,8 @@ const makeFaviconWebContents = (options?: {
506506
send: webviewSend,
507507
session: { fetch },
508508
navigationHistory: { canGoBack: () => false, canGoForward: () => false },
509+
isFocused: vi.fn(() => true),
510+
focusedFrame: {} as object | null,
509511
setIgnoreMenuShortcuts: vi.fn(),
510512
setWindowOpenHandler: vi.fn(),
511513
executeJavaScriptInIsolatedWorld,
@@ -643,7 +645,6 @@ describe("PreviewManager", () => {
643645
const hostWebContents = { sendInputEvent, isDestroyed: () => false };
644646
Object.assign(preview.webContents, { hostWebContents });
645647
fromId.mockReturnValue(preview.webContents);
646-
getFocusedWebContents.mockReturnValue(preview.webContents as never);
647648
yield* manager.setMainWindow({
648649
isDestroyed: () => false,
649650
once: vi.fn(),
@@ -708,7 +709,6 @@ describe("PreviewManager", () => {
708709
hostWebContents: { isDestroyed: () => false, send },
709710
});
710711
fromId.mockReturnValue(preview.webContents);
711-
getFocusedWebContents.mockReturnValue(preview.webContents as never);
712712
yield* manager.createTab("tab_reopen");
713713
yield* manager.registerWebview("tab_reopen", 42);
714714
const shortcut = parseKeybindingShortcut(chord)!;
@@ -737,12 +737,12 @@ describe("PreviewManager", () => {
737737
expect(preventDefault).toHaveBeenCalledTimes("isAutoRepeat" in overrides ? 1 : 0);
738738
expect(send).toHaveBeenCalledOnce();
739739
}
740-
getFocusedWebContents.mockReturnValue(null);
740+
vi.mocked((preview.webContents as Electron.WebContents).isFocused).mockReturnValue(false);
741741
preventDefault.mockClear();
742742
beforeInput({ preventDefault } as never, input as never);
743743
expect(preventDefault).not.toHaveBeenCalled();
744744
expect(send).toHaveBeenCalledOnce();
745-
getFocusedWebContents.mockReturnValue(preview.webContents as never);
745+
vi.mocked((preview.webContents as Electron.WebContents).isFocused).mockReturnValue(true);
746746
yield* manager.setForwardedShortcuts([]);
747747
beforeInput({ preventDefault } as never, input as never);
748748
expect(preventDefault).not.toHaveBeenCalled();
@@ -770,7 +770,6 @@ describe("PreviewManager", () => {
770770
hostWebContents: { isDestroyed: () => false, send },
771771
});
772772
fromId.mockReturnValue(preview.webContents);
773-
getFocusedWebContents.mockReturnValue(preview.webContents as never);
774773
yield* manager.createTab("tab_alt_graph");
775774
yield* manager.registerWebview("tab_alt_graph", 42);
776775
yield* manager.setForwardedShortcuts([
@@ -812,7 +811,6 @@ describe("PreviewManager", () => {
812811

813812
for (const browser of [preview, popup]) {
814813
const contents = browser.webContents as Electron.WebContents;
815-
getFocusedWebContents.mockReturnValue(browser.webContents as never);
816814
const beforeInput = browser.listeners.get("before-input-event")!;
817815
const preventDefault = vi.fn();
818816
const input = {
@@ -834,7 +832,7 @@ describe("PreviewManager", () => {
834832
beforeInput({ preventDefault } as never, { ...input, key: "w" } as never);
835833
expect(contents.setIgnoreMenuShortcuts).toHaveBeenLastCalledWith(true);
836834

837-
getFocusedWebContents.mockReturnValue(null);
835+
vi.mocked(contents.isFocused).mockReturnValue(false);
838836
beforeInput({ preventDefault } as never, input as never);
839837
expect(contents.setIgnoreMenuShortcuts).toHaveBeenLastCalledWith(true);
840838
expect(preventDefault).not.toHaveBeenCalled();
@@ -843,6 +841,61 @@ describe("PreviewManager", () => {
843841
),
844842
);
845843

844+
effectIt.effect("keeps editing and forwarded shortcuts in whichever tab holds focus", () =>
845+
withManager((manager) =>
846+
Effect.gen(function* () {
847+
const send = vi.fn();
848+
const host = { isDestroyed: () => false, send };
849+
const older = makeFaviconWebContents({ id: 42 });
850+
const newer = makeFaviconWebContents({ id: 43 });
851+
for (const tab of [older, newer]) Object.assign(tab.webContents, { hostWebContents: host });
852+
// Electron names one arbitrary guest here when several share a focused window.
853+
getFocusedWebContents.mockReturnValue(newer.webContents as never);
854+
fromId.mockReturnValue(older.webContents);
855+
yield* manager.createTab("tab_older");
856+
yield* manager.registerWebview("tab_older", 42);
857+
fromId.mockReturnValue(newer.webContents);
858+
yield* manager.createTab("tab_newer");
859+
yield* manager.registerWebview("tab_newer", 43);
860+
const shortcut = parseKeybindingShortcut("mod+shift+t")!;
861+
yield* manager.setForwardedShortcuts([{ command: "view.reopenClosed", shortcut }]);
862+
863+
const beforeInput = older.listeners.get("before-input-event")!;
864+
const key = { type: "keyDown", meta: true, control: false, alt: false };
865+
beforeInput(
866+
{ preventDefault: vi.fn() } as never,
867+
{ ...key, key: "v", shift: false } as never,
868+
);
869+
expect(
870+
(older.webContents as Electron.WebContents).setIgnoreMenuShortcuts,
871+
).toHaveBeenLastCalledWith(false);
872+
873+
const preventDefault = vi.fn();
874+
beforeInput({ preventDefault } as never, { ...key, key: "T", shift: true } as never);
875+
expect(preventDefault).toHaveBeenCalledOnce();
876+
expect(send).toHaveBeenCalledExactlyOnceWith("desktop:menu-action", "view.reopenClosed");
877+
878+
// The person types in the older tab, so keys reaching the newer one are an agent's.
879+
Object.assign(newer.webContents, { focusedFrame: null });
880+
const agentInput = newer.listeners.get("before-input-event")!;
881+
const agentPreventDefault = vi.fn();
882+
agentInput(
883+
{ preventDefault: agentPreventDefault } as never,
884+
{ ...key, key: "v", shift: false } as never,
885+
);
886+
expect(
887+
(newer.webContents as Electron.WebContents).setIgnoreMenuShortcuts,
888+
).toHaveBeenLastCalledWith(true);
889+
agentInput(
890+
{ preventDefault: agentPreventDefault } as never,
891+
{ ...key, key: "T", shift: true } as never,
892+
);
893+
expect(agentPreventDefault).not.toHaveBeenCalled();
894+
expect(send).toHaveBeenCalledOnce();
895+
}),
896+
),
897+
);
898+
846899
effectIt.effect("rejects a destroyed webview during registration", () =>
847900
withManager((manager) =>
848901
Effect.gen(function* () {
@@ -3322,6 +3375,85 @@ describe("PreviewManager", () => {
33223375
),
33233376
);
33243377

3378+
effectIt.effect(
3379+
"releases queued STOP after stalled recording warmups and ignores late completion",
3380+
() =>
3381+
withManager((manager) =>
3382+
Effect.gen(function* () {
3383+
const warmupStarted = Promise.withResolvers<void>();
3384+
const firstWarmup = Promise.withResolvers<TestCapturedPreviewImage>();
3385+
const lateWarmup = Promise.withResolvers<TestCapturedPreviewImage>();
3386+
const capturePage = vi
3387+
.fn<() => Promise<TestCapturedPreviewImage>>()
3388+
.mockImplementationOnce(() => {
3389+
warmupStarted.resolve();
3390+
return firstWarmup.promise;
3391+
})
3392+
.mockImplementation(() => lateWarmup.promise);
3393+
const host = makeTestHostWebContents();
3394+
const webContents = makeTestPreviewWebContents(capturePage, 42, host);
3395+
const windowThrottling = vi.fn();
3396+
fromId.mockReturnValue(webContents);
3397+
yield* manager.createTab("tab_recording_stalled_warmup");
3398+
yield* manager.registerWebview("tab_recording_stalled_warmup", 42);
3399+
yield* manager.setMainWindow({
3400+
isDestroyed: () => false,
3401+
once: vi.fn(),
3402+
webContents: { setBackgroundThrottling: windowThrottling },
3403+
} as never);
3404+
3405+
const image = {
3406+
toJPEG: () => Buffer.from("late-recording-frame"),
3407+
getSize: () => ({ width: 1280, height: 720 }),
3408+
};
3409+
try {
3410+
const start = yield* manager
3411+
.startRecording("tab_recording_stalled_warmup")
3412+
.pipe(Effect.forkChild({ startImmediately: true }));
3413+
yield* Effect.promise(() => warmupStarted.promise);
3414+
let stopped = false;
3415+
const stop = yield* manager.stopRecording("tab_recording_stalled_warmup").pipe(
3416+
Effect.andThen(
3417+
Effect.sync(() => {
3418+
stopped = true;
3419+
}),
3420+
),
3421+
Effect.forkChild({ startImmediately: true }),
3422+
);
3423+
expect(webContents.setBackgroundThrottling.mock.calls).toEqual([[false]]);
3424+
expect(windowThrottling.mock.calls).toEqual([[false]]);
3425+
yield* TestClock.adjust(1_000);
3426+
expect(stopped).toBe(false);
3427+
expect(host.executeJavaScript).not.toHaveBeenCalled();
3428+
yield* TestClock.adjust(1_000);
3429+
expect(stopped).toBe(true);
3430+
yield* Fiber.join(start);
3431+
yield* Fiber.join(stop);
3432+
expect(capturePage).toHaveBeenCalledTimes(2);
3433+
expect(host.executeJavaScript).toHaveBeenCalledOnce();
3434+
expect(webContents.setBackgroundThrottling.mock.calls).toEqual([[false], [true]]);
3435+
expect(windowThrottling.mock.calls).toEqual([[false], [true]]);
3436+
expect(webviewSend).toHaveBeenLastCalledWith("preview:recording-cursor", false);
3437+
3438+
lateWarmup.resolve(image);
3439+
yield* TestClock.adjust(0);
3440+
const streams: Array<{ video?: unknown }> = [];
3441+
host.displayMediaHandler()?.({ frame: host.mainFrame }, (value) => streams.push(value));
3442+
expect(streams).toEqual([{}]);
3443+
expect(capturePage).toHaveBeenCalledTimes(2);
3444+
expect(host.executeJavaScript).toHaveBeenCalledOnce();
3445+
expect(webContents.setBackgroundThrottling.mock.calls).toEqual([[false], [true]]);
3446+
expect(windowThrottling.mock.calls).toEqual([[false], [true]]);
3447+
expect(webviewSend).toHaveBeenLastCalledWith("preview:recording-cursor", false);
3448+
} finally {
3449+
// Release stalled native calls after a failing assertion against unchanged base.
3450+
firstWarmup.resolve(image);
3451+
lateWarmup.resolve(image);
3452+
}
3453+
}),
3454+
),
3455+
);
3456+
33253457
effectIt.effect("continues native recording when the source warmup fails", () =>
33263458
withManager((manager) =>
33273459
Effect.gen(function* () {

‎apps/desktop/src/preview/Manager.ts‎

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1479,12 +1479,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
14791479
}).pipe(Effect.ignore),
14801480
);
14811481
};
1482+
// Not getFocusedWebContents(): every guest of a focused window reports focus.
1483+
// An agent's keys reach a guest without a focused frame through GuestInput.
1484+
const holdsKeyboardFocus = (contents: Electron.WebContents) =>
1485+
contents.isFocused() && contents.focusedFrame !== null;
14821486
const syncMenuShortcuts = (contents: Electron.WebContents, input: Electron.Input): void => {
14831487
if (input.type !== "keyDown") return;
1484-
// Background automation must not edit whichever other renderer has focus.
14851488
contents.setIgnoreMenuShortcuts(
1486-
!isPreviewEditingShortcut(input, hostPlatform) ||
1487-
webContents.getFocusedWebContents() !== contents,
1489+
!isPreviewEditingShortcut(input, hostPlatform) || !holdsKeyboardFocus(contents),
14881490
);
14891491
};
14901492
// A popup opens with Electron's default handler, so the page inside it could
@@ -1505,7 +1507,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
15051507
!input.isComposing &&
15061508
host &&
15071509
!host.isDestroyed() &&
1508-
webContents.getFocusedWebContents() === wc &&
1510+
holdsKeyboardFocus(wc) &&
15091511
!(
15101512
hostPlatform !== "darwin" &&
15111513
input.control &&
@@ -3095,14 +3097,20 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
30953097
yield* attempt({ operation: "recording.cursor", tabId, webContentsId: wc.id }, () =>
30963098
wc.send(RECORDING_CURSOR_CHANNEL, true, options, tab?.controller),
30973099
);
3098-
yield* attemptPromise(
3099-
{
3100-
operation: "recording.warmSource",
3101-
tabId,
3102-
webContentsId: wc.id,
3103-
},
3104-
() => wc.capturePage().then(() => undefined),
3105-
).pipe(Effect.retry({ times: 1 }), Effect.ignore);
3100+
const warmupContext = { operation: "recording.warmSource", tabId, webContentsId: wc.id };
3101+
yield* Effect.tryPromise({
3102+
// Electron cannot cancel capturePage, but a stalled warmup must release the tab lock.
3103+
try: (_signal) => wc.capturePage(),
3104+
catch: (cause) => new PreviewOperationError({ ...warmupContext, cause }),
3105+
}).pipe(
3106+
Effect.timeout(CAPTURE_PAGE_ATTEMPT_TIMEOUT_MS),
3107+
Effect.catchTags({
3108+
TimeoutError: (cause) =>
3109+
Effect.fail(new PreviewOperationError({ ...warmupContext, cause })),
3110+
}),
3111+
Effect.retry({ times: 1 }),
3112+
Effect.ignore,
3113+
);
31063114
const currentWebContents = yield* requireWebContents(tabId);
31073115
if (currentWebContents !== wc || wc.isDestroyed()) {
31083116
return yield* new PreviewWebContentsNotFoundError({

0 commit comments

Comments
 (0)