fix(site): copy token value from modal (#25399)

This commit is contained in:
Matt Vollmer
2026-05-15 16:52:55 -04:00
committed by GitHub
parent 2b612abe7b
commit d9976768db
4 changed files with 87 additions and 7 deletions
@@ -86,17 +86,17 @@ export const CodeExample: FC<CodeExampleProps> = ({
)}
</code>
<div className="flex items-center gap-1">
<div className="flex items-center gap-1 select-none">
{showRevealButton && redactPattern && !secret && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="subtle"
aria-label={showButtonLabel}
onClick={() => setShowFullValue(!showFullValue)}
>
{icon}
<span className="sr-only">{showButtonLabel}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{showButtonLabel}</TooltipContent>
@@ -29,11 +29,11 @@ export const CopyButton: FC<CopyButtonProps> = ({
<Button
size="icon"
variant="subtle"
aria-label={label}
onClick={() => copyToClipboard(text)}
{...buttonProps}
>
{showCopiedSuccess ? <CheckIcon /> : <CopyIcon />}
<span className="sr-only">{label}</span>
</Button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>{label}</TooltipContent>
+10 -1
View File
@@ -3,6 +3,7 @@ import { toast } from "sonner";
const CLIPBOARD_TIMEOUT_MS = 1_000;
export const COPY_FAILED_MESSAGE = "Failed to copy text to clipboard";
const DIALOG_SELECTOR = 'dialog[open], [role="dialog"], [role="alertdialog"]';
export const HTTP_FALLBACK_DATA_ID = "http-fallback";
export type UseClipboardInput = Readonly<{
@@ -99,6 +100,14 @@ export const useClipboard = (
function simulateClipboardWrite(textToCopy: string): boolean {
const previousFocusTarget = document.activeElement;
const dummyInput = document.createElement("input");
// Keep the dummy input inside an open dialog so focus traps allow
// execCommand("copy") to select it.
const activeDialog =
previousFocusTarget instanceof HTMLElement
? previousFocusTarget.closest(DIALOG_SELECTOR)
: undefined;
const dummyInputContainer =
activeDialog ?? document.querySelector(DIALOG_SELECTOR) ?? document.body;
// Have to add test ID to dummy element for mocking purposes in tests
dummyInput.setAttribute("data-testid", HTTP_FALLBACK_DATA_ID);
@@ -119,7 +128,7 @@ function simulateClipboardWrite(textToCopy: string): boolean {
style.padding = "0";
style.border = "0";
document.body.appendChild(dummyInput);
dummyInputContainer.appendChild(dummyInput);
dummyInput.value = textToCopy;
dummyInput.focus();
dummyInput.select();
@@ -1,19 +1,31 @@
import { screen, within } from "@testing-library/react";
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { API } from "#/api/api";
import { HTTP_FALLBACK_DATA_ID } from "#/hooks/useClipboard";
import {
renderWithAuth,
waitForLoaderToBeRemoved,
} from "#/testHelpers/renderHelpers";
import CreateTokenPage from "./CreateTokenPage";
import { NANO_HOUR } from "./utils";
describe("TokenPage", () => {
it("shows the success modal", async () => {
const originalExecCommand = document.execCommand;
const originalNavigator = navigator;
afterEach(() => {
document.execCommand = originalExecCommand;
vi.restoreAllMocks();
});
const createToken = async () => {
vi.spyOn(API, "getTokenConfig").mockResolvedValue({
max_token_lifetime: 90 * 24 * NANO_HOUR,
});
vi.spyOn(API, "createToken").mockResolvedValueOnce({
key: "abcd",
});
// When
const { container } = renderWithAuth(<CreateTokenPage />, {
route: "/settings/tokens/new",
path: "/settings/tokens/new",
@@ -25,8 +37,67 @@ describe("TokenPage", () => {
await userEvent.click(
within(form).getByRole("button", { name: /create token/i }),
);
};
it("shows the success modal", async () => {
// When
await createToken();
// Then
expect(screen.getByText("abcd")).toBeInTheDocument();
});
it("selects only the created token from the success modal", async () => {
await createToken();
const tokenContainer = screen.getByText("abcd").closest("div");
expect(tokenContainer).toBeInTheDocument();
const selectedRange = document.createRange();
selectedRange.selectNodeContents(tokenContainer as HTMLElement);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(selectedRange);
expect(selection?.toString().trim()).toBe("abcd");
selection?.removeAllRanges();
});
it("copies the created token from the success modal when clipboard fallback is used", async () => {
const mockClipboard: Clipboard = {
...originalNavigator.clipboard,
writeText: vi.fn().mockRejectedValue(new Error("Clipboard unavailable")),
};
vi.spyOn(window, "navigator", "get").mockImplementation(() => ({
...originalNavigator,
clipboard: mockClipboard,
}));
let copiedText = "";
document.execCommand = vi.fn((commandId) => {
const dummyInput = document.querySelector(
`input[data-testid=${HTTP_FALLBACK_DATA_ID}]`,
);
const inputCanReceiveDialogFocus =
commandId === "copy" &&
dummyInput instanceof HTMLInputElement &&
dummyInput.closest('[role="dialog"]') !== null &&
document.activeElement === dummyInput;
if (!inputCanReceiveDialogFocus) {
return false;
}
copiedText = dummyInput.value;
return true;
});
await createToken();
await userEvent.click(screen.getByRole("button", { name: "Copy code" }));
await waitFor(() => {
expect(copiedText).toBe("abcd");
});
});
});