mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site): copy token value from modal (#25399)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user