Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions apps/desktop/src/snapShot/SnapShotAccessibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect, it } from "vite-plus/test";

import { readAccessibleWindowContextWithApp } from "./SnapShotAccessibility.ts";

const bounds = { x: 0, y: 33, width: 1470, height: 857 };
const window = {
name: "Issues · pingdotgg/t3code - Google Chrome – Profile",
role: "window",
bounds,
children: async () => [],
tree: async () => ({
role: "window",
name: "Issues · pingdotgg/t3code",
children: [{ role: "link", name: "Issues" }],
}),
};
const App = {
byPid: async (pid: number) => ({ children: async () => (pid === 42 ? [window] : []) }),
} as unknown as Parameters<typeof readAccessibleWindowContextWithApp>[0];

const request = {
active: {
title: "Issues · pingdotgg/t3code",
bounds,
owner: { processId: 42, bundleId: "com.google.Chrome" },
},
platform: "darwin" as const,
sourceTitle: "Issues · pingdotgg/t3code",
imageSize: { width: 1470, height: 857 },
};

it("includes Chrome accessibility context when macOS adds a profile to its title", async () => {
const context = await readAccessibleWindowContextWithApp(App, request);
expect(context?.accessibleText).toContain("Issues");
expect(context?.accessibility).toBeDefined();
});

it("does not borrow browser accessibility from another process or application", async () => {
for (const owner of [
{ processId: 43, bundleId: "com.google.Chrome" },
{ processId: 42, bundleId: "com.example.Editor" },
]) {
expect(
await readAccessibleWindowContextWithApp(App, {
...request,
active: { ...request.active, owner },
}),
).toBeUndefined();
}
});
11 changes: 9 additions & 2 deletions 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 @@ -112,7 +112,14 @@ async function readCapturedWindowAccessibility(
const matchMode = isWaylandSession(platform, process.env) ? "wayland" : "screen-bounds";
const window = findAccessibleWindow(
windows,
{ title: active.title, sourceTitle, bounds: active.bounds, clientBounds: active.clientBounds },
{
title: active.title,
sourceTitle,
bounds: active.bounds,
clientBounds: active.clientBounds,
platform,
bundleId: active.owner.bundleId,
},
matchMode,
);
if (!window) {
Expand Down
83 changes: 83 additions & 0 deletions apps/desktop/src/snapShot/snapShot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,89 @@ describe("findAccessibleWindow", () => {
bounds: { x: 100, y: 200, width: 800, height: 600 },
};

const chromeCapture = {
title: "Issues · pingdotgg/t3code",
bounds: { x: 0, y: 33, width: 1470, height: 857 },
platform: "darwin" as const,
bundleId: "com.google.Chrome",
};

it.each([
["com.google.Chrome", "Google Chrome"],
["com.google.Chrome.beta", "Google Chrome"],
["com.google.Chrome.dev", "Google Chrome"],
["com.google.Chrome.canary", "Google Chrome"],
["org.chromium.Chromium", "Chromium"],
])("matches a macOS browser profile suffix for %s", (bundleId, browserName) => {
const window = {
name: `${chromeCapture.title} - ${browserName} – Profile`,
bounds: chromeCapture.bounds,
};
expect(findAccessibleWindow([window], { ...chromeCapture, bundleId })).toBe(window);
});

it("matches a Chrome title without a profile suffix", () => {
const window = {
name: `${chromeCapture.title} - Google Chrome`,
bounds: chromeCapture.bounds,
};
expect(findAccessibleWindow([window], chromeCapture)).toBe(window);
});

it.each([
{ 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 },
])("keeps strict titles outside macOS Chrome: %j", (identity) => {
const window = {
name: `${chromeCapture.title} - Google Chrome – Profile`,
bounds: chromeCapture.bounds,
};
expect(findAccessibleWindow([window], { ...chromeCapture, ...identity })).toBeUndefined();
});

it.each([
"Different page - Google Chrome – Profile",
"Issues · pingdotgg/t3code copied - Google Chrome – Profile",
"Issues · pingdotgg/t3code - Google ChromeSomething",
])("rejects unrelated browser titles: %s", (name) => {
expect(
findAccessibleWindow([{ name, bounds: chromeCapture.bounds }], chromeCapture),
).toBeUndefined();
});

it("keeps bounds checks and rejects ambiguous browser suffix matches", () => {
const window = {
name: `${chromeCapture.title} - Google Chrome – Profile`,
bounds: chromeCapture.bounds,
};
expect(
findAccessibleWindow([{ ...window, bounds: { ...window.bounds, x: 3 } }], chromeCapture),
).toBeUndefined();
expect(
findAccessibleWindow([window, { ...window, active: true }], chromeCapture),
).toBeUndefined();
expect(findAccessibleWindow([window], chromeCapture, "wayland")).toBeUndefined();
});

it("prefers an exact title over a browser suffix match", () => {
const exact = { name: chromeCapture.title, bounds: chromeCapture.bounds };
const browser = {
name: `${chromeCapture.title} - Google Chrome – Profile`,
bounds: chromeCapture.bounds,
active: true,
};
expect(findAccessibleWindow([browser, exact], chromeCapture)).toBe(exact);
});

it("does not strip browser suffixes from captured page titles", () => {
const window = { name: "Page - Google Chrome – Profile", bounds: chromeCapture.bounds };
expect(
findAccessibleWindow([window], { ...chromeCapture, title: "Page - Google Chrome" }),
).toBeUndefined();
});

it("matches one window by its captured bounds", () => {
const windows = [
{ name: "Private", bounds: { x: 0, y: 0, width: 400, height: 300 } },
Expand Down
34 changes: 30 additions & 4 deletions apps/desktop/src/snapShot/snapShot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,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",
): T | undefined {
Expand All @@ -487,19 +489,43 @@ export function findAccessibleWindow<
matchMode === "wayland" && captured.clientBounds
? [captured.bounds, captured.clientBounds]
: [captured.bounds];
const matches = windows.filter((window) => {
const candidates = windows.filter((window) => {
const bounds = window.bounds;
return (
titles.has(normalizeTitle(window.name ?? "")) &&
bounds !== null &&
candidateBounds.some((candidate) =>
boundsKeys.every((key) => Math.abs(bounds[key] - candidate[key]) <= 2),
)
);
});
const matches = candidates.filter((window) => titles.has(normalizeTitle(window.name ?? "")));
if (matches.length === 1) return matches[0];
const activeMatches = matches.filter((window) => safeProperty(() => window.active) === true);
return activeMatches.length === 1 ? activeMatches[0] : undefined;
if (matches.length > 1) {
const activeMatches = matches.filter((window) => safeProperty(() => window.active) === true);
return activeMatches.length === 1 ? activeMatches[0] : undefined;
}

// Chrome's macOS AX title can append the browser and profile to the captured page title.
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 = candidates.filter((window) => {
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>> = {
Expand Down
Loading