Skip to content
63 changes: 63 additions & 0 deletions apps/desktop/src/snapShot/DesktopSnapShot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2725,6 +2725,69 @@ it.each([
}
});

it.each([
["com.google.Chrome", "Google Chrome – Profile"],
["org.chromium.Chromium", "Chromium"],
])("reads macOS %s page context with an accessibility title suffix", async (bundleId, suffix) => {
const title = "Issues · pingdotgg/t3code";
const bounds = { x: 0, y: 33, width: 1470, height: 857 };
const window = {
role: "window",
name: `${title} - ${suffix}`,
bounds,
tree: async () => ({ name: title, children: [{ name: "New issue", children: [] }] }),
children: async () => [{ role: "button", name: "New issue", bounds, children: async () => [] }],
};
accessibilityByPidMock.mockReset().mockImplementation(async (pid: number) => ({
children: async () => (pid === 42 ? [window] : []),
}));

const context = await readAccessibleWindowContext(
{ title, bounds, owner: { processId: 42, bundleId } },
"darwin",
title,
);

assert.equal(context?.accessibleText, `${title}\nNew issue`);
assert.equal(context?.accessibility?.format, "element-tree");
assert.equal(
context?.accessibility?.format === "element-tree"
? context.accessibility.root.children[0]?.name
: undefined,
"New issue",
);
});

it("does not borrow another Chrome process context when the captured PID has no windows", async () => {
const title = "Issues · pingdotgg/t3code";
const bounds = { x: 0, y: 33, width: 1470, height: 857 };
const otherWindow = {
role: "window",
name: `${title} - Google Chrome – Profile`,
bounds,
tree: async () => ({
name: title,
children: [{ name: "Other process private text", children: [] }],
}),
children: async () => [],
};
accessibilityByPidMock.mockReset().mockResolvedValue({
pid: 42,
children: async () => [],
});
accessibilityListMock.mockResolvedValue([
{ pid: 43, bundleId: "com.google.Chrome", children: async () => [otherWindow] },
]);

assert.isUndefined(
await readAccessibleWindowContext(
{ title, bounds, owner: { processId: 42, bundleId: "com.google.Chrome" } },
"darwin",
title,
),
);
});

