mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Polish onboarding runtime error states (#2116)
Signed-off-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
co-authored by
npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3
Wes
Pinky
parent
820d023305
commit
31090b186f
@@ -0,0 +1,49 @@
|
|||||||
|
import { AlertTriangle } from "lucide-react";
|
||||||
|
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||||
|
|
||||||
|
type RuntimeErrorTooltipProps = {
|
||||||
|
className?: string;
|
||||||
|
detail: string;
|
||||||
|
label: string;
|
||||||
|
showIcon?: boolean;
|
||||||
|
testId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RuntimeErrorTooltip({
|
||||||
|
className,
|
||||||
|
detail,
|
||||||
|
label,
|
||||||
|
showIcon = false,
|
||||||
|
testId,
|
||||||
|
}: RuntimeErrorTooltipProps) {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span
|
||||||
|
aria-label={`${label}. ${detail}`}
|
||||||
|
className={className}
|
||||||
|
data-testid={testId}
|
||||||
|
role="status"
|
||||||
|
// biome-ignore lint/a11y/noNoninteractiveTabindex: Focus exposes the error tooltip to keyboard users without adding a nested card action
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{showIcon ? (
|
||||||
|
<AlertTriangle
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-3.5 w-3.5 shrink-0"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<span className="min-w-0 truncate">{label}</span>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="max-w-80 text-left"
|
||||||
|
side="bottom"
|
||||||
|
sideOffset={12}
|
||||||
|
>
|
||||||
|
<span className="leading-4">{detail}</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
runtimeCanBeSelected,
|
runtimeCanBeSelected,
|
||||||
} from "./onboardingRuntimeSelection";
|
} from "./onboardingRuntimeSelection";
|
||||||
import { ONBOARDING_PRIMARY_CTA_CLASS } from "./OnboardingChrome";
|
import { ONBOARDING_PRIMARY_CTA_CLASS } from "./OnboardingChrome";
|
||||||
|
import { RuntimeErrorTooltip } from "./RuntimeErrorTooltip";
|
||||||
import { OnboardingFooter } from "./OnboardingFooter";
|
import { OnboardingFooter } from "./OnboardingFooter";
|
||||||
import { getRuntimeDisplayLabel, RuntimeIcon } from "./RuntimeIcon";
|
import { getRuntimeDisplayLabel, RuntimeIcon } from "./RuntimeIcon";
|
||||||
import {
|
import {
|
||||||
@@ -153,6 +154,7 @@ function RuntimeStatus({
|
|||||||
runtime.authStatus.status === "logged_out",
|
runtime.authStatus.status === "logged_out",
|
||||||
});
|
});
|
||||||
const connectMutation = useConnectAcpRuntimeMutation();
|
const connectMutation = useConnectAcpRuntimeMutation();
|
||||||
|
const runtimesQuery = useAcpRuntimesQuery();
|
||||||
const authMethods = getOnboardingAuthMethods(
|
const authMethods = getOnboardingAuthMethods(
|
||||||
runtime,
|
runtime,
|
||||||
methodsQuery.data?.methods ?? [],
|
methodsQuery.data?.methods ?? [],
|
||||||
@@ -197,14 +199,18 @@ function RuntimeStatus({
|
|||||||
SET UP
|
SET UP
|
||||||
</Button>
|
</Button>
|
||||||
{methodsQuery.error instanceof Error ? (
|
{methodsQuery.error instanceof Error ? (
|
||||||
<span className="text-2xs text-destructive">
|
<RuntimeErrorTooltip
|
||||||
Couldn’t load sign-in options.
|
className="absolute inset-x-3 bottom-2 truncate text-xs leading-4 text-destructive"
|
||||||
</span>
|
detail="Couldn’t load sign-in options."
|
||||||
|
label="Sign-in unavailable"
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{connectMutation.error instanceof Error ? (
|
{connectMutation.error instanceof Error ? (
|
||||||
<span className="text-2xs text-destructive">
|
<RuntimeErrorTooltip
|
||||||
{connectMutation.error.message}
|
className="absolute inset-x-3 bottom-2 truncate text-xs leading-4 text-destructive"
|
||||||
</span>
|
detail="Couldn’t start sign-in. Try again."
|
||||||
|
label="Sign-in failed"
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -223,11 +229,44 @@ function RuntimeStatus({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (installError) {
|
if (
|
||||||
|
runtime.availability === "available" &&
|
||||||
|
runtime.authStatus.status === "unknown"
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-8 w-8 items-center justify-center">
|
<Button
|
||||||
<AlertTriangle className="h-4 w-4 text-destructive" />
|
aria-label={`Check ${runtime.label} again`}
|
||||||
</div>
|
className="buzz-onboarding-runtime-setup h-5 rounded-full bg-[var(--buzz-welcome-chartreuse)]/30 px-2.5 font-mono !text-badge font-normal uppercase text-foreground hover:bg-[var(--buzz-welcome-chartreuse)]/40"
|
||||||
|
disabled={runtimesQuery.isFetching}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
void runtimesQuery.refetch();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
{runtimesQuery.isFetching ? "CHECKING…" : "CHECK AGAIN"}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installError && runtime.canAutoInstall) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
aria-label={`Retry ${runtime.label} setup`}
|
||||||
|
className="buzz-onboarding-runtime-setup h-5 rounded-full bg-[var(--buzz-welcome-chartreuse)]/30 px-2.5 font-mono !text-badge font-normal uppercase text-foreground hover:bg-[var(--buzz-welcome-chartreuse)]/40"
|
||||||
|
data-setup-flash={isSetupFlashing ? "true" : undefined}
|
||||||
|
data-testid={`onboarding-runtime-install-${runtime.id}`}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onSelect();
|
||||||
|
onInstall();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
SET UP
|
||||||
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,7 +441,7 @@ function runtimeDetailText(runtime: AcpRuntimeCatalogEntry): string {
|
|||||||
if (runtime.availability === "cli_missing") {
|
if (runtime.availability === "cli_missing") {
|
||||||
return "ACP adapter detected; CLI missing.";
|
return "ACP adapter detected; CLI missing.";
|
||||||
}
|
}
|
||||||
return "Not installed yet.";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSupportedOnboardingAuthMethod(
|
function isSupportedOnboardingAuthMethod(
|
||||||
@@ -452,36 +491,26 @@ function getOnboardingAuthMethods(
|
|||||||
return supported;
|
return supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RuntimeAuthActions({ runtime }: { runtime: AcpRuntimeCatalogEntry }) {
|
function RuntimeAuthError({ runtime }: { runtime: AcpRuntimeCatalogEntry }) {
|
||||||
const runtimesQuery = useAcpRuntimesQuery();
|
|
||||||
|
|
||||||
if (runtime.authStatus.status === "config_invalid") {
|
if (runtime.authStatus.status === "config_invalid") {
|
||||||
return (
|
return (
|
||||||
<p className="mt-2 text-2xs leading-4 text-destructive">
|
<RuntimeErrorTooltip
|
||||||
{runtime.authStatus.diagnostic}
|
className="absolute inset-x-3 bottom-2 truncate text-xs leading-4 text-destructive"
|
||||||
</p>
|
detail="Check this runtime’s configuration and try again."
|
||||||
|
label="Configuration invalid"
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (runtime.authStatus.status === "unknown") {
|
if (
|
||||||
|
runtime.availability === "available" &&
|
||||||
|
runtime.authStatus.status === "unknown"
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 flex flex-col items-center gap-1.5">
|
<RuntimeErrorTooltip
|
||||||
<span className="text-2xs text-muted-foreground">
|
className="absolute inset-x-3 bottom-2 truncate text-xs leading-4 text-destructive"
|
||||||
Couldn’t verify authentication.
|
detail="Couldn’t verify authentication."
|
||||||
</span>
|
label="Status unavailable"
|
||||||
<Button
|
/>
|
||||||
disabled={runtimesQuery.isFetching}
|
|
||||||
className="h-6 rounded-full px-2 text-2xs"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
void runtimesQuery.refetch();
|
|
||||||
}}
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
{runtimesQuery.isFetching ? "Checking…" : "Check again"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -552,18 +581,29 @@ function RuntimeCard({
|
|||||||
runtime={runtime}
|
runtime={runtime}
|
||||||
setupFlashToken={setupFlashToken}
|
setupFlashToken={setupFlashToken}
|
||||||
/>
|
/>
|
||||||
{!isAvailable && !installError ? (
|
{!isAvailable && runtimeDetailText(runtime) ? (
|
||||||
<p className="max-w-[13rem] text-2xs leading-4 text-muted-foreground">
|
<p
|
||||||
|
aria-hidden={installError ? "true" : undefined}
|
||||||
|
className={cn(
|
||||||
|
"max-w-[13rem] text-2xs leading-4 text-muted-foreground",
|
||||||
|
installError && "invisible",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{runtimeDetailText(runtime)}
|
{runtimeDetailText(runtime)}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{installError ? (
|
|
||||||
<p className="max-w-[13rem] text-2xs leading-4 text-destructive">
|
|
||||||
{installError}
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
<RuntimeAuthActions runtime={runtime} />
|
|
||||||
</div>
|
</div>
|
||||||
|
{installError ? (
|
||||||
|
<RuntimeErrorTooltip
|
||||||
|
className="absolute inset-x-3 bottom-2 flex min-w-0 items-center justify-center gap-1.5 overflow-hidden whitespace-nowrap text-xs leading-4 text-destructive"
|
||||||
|
detail="Setup couldn’t be completed. Try again."
|
||||||
|
label="Setup failed"
|
||||||
|
showIcon
|
||||||
|
testId={`onboarding-runtime-error-${runtime.id}`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<RuntimeAuthError runtime={runtime} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ type E2eConfig = {
|
|||||||
acpRuntimesCatalog?: RawAcpRuntimeCatalogEntry[];
|
acpRuntimesCatalog?: RawAcpRuntimeCatalogEntry[];
|
||||||
acpRuntimesDelayMs?: number;
|
acpRuntimesDelayMs?: number;
|
||||||
acpAuthMethods?: Record<string, RawAcpAuthMethodsResult>;
|
acpAuthMethods?: Record<string, RawAcpAuthMethodsResult>;
|
||||||
|
acpAuthMethodsError?: string;
|
||||||
connectAcpRuntimeResult?: RawConnectAcpRuntimeResult;
|
connectAcpRuntimeResult?: RawConnectAcpRuntimeResult;
|
||||||
connectAcpRuntimeDelayMs?: number;
|
connectAcpRuntimeDelayMs?: number;
|
||||||
connectAcpRuntimeError?: string;
|
connectAcpRuntimeError?: string;
|
||||||
@@ -6699,6 +6700,10 @@ async function handleDiscoverAcpAuthMethods(
|
|||||||
args: { runtimeId?: string },
|
args: { runtimeId?: string },
|
||||||
config: E2eConfig | undefined,
|
config: E2eConfig | undefined,
|
||||||
): Promise<RawAcpAuthMethodsResult> {
|
): Promise<RawAcpAuthMethodsResult> {
|
||||||
|
const error = config?.mock?.acpAuthMethodsError;
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
const runtimeId = args.runtimeId ?? "";
|
const runtimeId = args.runtimeId ?? "";
|
||||||
const configured = config?.mock?.acpAuthMethods?.[runtimeId];
|
const configured = config?.mock?.acpAuthMethods?.[runtimeId];
|
||||||
if (configured) {
|
if (configured) {
|
||||||
|
|||||||
@@ -636,13 +636,35 @@ for (const authStatus of [
|
|||||||
await setupButton.click();
|
await setupButton.click();
|
||||||
await expect(card).toHaveAttribute("aria-checked", "true");
|
await expect(card).toHaveAttribute("aria-checked", "true");
|
||||||
} else if (authStatus.status === "unknown") {
|
} else if (authStatus.status === "unknown") {
|
||||||
|
const error = card.getByRole("status", {
|
||||||
|
name: /Status unavailable/,
|
||||||
|
});
|
||||||
|
await expect(error).toBeVisible();
|
||||||
|
await expect(error).toHaveCSS("font-size", "12px");
|
||||||
|
await expect(error).toHaveCSS("position", "absolute");
|
||||||
|
await error.hover();
|
||||||
|
await expect(page.getByRole("tooltip")).toHaveText(
|
||||||
|
"Couldn’t verify authentication.",
|
||||||
|
);
|
||||||
await expect(
|
await expect(
|
||||||
card.getByText("Couldn’t verify authentication"),
|
card.getByRole("button", { name: "Check Claude again" }),
|
||||||
).toBeVisible();
|
).toHaveText("CHECK AGAIN");
|
||||||
await card.click();
|
await card.click();
|
||||||
await expect(card).toHaveAttribute("aria-checked", "true");
|
await expect(card).toHaveAttribute("aria-checked", "true");
|
||||||
} else {
|
} else {
|
||||||
await expect(card.getByText("Fix Claude config")).toBeVisible();
|
const error = card.getByRole("status", {
|
||||||
|
name: /Configuration invalid/,
|
||||||
|
});
|
||||||
|
await expect(error).toBeVisible();
|
||||||
|
await expect(error).toHaveCSS("font-size", "12px");
|
||||||
|
await expect(error).toHaveCSS("position", "absolute");
|
||||||
|
await error.hover();
|
||||||
|
await expect(page.getByRole("tooltip")).toHaveText(
|
||||||
|
"Check this runtime’s configuration and try again.",
|
||||||
|
);
|
||||||
|
await expect(page.getByRole("tooltip")).not.toContainText(
|
||||||
|
"Fix Claude config",
|
||||||
|
);
|
||||||
await card.click();
|
await card.click();
|
||||||
await expect(card).toHaveAttribute("aria-checked", "true");
|
await expect(card).toHaveAttribute("aria-checked", "true");
|
||||||
}
|
}
|
||||||
@@ -678,6 +700,199 @@ test("setup cards only show checks after user selection", async ({ page }) => {
|
|||||||
).toHaveText("INSTALLED");
|
).toHaveText("INSTALLED");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("unavailable sign-in options use the compact error and tooltip pattern", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const discoveryError = "Auth discovery failed with sensitive details";
|
||||||
|
await installMockBridge(
|
||||||
|
page,
|
||||||
|
{
|
||||||
|
acpRuntimesCatalog: [
|
||||||
|
availableRuntime("claude", { status: "logged_out" }),
|
||||||
|
],
|
||||||
|
acpAuthMethodsError: discoveryError,
|
||||||
|
},
|
||||||
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
||||||
|
);
|
||||||
|
await page.goto("/");
|
||||||
|
await navigateToSetupPage(page);
|
||||||
|
|
||||||
|
const card = page.getByTestId("onboarding-runtime-claude");
|
||||||
|
const setupButton = card.getByTestId(
|
||||||
|
"onboarding-runtime-instructions-claude",
|
||||||
|
);
|
||||||
|
const error = card.getByRole("status", { name: /Sign-in unavailable/ });
|
||||||
|
await expect(error).toBeVisible();
|
||||||
|
await expect(error).toHaveCSS("font-size", "12px");
|
||||||
|
await expect(error).toHaveCSS("position", "absolute");
|
||||||
|
await expect(error).toHaveCSS("white-space", "nowrap");
|
||||||
|
await expect(error).not.toHaveAttribute("title");
|
||||||
|
await error.hover();
|
||||||
|
await expect(page.getByRole("tooltip")).toHaveText(
|
||||||
|
"Couldn’t load sign-in options.",
|
||||||
|
);
|
||||||
|
await expect(page.getByRole("tooltip")).not.toContainText(discoveryError);
|
||||||
|
await expect(setupButton).toBeVisible();
|
||||||
|
await expect(setupButton).toHaveText("SET UP");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("failed sign-in uses the compact error and tooltip pattern", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const connectionError = "Terminal launch failed with sensitive details";
|
||||||
|
await installMockBridge(
|
||||||
|
page,
|
||||||
|
{
|
||||||
|
acpRuntimesCatalog: [
|
||||||
|
availableRuntime("claude", { status: "logged_out" }),
|
||||||
|
],
|
||||||
|
acpAuthMethods: {
|
||||||
|
claude: {
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
id: "login",
|
||||||
|
name: "Sign in",
|
||||||
|
description: null,
|
||||||
|
type: "terminal",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
connectAcpRuntimeError: connectionError,
|
||||||
|
},
|
||||||
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
||||||
|
);
|
||||||
|
await page.goto("/");
|
||||||
|
await navigateToSetupPage(page);
|
||||||
|
|
||||||
|
const card = page.getByTestId("onboarding-runtime-claude");
|
||||||
|
const setupButton = card.getByTestId(
|
||||||
|
"onboarding-runtime-instructions-claude",
|
||||||
|
);
|
||||||
|
const heading = card.getByRole("heading", { name: "Claude" });
|
||||||
|
const headingTopBefore = await heading.evaluate(
|
||||||
|
(element) => element.getBoundingClientRect().top,
|
||||||
|
);
|
||||||
|
const setupTopBefore = await setupButton.evaluate(
|
||||||
|
(element) => element.getBoundingClientRect().top,
|
||||||
|
);
|
||||||
|
|
||||||
|
await setupButton.click();
|
||||||
|
|
||||||
|
const error = card.getByRole("status", { name: /Sign-in failed/ });
|
||||||
|
await expect(error).toBeVisible();
|
||||||
|
await expect(error).toHaveCSS("font-size", "12px");
|
||||||
|
await expect(error).toHaveCSS("position", "absolute");
|
||||||
|
await expect(error).toHaveCSS("white-space", "nowrap");
|
||||||
|
await expect(error).not.toHaveAttribute("title");
|
||||||
|
await error.hover();
|
||||||
|
await expect(page.getByRole("tooltip")).toHaveText(
|
||||||
|
"Couldn’t start sign-in. Try again.",
|
||||||
|
);
|
||||||
|
await expect(page.getByRole("tooltip")).not.toContainText(connectionError);
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await expect(page.getByRole("tooltip")).toBeHidden();
|
||||||
|
await error.focus();
|
||||||
|
const tooltip = page.getByRole("tooltip");
|
||||||
|
await expect(tooltip).toHaveText("Couldn’t start sign-in. Try again.");
|
||||||
|
await expect(error).toHaveAccessibleName(
|
||||||
|
"Sign-in failed. Couldn’t start sign-in. Try again.",
|
||||||
|
);
|
||||||
|
const [cardBox, setupBox, tooltipBox] = await Promise.all([
|
||||||
|
card.boundingBox(),
|
||||||
|
setupButton.boundingBox(),
|
||||||
|
tooltip.boundingBox(),
|
||||||
|
]);
|
||||||
|
expect(cardBox).not.toBeNull();
|
||||||
|
expect(setupBox).not.toBeNull();
|
||||||
|
expect(tooltipBox).not.toBeNull();
|
||||||
|
expect(tooltipBox?.y).toBeGreaterThanOrEqual(
|
||||||
|
(cardBox?.y ?? 0) + (cardBox?.height ?? 0),
|
||||||
|
);
|
||||||
|
expect(tooltipBox?.y).toBeGreaterThanOrEqual(
|
||||||
|
(setupBox?.y ?? 0) + (setupBox?.height ?? 0),
|
||||||
|
);
|
||||||
|
await expect(setupButton).toBeVisible();
|
||||||
|
expect(
|
||||||
|
await heading.evaluate((element) => element.getBoundingClientRect().top),
|
||||||
|
).toBe(headingTopBefore);
|
||||||
|
expect(
|
||||||
|
await setupButton.evaluate(
|
||||||
|
(element) => element.getBoundingClientRect().top,
|
||||||
|
),
|
||||||
|
).toBe(setupTopBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("failed install pins a single-line 12px error without moving card content", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const installError = "Install already in progress with additional details";
|
||||||
|
await installMockBridge(
|
||||||
|
page,
|
||||||
|
{
|
||||||
|
installAcpRuntimeResult: {
|
||||||
|
success: false,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
step: "Install adapter",
|
||||||
|
command: "npm install adapter",
|
||||||
|
success: false,
|
||||||
|
stdout: "",
|
||||||
|
stderr: installError,
|
||||||
|
exit_code: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
||||||
|
);
|
||||||
|
await page.goto("/");
|
||||||
|
await navigateToSetupPage(page);
|
||||||
|
|
||||||
|
const card = page.getByTestId("onboarding-runtime-claude");
|
||||||
|
const setupButton = page.getByTestId("onboarding-runtime-install-claude");
|
||||||
|
const heading = card.getByRole("heading", { name: "Claude" });
|
||||||
|
const headingTopBefore = await heading.evaluate(
|
||||||
|
(element) => element.getBoundingClientRect().top,
|
||||||
|
);
|
||||||
|
const detail = card.getByText("CLI detected; ACP adapter missing.");
|
||||||
|
const detailTopBefore = await detail.evaluate(
|
||||||
|
(element) => element.getBoundingClientRect().top,
|
||||||
|
);
|
||||||
|
const setupTopBefore = await setupButton.evaluate(
|
||||||
|
(element) => element.getBoundingClientRect().top,
|
||||||
|
);
|
||||||
|
|
||||||
|
await setupButton.click();
|
||||||
|
|
||||||
|
const error = page.getByTestId("onboarding-runtime-error-claude");
|
||||||
|
await expect(error).toBeVisible();
|
||||||
|
await expect(error).toHaveText(/Setup failed/);
|
||||||
|
await expect(error).not.toHaveAttribute("title");
|
||||||
|
await expect(setupButton).toBeVisible();
|
||||||
|
await expect(setupButton).toHaveText("SET UP");
|
||||||
|
await expect(error).toHaveCSS("font-size", "12px");
|
||||||
|
await expect(error).toHaveCSS("position", "absolute");
|
||||||
|
await expect(error).toHaveCSS("white-space", "nowrap");
|
||||||
|
await expect(error.locator("span")).toHaveCSS("text-overflow", "ellipsis");
|
||||||
|
await error.hover();
|
||||||
|
await expect(page.getByRole("tooltip")).toHaveText(
|
||||||
|
"Setup couldn’t be completed. Try again.",
|
||||||
|
);
|
||||||
|
await expect(page.getByRole("tooltip")).not.toContainText(installError);
|
||||||
|
expect(
|
||||||
|
await heading.evaluate((element) => element.getBoundingClientRect().top),
|
||||||
|
).toBe(headingTopBefore);
|
||||||
|
expect(
|
||||||
|
await detail.evaluate((element) => element.getBoundingClientRect().top),
|
||||||
|
).toBe(detailTopBefore);
|
||||||
|
expect(
|
||||||
|
await setupButton.evaluate(
|
||||||
|
(element) => element.getBoundingClientRect().top,
|
||||||
|
),
|
||||||
|
).toBe(setupTopBefore);
|
||||||
|
});
|
||||||
|
|
||||||
test("successful install still waits for refreshed runtime readiness", async ({
|
test("successful install still waits for refreshed runtime readiness", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ type MockBridgeOptions = {
|
|||||||
acpRuntimesCatalog?: Record<string, unknown>[];
|
acpRuntimesCatalog?: Record<string, unknown>[];
|
||||||
acpRuntimesDelayMs?: number;
|
acpRuntimesDelayMs?: number;
|
||||||
acpAuthMethods?: Record<string, { methods: Record<string, unknown>[] }>;
|
acpAuthMethods?: Record<string, { methods: Record<string, unknown>[] }>;
|
||||||
|
acpAuthMethodsError?: string;
|
||||||
connectAcpRuntimeResult?: { launched: boolean };
|
connectAcpRuntimeResult?: { launched: boolean };
|
||||||
connectAcpRuntimeDelayMs?: number;
|
connectAcpRuntimeDelayMs?: number;
|
||||||
connectAcpRuntimeError?: string;
|
connectAcpRuntimeError?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user