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);