it("reads Flatpak app text from the AT-SPI proxy when the compositor PID misses", async () => {
vi.stubEnv("XDG_SESSION_TYPE", "wayland");
const bounds = { x: 0, y: 0, width: 1_920, height: 1_048 };
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/snapShot/SnapShotAccessibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type AccessibleWindowIdentity = {
readonly title: string;
readonly bounds: Electron.Rectangle;
readonly clientBounds?: Electron.Rectangle;
readonly owner: { readonly processId: number };
readonly owner: { readonly processId: number; readonly bundleId?: string };
readonly accessibilityBoundsReliable?: boolean;
};

Expand Down Expand Up @@ -134,6 +134,8 @@ async function readCapturedWindowAccessibility(
sourceTitle,
bounds: active.bounds,
clientBounds: active.clientBounds,
platform,
bundleId: pidWindows.length > 0 ? active.owner.bundleId : undefined,
};
const window = findAccessibleWindow(
pidWindows.length > 0 || foreground !== undefined ? pidWindows : await windowsFromAppList(App),
Expand Down
52 changes: 52 additions & 0 deletions apps/desktop/src/snapShot/snapShot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,58 @@ describe("findAccessibleWindow", () => {
bounds: { x: 100, y: 200, width: 800, height: 600 },
};

const chromeCapture = {
title: "Issues · pingdotgg/t3code",
bounds: captured.bounds,
platform: "darwin" as const,
bundleId: "com.google.Chrome",
};
const chromeWindow = {
name: `${chromeCapture.title} - Google Chrome – Profile`,
bounds: chromeCapture.bounds,
active: true,
};

it("uses a browser suffix only when no exact title matches", () => {
const exact = { name: chromeCapture.title, bounds: chromeCapture.bounds };
expect(findAccessibleWindow([chromeWindow, exact], chromeCapture)).toBe(exact);
expect(findAccessibleWindow([chromeWindow], chromeCapture)).toBe(chromeWindow);
});

it("rejects browser suffix matches with wrong bounds, page titles, or ambiguous windows", () => {
expect(
findAccessibleWindow(
[{ ...chromeWindow, bounds: { ...chromeWindow.bounds, x: 0 } }],
chromeCapture,
),
).toBeUndefined();
expect(
findAccessibleWindow([chromeWindow, { ...chromeWindow, active: false }], chromeCapture),
).toBeUndefined();
expect(
findAccessibleWindow([chromeWindow], { ...chromeCapture, title: "Issues" }),
).toBeUndefined();
expect(
findAccessibleWindow([chromeWindow], {
...chromeCapture,
title: `${chromeCapture.title} - Google Chrome`,
}),
).toBeUndefined();
});

it("keeps strict titles outside macOS Chrome and Chromium", () => {
for (const identity of [
{ platform: "linux" as const, bundleId: "com.google.Chrome" },
{ platform: "win32" as const, bundleId: "com.google.Chrome" },
{ platform: "darwin" as const, bundleId: "com.example.Editor" },
{ platform: "darwin" as const, bundleId: undefined },
]) {
expect(
findAccessibleWindow([chromeWindow], { ...chromeCapture, ...identity }),
).toBeUndefined();
}
});

it("matches one window by its captured bounds", () => {
const windows = [
{ name: "Private", bounds: { x: 0, y: 0, width: 400, height: 300 } },
Expand Down
35 changes: 35 additions & 0 deletions apps/desktop/src/snapShot/snapShot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,8 @@ export function findAccessibleWindow<
readonly sourceTitle?: string;
readonly bounds: WindowBounds;
readonly clientBounds?: WindowBounds | undefined;
readonly platform?: NodeJS.Platform;
readonly bundleId?: string | undefined;
},
matchMode: "screen-bounds" | "wayland" = "screen-bounds",
options: { readonly allowUntitledUniqueBounds?: boolean } = {},
Expand Down Expand Up @@ -515,6 +517,8 @@ export function findAccessibleWindow<
const activeMatches = matches.filter((window) => safeProperty(() => window.active) === true);
if (activeMatches.length === 1) return activeMatches[0];
if (matches.length > 1) return undefined;
const browserMatch = findBrowserTitleMatch(windows, titles, captured, matchMode, matchesBounds);
if (browserMatch) return browserMatch;
}
// GTK4/libadwaita often exposes the frame as an unnamed group. A PID-scoped
// lookup can accept the one window whose bounds match; size-only guesses cannot.
Expand All @@ -525,6 +529,37 @@ export function findAccessibleWindow<
return boundsMatches.length === 1 ? boundsMatches[0] : undefined;
}

// Chrome's macOS AX title can append the browser and profile to the captured page title.
function findBrowserTitleMatch<T extends { readonly name?: string | null }>(
windows: readonly T[],
titles: ReadonlySet<string>,
captured: { readonly platform?: NodeJS.Platform; readonly bundleId?: string | undefined },
matchMode: "screen-bounds" | "wayland",
matchesBounds: (window: T) => boolean,
): T | undefined {
if (captured.platform !== "darwin" || matchMode !== "screen-bounds") return undefined;
const browserName = /^com\.google\.Chrome(?:\.(?:beta|dev|canary))?$/.test(
captured.bundleId ?? "",
)
? "Google Chrome"
: captured.bundleId === "org.chromium.Chromium"
? "Chromium"
: undefined;
if (!browserName) return undefined;
const browserMatches = windows.filter((window) => {
if (!matchesBounds(window)) return false;
const name = window.name?.trim() ?? "";
return [...titles].some((title) => {
const prefix = `${title} - ${browserName}`;
return (
name === prefix ||
(name.startsWith(prefix) && /^ [–—-] \S.*$/u.test(name.slice(prefix.length)))
);
});
});
return browserMatches.length === 1 ? browserMatches[0] : undefined;
}

const ELECTRON_KEY_NAMES: Readonly<Record<string, string>> = {
" ": "Space",
"+": "Plus",
Expand Down
Loading