fix(frontend): reuse clipboard fallback for Lark auth (#4767)

This commit is contained in:
Ryker_Feng 2026-08-11 21:08:52 +08:00 committed by GitHub
parent 23695a07a6
commit 9ba04bf80c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 97 additions and 3 deletions

View File

@ -25,6 +25,7 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useAuth } from "@/core/auth/AuthProvider"; import { useAuth } from "@/core/auth/AuthProvider";
import { writeTextToClipboard } from "@/core/clipboard";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { import {
larkIntegrationQueryKey, larkIntegrationQueryKey,
@ -594,10 +595,10 @@ function LarkIntegrationCard() {
const handleCopyAuthLink = async () => { const handleCopyAuthLink = async () => {
if (!pendingFlow) return; if (!pendingFlow) return;
try { const didCopy = await writeTextToClipboard(pendingFlow.verification_url);
await navigator.clipboard.writeText(pendingFlow.verification_url); if (didCopy) {
toast.success(t.clipboard.copiedToClipboard); toast.success(t.clipboard.copiedToClipboard);
} catch { } else {
toast.error(t.clipboard.failedToCopyToClipboard); toast.error(t.clipboard.failedToCopyToClipboard);
} }
}; };

View File

@ -48,6 +48,99 @@ test.describe("Integrations settings", () => {
await expect(dialog.getByText("Lark / Feishu CLI")).toBeVisible(); await expect(dialog.getByText("Lark / Feishu CLI")).toBeVisible();
}); });
test("falls back when copying a Lark authorization link without the Clipboard API", async ({
page,
}) => {
await page.addInitScript(() => {
Object.defineProperty(document, "execCommand", {
configurable: true,
value: (command: string) => {
if (command !== "copy") return false;
const copiedText =
document.querySelector<HTMLTextAreaElement>(
"textarea[readonly]",
)?.value;
(window as typeof window & { __copiedText?: string }).__copiedText =
copiedText;
return true;
},
});
});
mockLangGraphAPI(page);
const configuredStatus = configuredLarkStatus();
await page.route("**/api/integrations/lark/status", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
...configuredStatus,
auth: {
status: "not_authorized",
message: "Lark user authorization is not configured",
user: "existing-user",
verified: false,
},
}),
});
});
await page.route("**/api/integrations/lark/auth/start", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
verification_url: "about:blank#lark-auth-copy-fallback",
device_code: "copy-fallback-device-code",
generation: "copy-fallback-generation",
expires_in: 600,
user_code: null,
hint: null,
}),
});
});
await page.route(
"**/api/integrations/lark/auth/complete",
async (route) => {
await route.fulfill({
status: 504,
contentType: "application/json",
body: JSON.stringify({ detail: "Authorization still pending." }),
});
},
);
await page.goto("/workspace/chats/new?settings=integrations");
const dialog = page.getByRole("dialog", { name: "Settings" });
const popupPromise = page.waitForEvent("popup");
await dialog.getByRole("button", { name: "Connect Lark" }).click();
const popup = await popupPromise;
await expect(
dialog.getByText("about:blank#lark-auth-copy-fallback"),
).toBeVisible();
await popup.close();
// The app installs a compatibility shim during startup. Remove it here to
// model environments where Clipboard API access disappears at copy time.
await page.evaluate(() => {
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: undefined,
});
});
await dialog.getByRole("button", { name: "Copy link" }).click();
await expect
.poll(() =>
page.evaluate(
() =>
(window as typeof window & { __copiedText?: string }).__copiedText,
),
)
.toBe("about:blank#lark-auth-copy-fallback");
await expect(page.getByText("Copied to clipboard")).toBeVisible();
});
test("keeps a single settings dialog across deep link and nav menu openings", async ({ test("keeps a single settings dialog across deep link and nav menu openings", async ({
page, page,
}) => { }) => {