From f2f874cdbc883bf2cd67f65fae25fe30959def4d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Almeida?=
<57680069+zeval@users.noreply.github.com>
Date: Fri, 21 Aug 2026 16:34:07 +0100
Subject: [PATCH] feat(ui): localize walkthrough discard confirmation (#2879)
---
.../review/walkthrough-overlay.test.tsx | 240 +++++++++++++++++-
.../components/review/walkthrough-overlay.tsx | 178 +++++++------
apps/web/e2e/pages/session-page.ts | 4 +-
.../tests/review/mobile-walkthrough.spec.ts | 31 ++-
apps/web/e2e/tests/review/walkthrough.spec.ts | 30 ++-
5 files changed, 370 insertions(+), 113 deletions(-)
diff --git a/apps/web/components/review/walkthrough-overlay.test.tsx b/apps/web/components/review/walkthrough-overlay.test.tsx
index 481cc4f4b6..53747204be 100644
--- a/apps/web/components/review/walkthrough-overlay.test.tsx
+++ b/apps/web/components/review/walkthrough-overlay.test.tsx
@@ -1,10 +1,11 @@
-import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
-import { beforeEach, describe, expect, it, vi } from "vitest";
+import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ConnectionStatus } from "@/lib/types/connection";
import type { TaskWalkthrough } from "@/lib/types/http";
import { StateProvider, useAppStore } from "@/components/state-provider";
import { deleteTaskWalkthrough, getTaskWalkthrough } from "@/lib/api/domains/walkthrough-api";
import { getOpenWalkthroughTaskId, setOpenWalkthroughTaskId } from "@/lib/walkthrough-open-state";
+import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
import { WalkthroughOverlay } from "./walkthrough-overlay";
vi.mock("@/lib/api/domains/walkthrough-api", () => ({
@@ -20,13 +21,37 @@ vi.mock("@/components/diff/walkthrough-floating-window", () => ({
WalkthroughFloatingWindow: () =>
,
}));
+vi.mock("@/hooks/use-responsive-breakpoint", () => ({
+ useResponsiveBreakpoint: vi.fn(),
+}));
+
const TASK_ID = "task-1";
const SECOND_TASK_ID = "task-2";
+const LAUNCHER_TEST_ID = "walkthrough-launcher";
+const FLOATING_TEST_ID = "walkthrough-floating";
+const DISCARD_BUTTON_TEST_ID = "walkthrough-discard";
+const DISCARD_CONFIRMATION_TEST_ID = "walkthrough-discard-confirmation";
+const ALERT_DIALOG_ROLE = "alertdialog";
+const CANCEL_BUTTON_NAME = "Cancel";
+const DISCARD_BUTTON_NAME = "Discard walkthrough";
-function walkthrough(): TaskWalkthrough {
+function responsive(isFinePointer: boolean): ReturnType {
return {
- id: "walkthrough-1",
- task_id: TASK_ID,
+ breakpoint: isFinePointer ? "desktop" : "mobile",
+ isMobile: !isFinePointer,
+ isTablet: false,
+ isDesktop: isFinePointer,
+ isCompactDesktop: false,
+ isFullDesktop: isFinePointer,
+ isFinePointer,
+ usesDesktopWorkbench: isFinePointer,
+ };
+}
+
+function walkthrough(taskId = TASK_ID): TaskWalkthrough {
+ return {
+ id: `walkthrough-${taskId}`,
+ task_id: taskId,
title: "Walkthrough",
created_by: "agent",
created_at: "2026-07-07T12:00:00Z",
@@ -37,10 +62,13 @@ function walkthrough(): TaskWalkthrough {
let setConnectionStatus: ((status: ConnectionStatus) => void) | null = null;
let setActiveTask: ((taskId: string) => void) | null = null;
+let setStoredWalkthrough: ((taskId: string, walkthrough: TaskWalkthrough | null) => void) | null =
+ null;
function StoreProbe() {
setConnectionStatus = useAppStore((s) => s.setConnectionStatus);
setActiveTask = useAppStore((s) => s.setActiveTask);
+ setStoredWalkthrough = useAppStore((s) => s.setWalkthrough);
return null;
}
@@ -51,6 +79,7 @@ function ActiveTaskOverlay() {
function renderOverlay({ followActiveTask = false } = {}) {
setConnectionStatus = null;
+ setStoredWalkthrough = null;
return render(
{
+ cleanup();
+ vi.unstubAllGlobals();
+});
+
describe("WalkthroughOverlay", () => {
- beforeEach(() => {
- vi.mocked(getTaskWalkthrough).mockReset();
- vi.mocked(deleteTaskWalkthrough).mockReset();
- setOpenWalkthroughTaskId(null);
- });
+ beforeEach(resetOverlayMocks);
it("waits for websocket connection before backfilling the launcher", async () => {
vi.mocked(getTaskWalkthrough).mockResolvedValue(walkthrough());
@@ -86,7 +123,7 @@ describe("WalkthroughOverlay", () => {
act(() => setConnectionStatus?.("connected"));
- expect(await screen.findByTestId("walkthrough-launcher")).toBeTruthy();
+ expect(await screen.findByTestId(LAUNCHER_TEST_ID)).toBeTruthy();
expect(getTaskWalkthrough).toHaveBeenCalledWith(TASK_ID);
});
@@ -95,12 +132,12 @@ describe("WalkthroughOverlay", () => {
renderOverlay();
await waitFor(() => expect(setConnectionStatus).toBeTruthy());
act(() => setConnectionStatus?.("connected"));
- const launcher = await screen.findByTestId("walkthrough-launcher");
+ const launcher = await screen.findByTestId(LAUNCHER_TEST_ID);
fireEvent.click(launcher);
await waitFor(() => expect(getOpenWalkthroughTaskId()).toBe(TASK_ID));
- expect(screen.getByTestId("walkthrough-floating")).toBeTruthy();
+ expect(screen.getByTestId(FLOATING_TEST_ID)).toBeTruthy();
expect(launcher.className).not.toContain("bg-accent");
const count = within(launcher).getByText("1/1");
expect(count.className).toContain("text-foreground");
@@ -117,7 +154,7 @@ describe("WalkthroughOverlay", () => {
const view = renderOverlay({ followActiveTask: true });
act(() => setConnectionStatus?.("connected"));
- fireEvent.click(await within(view.container).findByTestId("walkthrough-launcher"));
+ fireEvent.click(await within(view.container).findByTestId(LAUNCHER_TEST_ID));
await waitFor(() => expect(getOpenWalkthroughTaskId()).toBe(TASK_ID));
act(() => setActiveTask?.(SECOND_TASK_ID));
@@ -125,3 +162,178 @@ describe("WalkthroughOverlay", () => {
await waitFor(() => expect(getOpenWalkthroughTaskId()).toBeNull());
});
});
+
+describe("WalkthroughOverlay discard task binding", () => {
+ beforeEach(resetOverlayMocks);
+
+ it("keeps discard bound to the task that opened the confirmation", async () => {
+ vi.mocked(getTaskWalkthrough).mockImplementation(async (taskId) => walkthrough(taskId));
+ vi.mocked(deleteTaskWalkthrough).mockResolvedValue(undefined);
+ renderOverlay({ followActiveTask: true });
+ act(() => {
+ setStoredWalkthrough?.(SECOND_TASK_ID, walkthrough(SECOND_TASK_ID));
+ setConnectionStatus?.("connected");
+ });
+
+ await screen.findByTestId(LAUNCHER_TEST_ID);
+ fireEvent.click(screen.getByTestId(DISCARD_BUTTON_TEST_ID));
+ await screen.findByTestId(DISCARD_CONFIRMATION_TEST_ID);
+
+ act(() => setActiveTask?.(SECOND_TASK_ID));
+
+ await screen.findByTestId(LAUNCHER_TEST_ID);
+ await waitFor(() => expect(screen.queryByTestId(DISCARD_CONFIRMATION_TEST_ID)).toBeNull());
+ expect(deleteTaskWalkthrough).not.toHaveBeenCalled();
+
+ act(() => setActiveTask?.(TASK_ID));
+
+ const confirmation = await screen.findByTestId(DISCARD_CONFIRMATION_TEST_ID);
+ fireEvent.click(
+ within(confirmation).getByRole("button", {
+ name: DISCARD_BUTTON_NAME,
+ }),
+ );
+
+ await waitFor(() => expect(deleteTaskWalkthrough).toHaveBeenCalledWith(TASK_ID));
+ expect(deleteTaskWalkthrough).not.toHaveBeenCalledWith(SECOND_TASK_ID);
+ });
+});
+
+describe("WalkthroughOverlay fine-pointer discard confirmation", () => {
+ beforeEach(resetOverlayMocks);
+
+ it("confirms fine-pointer discard in an anchored non-modal shell", async () => {
+ vi.mocked(getTaskWalkthrough).mockResolvedValue(walkthrough());
+ renderOverlay();
+ act(() => setConnectionStatus?.("connected"));
+
+ const launcher = await screen.findByTestId(LAUNCHER_TEST_ID);
+ fireEvent.click(screen.getByTestId(DISCARD_BUTTON_TEST_ID));
+
+ const confirmation = await screen.findByTestId(DISCARD_CONFIRMATION_TEST_ID);
+ expect(confirmation.getAttribute("role")).toBe("dialog");
+ expect(screen.queryByRole(ALERT_DIALOG_ROLE)).toBeNull();
+ expect(launcher).toBeTruthy();
+ });
+
+ it("cancels discard without closing the walkthrough", async () => {
+ vi.mocked(getTaskWalkthrough).mockResolvedValue(walkthrough());
+ renderOverlay();
+ act(() => setConnectionStatus?.("connected"));
+
+ const launcher = await screen.findByTestId(LAUNCHER_TEST_ID);
+ fireEvent.click(launcher);
+ await waitFor(() => expect(screen.getByTestId(FLOATING_TEST_ID)).toBeTruthy());
+ fireEvent.click(screen.getByTestId(DISCARD_BUTTON_TEST_ID));
+ fireEvent.click(
+ within(screen.getByTestId(DISCARD_CONFIRMATION_TEST_ID)).getByRole("button", {
+ name: CANCEL_BUTTON_NAME,
+ }),
+ );
+
+ await waitFor(() => expect(screen.queryByTestId(DISCARD_CONFIRMATION_TEST_ID)).toBeNull());
+ expect(screen.getByTestId(FLOATING_TEST_ID)).toBeTruthy();
+ expect(getOpenWalkthroughTaskId()).toBe(TASK_ID);
+ });
+
+ it("closes the local shell before discarding once", async () => {
+ vi.mocked(getTaskWalkthrough).mockResolvedValue(walkthrough());
+ let shellClosedBeforeDelete = false;
+ vi.mocked(deleteTaskWalkthrough).mockImplementation(async () => {
+ shellClosedBeforeDelete = screen.queryByTestId(DISCARD_CONFIRMATION_TEST_ID) === null;
+ });
+ renderOverlay();
+ act(() => setConnectionStatus?.("connected"));
+
+ await screen.findByTestId(LAUNCHER_TEST_ID);
+ fireEvent.click(screen.getByTestId(DISCARD_BUTTON_TEST_ID));
+ fireEvent.click(
+ within(screen.getByTestId(DISCARD_CONFIRMATION_TEST_ID)).getByRole("button", {
+ name: DISCARD_BUTTON_NAME,
+ }),
+ );
+
+ await waitFor(() => expect(deleteTaskWalkthrough).toHaveBeenCalledTimes(1));
+ expect(shellClosedBeforeDelete).toBe(true);
+ expect(screen.queryByTestId(DISCARD_CONFIRMATION_TEST_ID)).toBeNull();
+ expect(screen.queryByTestId(LAUNCHER_TEST_ID)).toBeNull();
+ expect(screen.queryByTestId(FLOATING_TEST_ID)).toBeNull();
+ });
+});
+
+describe("WalkthroughOverlay coarse-pointer discard confirmation", () => {
+ beforeEach(resetOverlayMocks);
+
+ it("morphs coarse-pointer discard into local touch actions", async () => {
+ vi.mocked(useResponsiveBreakpoint).mockReturnValue(responsive(false));
+ vi.mocked(getTaskWalkthrough).mockResolvedValue(walkthrough());
+ renderOverlay();
+ act(() => setConnectionStatus?.("connected"));
+
+ await screen.findByTestId(LAUNCHER_TEST_ID);
+ fireEvent.click(screen.getByTestId(DISCARD_BUTTON_TEST_ID));
+
+ const confirmation = await screen.findByTestId(DISCARD_CONFIRMATION_TEST_ID);
+ expect(confirmation.getAttribute("role")).toBe("group");
+ expect(screen.queryByRole(ALERT_DIALOG_ROLE)).toBeNull();
+ expect(
+ within(confirmation).getByRole("button", { name: DISCARD_BUTTON_NAME }).className,
+ ).toContain("h-11");
+ });
+
+ it("restores focus to the coarse-pointer discard button after cancel", async () => {
+ let runAnimationFrame: FrameRequestCallback | null = null;
+ vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {
+ runAnimationFrame = callback;
+ return 0;
+ });
+ vi.mocked(useResponsiveBreakpoint).mockReturnValue(responsive(false));
+ vi.mocked(getTaskWalkthrough).mockResolvedValue(walkthrough());
+ renderOverlay();
+ act(() => setConnectionStatus?.("connected"));
+
+ await screen.findByTestId(LAUNCHER_TEST_ID);
+ fireEvent.click(screen.getByTestId(DISCARD_BUTTON_TEST_ID));
+ fireEvent.click(
+ within(await screen.findByTestId(DISCARD_CONFIRMATION_TEST_ID)).getByRole("button", {
+ name: CANCEL_BUTTON_NAME,
+ }),
+ );
+ act(() => runAnimationFrame?.(0));
+
+ expect(document.activeElement).toBe(screen.getByTestId(DISCARD_BUTTON_TEST_ID));
+ });
+});
+
+describe("WalkthroughOverlay discard mutation state", () => {
+ beforeEach(resetOverlayMocks);
+
+ it("disables repeated discard while deletion is in flight", async () => {
+ let finishDelete: (() => void) | null = null;
+ vi.mocked(getTaskWalkthrough).mockResolvedValue(walkthrough());
+ vi.mocked(deleteTaskWalkthrough).mockImplementation(
+ () =>
+ new Promise((resolve) => {
+ finishDelete = resolve;
+ }),
+ );
+ renderOverlay();
+ act(() => setConnectionStatus?.("connected"));
+
+ await screen.findByTestId(LAUNCHER_TEST_ID);
+ fireEvent.click(screen.getByTestId(DISCARD_BUTTON_TEST_ID));
+ fireEvent.click(
+ within(await screen.findByTestId(DISCARD_CONFIRMATION_TEST_ID)).getByRole("button", {
+ name: DISCARD_BUTTON_NAME,
+ }),
+ );
+
+ await waitFor(() => expect(deleteTaskWalkthrough).toHaveBeenCalledTimes(1));
+ const disabledDuringDelete = (screen.getByTestId(DISCARD_BUTTON_TEST_ID) as HTMLButtonElement)
+ .disabled;
+
+ act(() => finishDelete?.());
+ await waitFor(() => expect(screen.queryByTestId(LAUNCHER_TEST_ID)).toBeNull());
+ expect(disabledDuringDelete).toBe(true);
+ });
+});
diff --git a/apps/web/components/review/walkthrough-overlay.tsx b/apps/web/components/review/walkthrough-overlay.tsx
index dd4aef0372..6002188aad 100644
--- a/apps/web/components/review/walkthrough-overlay.tsx
+++ b/apps/web/components/review/walkthrough-overlay.tsx
@@ -1,17 +1,7 @@
"use client";
-import { useEffect, useRef, useState, type MouseEvent } from "react";
+import { useEffect, useRef, useState, type RefObject } from "react";
import { IconRoute, IconX } from "@tabler/icons-react";
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@kandev/ui/alert-dialog";
import { useAppStore } from "@/components/state-provider";
import { useToast } from "@/components/toast-provider";
import { deleteTaskWalkthrough, getTaskWalkthrough } from "@/lib/api/domains/walkthrough-api";
@@ -20,6 +10,9 @@ import { clearOpenWalkthroughTaskId, setOpenWalkthroughTaskId } from "@/lib/walk
import { cn } from "@kandev/ui/lib/utils";
import type { TaskWalkthrough } from "@/lib/types/http";
import { useTranslation } from "react-i18next";
+import { ActionConfirmPopover } from "@/components/confirmation/action-confirm-popover";
+import { InlineConfirmActions } from "@/components/confirmation/inline-confirm-actions";
+import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
type WalkthroughOverlayProps = {
/** The task whose walkthrough launcher should be shown. */
@@ -31,22 +24,40 @@ type WalkthroughOverlayProps = {
type WalkthroughLauncherProps = {
activeStep: number;
+ confirmDiscardOpen: boolean;
+ discardAnchorRef: RefObject;
+ discardDisabled: boolean;
hasUnseen: boolean;
+ isFinePointer: boolean;
isOpen: boolean;
onDiscardClick: () => void;
+ onDiscardCancel: () => void;
+ onDiscardClose: () => void;
+ onDiscardConfirm: () => void | Promise;
onToggle: () => void;
stepCount: number;
};
function WalkthroughLauncher({
activeStep,
+ confirmDiscardOpen,
+ discardAnchorRef,
+ discardDisabled,
hasUnseen,
+ isFinePointer,
isOpen,
onDiscardClick,
+ onDiscardCancel,
+ onDiscardClose,
+ onDiscardConfirm,
onToggle,
stepCount,
}: WalkthroughLauncherProps) {
const { t } = useTranslation();
+ const discardLabel = t("review:discardWalkthrough");
+ const discardVisibility = isFinePointer
+ ? "opacity-0 pointer-events-none scale-90 group-hover:pointer-events-auto group-hover:scale-100 group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:scale-100 group-focus-within:opacity-100"
+ : "";
return (
-
+ {isFinePointer || !confirmDiscardOpen ? (
+
+ ) : (
+
+
+
+ )}
);
}
-type DiscardWalkthroughDialogProps = {
- discarding: boolean;
- onConfirm: (event: MouseEvent) => void;
- onOpenChange: (open: boolean) => void;
- open: boolean;
-};
-
-function DiscardWalkthroughDialog({
- discarding,
- onConfirm,
- onOpenChange,
- open,
-}: DiscardWalkthroughDialogProps) {
- const { t } = useTranslation();
- return (
-
-
-
- {t("review:discardWalkthroughTitle")}
-
- {t("review:discardWalkthroughDescription")}
-
-
-
-
- {t("common:cancel")}
-
-
- {t("review:discardWalkthrough")}
-
-
-
-
- );
-}
-
function useWalkthroughBackfill(params: {
connectionStatus: string;
setWalkthrough: (taskId: string, walkthrough: TaskWalkthrough | null) => void;
@@ -189,6 +178,8 @@ function useWalkthroughBackfill(params: {
*/
export function WalkthroughOverlay({ taskId, onSelectFile }: WalkthroughOverlayProps) {
const { t } = useTranslation();
+ const discardLabel = t("review:discardWalkthrough");
+ const { isFinePointer } = useResponsiveBreakpoint();
const walkthrough = useAppStore((s) => (taskId ? s.walkthroughs.byTaskId[taskId] : null));
const connectionStatus = useAppStore((s) => s.connection.status);
const activeStep = useAppStore((s) =>
@@ -199,10 +190,12 @@ export function WalkthroughOverlay({ taskId, onSelectFile }: WalkthroughOverlayP
);
const setWalkthrough = useAppStore((s) => s.setWalkthrough);
const [openTaskId, setOpenTaskId] = useState(null);
- const [confirmDiscardOpen, setConfirmDiscardOpen] = useState(false);
+ const [discardTargetTaskId, setDiscardTargetTaskId] = useState(null);
const [discarding, setDiscarding] = useState(false);
+ const discardAnchorRef = useRef(null);
const { toast } = useToast();
const open = taskId !== null && openTaskId === taskId;
+ const confirmDiscardOpen = taskId !== null && discardTargetTaskId === taskId;
useWalkthroughBackfill({ connectionStatus, setWalkthrough, taskId, walkthrough });
@@ -231,16 +224,15 @@ export function WalkthroughOverlay({ taskId, onSelectFile }: WalkthroughOverlayP
clearOpenWalkthroughTaskId(taskId);
setOpenTaskId(null);
};
- const confirmDiscard = async (event: MouseEvent) => {
- event.preventDefault();
- if (!taskId || discarding) return;
+ const discardWalkthrough = async (targetTaskId: string | null) => {
+ if (!targetTaskId || discarding) return;
setDiscarding(true);
try {
- await deleteTaskWalkthrough(taskId);
- clearOpenWalkthroughTaskId(taskId);
- setOpenTaskId(null);
- setWalkthrough(taskId, null);
- setConfirmDiscardOpen(false);
+ await deleteTaskWalkthrough(targetTaskId);
+ clearOpenWalkthroughTaskId(targetTaskId);
+ setOpenTaskId((currentTaskId) => (currentTaskId === targetTaskId ? null : currentTaskId));
+ setWalkthrough(targetTaskId, null);
+ setDiscardTargetTaskId(null);
toast({ title: t("review:walkthroughDiscarded"), variant: "success" });
} catch (error) {
console.error("Failed to discard walkthrough:", error);
@@ -255,18 +247,36 @@ export function WalkthroughOverlay({ taskId, onSelectFile }: WalkthroughOverlayP
{open ? : null}
setConfirmDiscardOpen(true)}
+ onDiscardClick={() => setDiscardTargetTaskId(taskId)}
+ onDiscardCancel={() => {
+ setDiscardTargetTaskId(null);
+ requestAnimationFrame(() => discardAnchorRef.current?.focus());
+ }}
+ onDiscardClose={() => setDiscardTargetTaskId(null)}
+ onDiscardConfirm={() => discardWalkthrough(discardTargetTaskId)}
onToggle={() => (open ? closeTour() : openTour())}
stepCount={walkthrough.steps.length}
/>
-
+ {isFinePointer ? (
+ setDiscardTargetTaskId(nextOpen ? taskId : null)}
+ onConfirm={() => discardWalkthrough(discardTargetTaskId)}
+ />
+ ) : null}
>
);
}
diff --git a/apps/web/e2e/pages/session-page.ts b/apps/web/e2e/pages/session-page.ts
index 9b6d04aa86..3a95bd04a9 100644
--- a/apps/web/e2e/pages/session-page.ts
+++ b/apps/web/e2e/pages/session-page.ts
@@ -1089,8 +1089,8 @@ export class SessionPage {
return this.page.getByTestId("walkthrough-discard");
}
- walkthroughDiscardDialog(): Locator {
- return this.page.getByRole("alertdialog", { name: "Discard walkthrough?" });
+ walkthroughDiscardConfirmation(): Locator {
+ return this.page.locator('[data-testid="walkthrough-discard-confirmation"]:visible');
}
walkthroughFloating(): Locator {
diff --git a/apps/web/e2e/tests/review/mobile-walkthrough.spec.ts b/apps/web/e2e/tests/review/mobile-walkthrough.spec.ts
index 251fe4a821..415da3d431 100644
--- a/apps/web/e2e/tests/review/mobile-walkthrough.spec.ts
+++ b/apps/web/e2e/tests/review/mobile-walkthrough.spec.ts
@@ -155,18 +155,41 @@ test.describe("Mobile code walkthrough", () => {
testPage,
apiClient,
seedData,
+ prCapture,
}) => {
await seedWalkthroughTask(testPage, apiClient, seedData);
const session = new SessionPage(testPage);
await expect(session.walkthroughLauncher()).toBeVisible({ timeout: 30_000 });
await expect(session.walkthroughDiscardButton()).toBeVisible({ timeout: 5_000 });
- await session.walkthroughDiscardButton().click();
- await expect(session.walkthroughDiscardDialog()).toBeVisible();
+ await session.walkthroughDiscardButton().tap();
+ const discardConfirmation = session.walkthroughDiscardConfirmation();
+ await expect(discardConfirmation).toBeVisible();
+ await expect(discardConfirmation).toHaveAttribute("role", "group");
+ await expect(testPage.getByRole("alertdialog")).toHaveCount(0);
+ await prCapture.screenshot("mobile-walkthrough-discard-confirmation", {
+ caption: "Mobile walkthrough keeps discard confirmation inside its touch launcher",
+ });
+ const actionBoxes = await Promise.all(
+ ["Cancel", "Discard walkthrough"].map((name) =>
+ discardConfirmation.getByRole("button", { name }).boundingBox(),
+ ),
+ );
+ for (const box of actionBoxes) {
+ if (!box) throw new Error("walkthrough discard action geometry unavailable");
+ expect(box.width).toBeGreaterThanOrEqual(44);
+ expect(box.height).toBeGreaterThanOrEqual(44);
+ }
+ await discardConfirmation.getByRole("button", { name: "Cancel" }).tap();
+ await expect(discardConfirmation).toHaveCount(0);
+ await expect(session.walkthroughLauncher()).toBeVisible();
+
+ await session.walkthroughDiscardButton().tap();
+ await expect(session.walkthroughDiscardConfirmation()).toBeVisible();
await session
- .walkthroughDiscardDialog()
+ .walkthroughDiscardConfirmation()
.getByRole("button", { name: "Discard walkthrough" })
- .click();
+ .tap();
await expect(session.walkthroughLauncher()).toHaveCount(0);
await expect(session.walkthroughFloating()).toHaveCount(0);
diff --git a/apps/web/e2e/tests/review/walkthrough.spec.ts b/apps/web/e2e/tests/review/walkthrough.spec.ts
index 48994997d4..d89ad30540 100644
--- a/apps/web/e2e/tests/review/walkthrough.spec.ts
+++ b/apps/web/e2e/tests/review/walkthrough.spec.ts
@@ -397,6 +397,7 @@ test.describe("Code walkthrough", () => {
testPage,
apiClient,
seedData,
+ prCapture,
}) => {
const session = await seedWalkthroughTask(
testPage,
@@ -438,25 +439,36 @@ test.describe("Code walkthrough", () => {
await session.walkthroughLauncher().hover();
await expect(session.walkthroughDiscardButton()).toBeVisible({ timeout: 5_000 });
await session.walkthroughDiscardButton().click();
- const discardDialog = session.walkthroughDiscardDialog();
- await expect(discardDialog).toBeVisible();
- await expectWalkthroughBehindDialog(testPage, discardDialog, [
- { locator: card, name: "walkthrough window" },
- { locator: session.walkthroughLauncher().locator(".."), name: "walkthrough launcher" },
+ const discardConfirmation = session.walkthroughDiscardConfirmation();
+ await expect(discardConfirmation).toBeVisible();
+ await expect(discardConfirmation).toHaveAttribute("role", "dialog");
+ await expect(testPage.getByRole("alertdialog")).toHaveCount(0);
+ await prCapture.screenshot("desktop-walkthrough-discard-confirmation", {
+ caption: "Desktop walkthrough confirms discard in its anchored toolbar",
+ });
+ const [confirmationZIndex, launcherZIndex] = await Promise.all([
+ discardConfirmation.evaluate((element) =>
+ Number.parseInt(getComputedStyle(element).zIndex, 10),
+ ),
+ session
+ .walkthroughLauncher()
+ .locator("..")
+ .evaluate((element) => Number.parseInt(getComputedStyle(element).zIndex, 10)),
]);
- await discardDialog.getByRole("button", { name: "Cancel" }).click();
+ expect(confirmationZIndex).toBeGreaterThan(launcherZIndex);
+ await discardConfirmation.getByRole("button", { name: "Cancel" }).click();
await expect(card).toBeVisible();
await expect(session.walkthroughLauncher()).toHaveCount(1);
await session.walkthroughLauncher().hover();
await session.walkthroughDiscardButton().click();
- await expect(session.walkthroughDiscardDialog()).toBeVisible();
+ await expect(session.walkthroughDiscardConfirmation()).toBeVisible();
await session
- .walkthroughDiscardDialog()
+ .walkthroughDiscardConfirmation()
.getByRole("button", { name: "Discard walkthrough" })
.click();
- await expect(session.walkthroughDiscardDialog()).toBeHidden({ timeout: 10_000 });
+ await expect(session.walkthroughDiscardConfirmation()).toBeHidden({ timeout: 10_000 });
await expect(session.walkthroughLauncher()).toHaveCount(0);
await expect(session.walkthroughFloating()).toHaveCount(0);
await expect(session.walkthroughEditorRange()).toHaveCount(0);