Skip to content
Merged
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
45 changes: 44 additions & 1 deletion e2e/selfhost/connect-modal-credential-ux.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
// it reads as the header value being built ("Authorization: Bearer ▏token");
// - the "Add authentication method" editor offers placement presets;
// - a prefix with no trailing space (sent joined to the value) warns, and the
// warning clears once the space is restored.
// warning clears once the space is restored;
// - a very long pasted key wraps inside the dialog instead of stretching it
// past the viewport (the preview breaks mid-token, the merged affix
// truncates).
// Video is the artifact.
import { randomBytes } from "node:crypto";

import { expect } from "@effect/vitest";
import { Effect } from "effect";
import { composePluginApi } from "@executor-js/api/server";
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
Expand All @@ -17,6 +21,16 @@ import { Api, Browser, Target } from "../src/services";

const api = composePluginApi([openApiHttpPlugin()] as const);

/** Horizontal overflow of the open dialog in px (0 = fits). Infinity when no
* dialog is open, so an assertion against it always fails loudly. */
const dialogOverflow = (page: {
readonly evaluate: <T>(fn: () => T) => Promise<T>;
}): Promise<number> =>
page.evaluate(() => {
const dialog = document.querySelector('[role="dialog"]');
return dialog === null ? Number.POSITIVE_INFINITY : dialog.scrollWidth - dialog.clientWidth;
});

const bearerSpec = (): string =>
JSON.stringify({
openapi: "3.0.3",
Expand Down Expand Up @@ -75,6 +89,35 @@ scenario(
await page.getByPlaceholder("Bearer ").first().fill("Bearer ");
await page.getByText("Prefix has no trailing space").waitFor({ state: "detached" });
});

await step("A long pasted key never widens the dialog", async () => {
// A key mistakenly pasted into Prefix is the worst case: one
// unbroken 700-char token rendered into the preview line. The
// dialog must keep its width (the preview wraps mid-token).
const longKey = `${"x".repeat(300)}Bearer${"x".repeat(400)}`;
// The preview line renders only once the placement is named.
await page.getByPlaceholder("Authorization").first().fill("Authorization");
await page.getByPlaceholder("Bearer ").first().fill(longKey);
await page.getByText("Preview").waitFor();
// ≤ 1px: subpixel rounding can report a fractional scrollWidth.
expect(
await dialogOverflow(page),
"the add-method dialog must not scroll horizontally",
).toBeLessThanOrEqual(1);
});

await step("The merged affix truncates a long saved prefix", async () => {
// Saving the method returns to the connect modal with the new
// method selected; its credential field merges the (700-char)
// prefix as the affix, which must truncate inside the field.
await page.getByRole("button", { name: "Add method", exact: true }).click();
await page.getByRole("heading", { name: /Add connection/ }).waitFor();
await page.getByText("Authorization: x").first().waitFor();
expect(
await dialogOverflow(page),
"the connect modal must not scroll horizontally",
).toBeLessThanOrEqual(1);
});
});
}),
apiClient.openapi
Expand Down
6 changes: 4 additions & 2 deletions packages/react/src/components/add-account-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,10 @@ function PasteCredentialInputs(props: {
// from offering to GENERATE a password here; the app's own 1Password
// picker covers filling an existing secret.
<div className="flex h-9 w-full min-w-0 items-stretch overflow-hidden rounded-md border border-input bg-transparent font-mono text-sm shadow-xs transition-colors focus-within:border-ring dark:bg-input/30">
<span className="flex shrink-0 select-none items-center whitespace-pre border-r border-input bg-muted/40 px-3 text-muted-foreground/70">
{props.affix}
{/* min-w-0 + inner ellipsis: a method saved with a very long
prefix must truncate inside the field, not stretch it. */}
<span className="flex min-w-0 max-w-[60%] select-none items-center border-r border-input bg-muted/40 px-3 text-muted-foreground/70">
<span className="overflow-hidden text-ellipsis whitespace-pre">{props.affix}</span>
</span>
{/* oxlint-disable-next-line react/forbid-elements */}
<input
Expand Down
6 changes: 4 additions & 2 deletions packages/react/src/lib/auth-placements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,11 @@ export function PlacementLine(props: { readonly placement: Placement; readonly m
// each child, which would drop the space after "Authorization:" and the
// trailing space carried by a prefix like "Bearer ", rendering
// "Authorization:Bearer••••••". whitespace-pre-wrap keeps those spaces while
// still allowing the line to wrap.
// still allowing the line to wrap. wrap-anywhere lets an unbroken value (a
// long API key pasted as the prefix) break mid-token — without it the line's
// min-content width is the whole key and it stretches the dialog off-screen.
return (
<span className="whitespace-pre-wrap font-mono text-xs text-muted-foreground">
<span className="wrap-anywhere whitespace-pre-wrap font-mono text-xs text-muted-foreground">
{lead}
{placement.prefix ? (
<span className="text-muted-foreground/60">{placement.prefix}</span>
Expand Down
Loading