refactor(google-meet): tidy browser create control

This commit is contained in:
Peter Steinberger
2026-04-24 23:34:27 +01:00
parent 5c445f7842
commit 9613a0759c
2 changed files with 73 additions and 59 deletions

View File

@@ -74,6 +74,39 @@ function captureStdout() {
};
}
async function runCreateMeetBrowserScript(params: { buttonText: string }) {
const location = {
href: "https://meet.google.com/new",
hostname: "meet.google.com",
};
const button = {
disabled: false,
innerText: params.buttonText,
textContent: params.buttonText,
getAttribute: (name: string) => (name === "aria-label" ? params.buttonText : null),
click: vi.fn(() => {
location.href = "https://meet.google.com/abc-defg-hij";
}),
};
const document = {
title: "Meet",
body: {
innerText: "Do you want people to hear you in the meeting?",
textContent: "Do you want people to hear you in the meeting?",
},
querySelectorAll: (selector: string) => (selector === "button" ? [button] : []),
};
vi.stubGlobal("document", document);
vi.stubGlobal("location", location);
const fn = (0, eval)(`(${CREATE_MEET_FROM_BROWSER_SCRIPT})`) as () => Promise<{
meetingUri?: string;
manualActionReason?: string;
notes?: string[];
retryAfterMs?: number;
}>;
return { button, result: await fn() };
}
type TestBridgeProcess = {
stdin?: { write(chunk: unknown): unknown } | null;
stdout?: { on(event: "data", listener: (chunk: unknown) => void): unknown } | null;
@@ -853,50 +886,15 @@ describe("google-meet plugin", () => {
])(
"uses browser automation for Meet's %s choice during browser creation",
async (buttonText, note) => {
const location = {
href: "https://meet.google.com/new",
hostname: "meet.google.com",
};
const button = {
disabled: false,
innerText: buttonText,
textContent: buttonText,
getAttribute: (name: string) => (name === "aria-label" ? buttonText : null),
click: vi.fn(() => {
location.href = "https://meet.google.com/abc-defg-hij";
}),
};
const document = {
title: "Meet",
body: {
innerText: "Do you want people to hear you in the meeting?",
textContent: "Do you want people to hear you in the meeting?",
},
querySelectorAll: (selector: string) => (selector === "button" ? [button] : []),
};
vi.stubGlobal("document", document);
vi.stubGlobal("location", location);
vi.useFakeTimers();
const { button, result } = await runCreateMeetBrowserScript({ buttonText });
try {
const fn = (0, eval)(`(${CREATE_MEET_FROM_BROWSER_SCRIPT})`) as () => Promise<{
meetingUri?: string;
manualActionReason?: string;
notes?: string[];
retryAfterMs?: number;
}>;
const result = await fn();
expect(result).toMatchObject({
retryAfterMs: 1000,
notes: [note],
});
expect(button.click).toHaveBeenCalledTimes(1);
expect(result.meetingUri).toBeUndefined();
expect(result.manualActionReason).toBeUndefined();
} finally {
vi.useRealTimers();
}
expect(result).toMatchObject({
retryAfterMs: 1000,
notes: [note],
});
expect(button.click).toHaveBeenCalledTimes(1);
expect(result.meetingUri).toBeUndefined();
expect(result.manualActionReason).toBeUndefined();
},
);

View File

@@ -231,6 +231,20 @@ type BrowserTab = {
url?: string;
};
type BrowserCreateStepResult = {
meetingUri?: string;
browserUrl?: string;
browserTitle?: string;
manualAction?: string;
manualActionReason?: GoogleMeetChromeHealth["manualActionReason"];
notes?: string[];
retryAfterMs?: number;
};
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function formatBrowserAutomationError(error: unknown): string {
if (error instanceof Error) {
return error.message;
@@ -242,6 +256,12 @@ function formatBrowserAutomationError(error: unknown): string {
}
}
function isBrowserNavigationInterruption(error: unknown): boolean {
return /execution context was destroyed|navigation|target closed/i.test(
formatBrowserAutomationError(error),
);
}
export type GoogleMeetBrowserCreateResult = {
meetingUri: string;
nodeId: string;
@@ -304,15 +324,13 @@ function readBrowserTab(result: unknown): BrowserTab | undefined {
return result && typeof result === "object" ? (result as BrowserTab) : undefined;
}
function readBrowserCreateResult(result: unknown): {
meetingUri?: string;
browserUrl?: string;
browserTitle?: string;
manualAction?: string;
manualActionReason?: GoogleMeetChromeHealth["manualActionReason"];
notes?: string[];
retryAfterMs?: number;
} {
function readStringArray(value: unknown): string[] | undefined {
return Array.isArray(value)
? value.filter((entry): entry is string => typeof entry === "string")
: undefined;
}
function readBrowserCreateResult(result: unknown): BrowserCreateStepResult {
const record = result && typeof result === "object" ? (result as Record<string, unknown>) : {};
const nested =
record.result && typeof record.result === "object"
@@ -327,9 +345,7 @@ function readBrowserCreateResult(result: unknown): {
typeof nested.manualActionReason === "string"
? (nested.manualActionReason as GoogleMeetChromeHealth["manualActionReason"])
: undefined,
notes: Array.isArray(nested.notes)
? nested.notes.filter((note): note is string => typeof note === "string")
: undefined,
notes: readStringArray(nested.notes),
retryAfterMs:
typeof nested.retryAfterMs === "number" && Number.isFinite(nested.retryAfterMs)
? nested.retryAfterMs
@@ -454,7 +470,7 @@ export async function createMeetWithBrowserProxyOnNode(params: {
throw new Error("Browser fallback opened Google Meet but did not return a targetId.");
}
const notes = new Set<string>();
let lastResult: ReturnType<typeof readBrowserCreateResult> | undefined;
let lastResult: BrowserCreateStepResult | undefined;
let lastError: unknown;
const deadline = Date.now() + timeoutMs;
while (Date.now() <= deadline) {
@@ -493,13 +509,13 @@ export async function createMeetWithBrowserProxyOnNode(params: {
}
throw new Error(result.manualAction);
}
await new Promise((resolve) => setTimeout(resolve, result.retryAfterMs ?? 500));
await sleep(result.retryAfterMs ?? 500);
} catch (error) {
lastError = error;
if (!/execution context was destroyed|navigation|target closed/i.test(String(error))) {
if (!isBrowserNavigationInterruption(error)) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, 1_000));
await sleep(1_000);
}
}
throw new Error(