diff --git a/DESIGN.md b/DESIGN.md index 6f03ef5a066..05d8865d46d 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -841,6 +841,13 @@ status picker with its monospaced task ID, project and people. The title remains editable and the existing task-actions menu stays at the upper right. Typography and spacing use the shared tokens; toggling off restores the ordinary header. +Task breadcrumbs link the immediate parent before the current task. Each task +title displays at most 48 Unicode code points, including an ellipsis; saved +titles, hover text and accessible names retain the full title. The parent uses +at most 40% of the breadcrumb row, and both titles truncate when space runs out. +Mobile omits the collection link and parent identifier to preserve both titles +and the current task identifier. Private parents show a noninteractive label. + The local task detail also offers a **Thread preview** selector. DES-1 opens a complete synthetic conversation matching the staging transcript layout; other tasks start with their saved thread. Synthetic runner events pass through diff --git a/ui/src/components/BreadcrumbBar.test.tsx b/ui/src/components/BreadcrumbBar.test.tsx index ac16cc918c1..c9a0f438185 100644 --- a/ui/src/components/BreadcrumbBar.test.tsx +++ b/ui/src/components/BreadcrumbBar.test.tsx @@ -1,6 +1,6 @@ // @vitest-environment jsdom -import { act, useEffect, type ReactNode } from "react"; +import { act, useEffect, type ComponentProps, type ReactNode } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { BreadcrumbProvider, useBreadcrumbs } from "../context/BreadcrumbContext"; @@ -24,8 +24,8 @@ vi.mock("@/hooks/useAgentChatNavigation", () => ({ vi.mock("@/lib/router", () => ({ useNavigate: () => navigate, - Link: ({ children, className, to }: { children: ReactNode; className?: string; to: string }) => ( - {children} + Link: ({ children, to, ...props }: ComponentProps<"a"> & { to: string }) => ( + {children} ), })); @@ -62,12 +62,16 @@ function TaskBreadcrumbs({ taskDetailLayout = false, identifier = "PAP-16679", sourceHref = "/issues", + parentTitle, + title = "Hire your first engineer and create a hiring plan", }: { onOpen?: () => void; panelControl?: { open: boolean; onToggle: () => void }; taskDetailLayout?: boolean; identifier?: string; sourceHref?: string; + parentTitle?: string; + title?: string; }) { const { setBreadcrumbs, @@ -78,8 +82,9 @@ function TaskBreadcrumbs({ useEffect(() => { setBreadcrumbs([ { label: "Tasks", href: sourceHref }, + ...(parentTitle ? [{ label: parentTitle, identifier: "PAP-120", href: "/issues/PAP-120" }] : []), { - label: "Hire your first engineer and create a hiring plan", + label: title, identifier, leading: "status", }, @@ -104,6 +109,8 @@ function TaskBreadcrumbs({ setBreadcrumbToolbar, setBreadcrumbs, sourceHref, + parentTitle, + title, ]); return ; @@ -234,7 +241,8 @@ describe("BreadcrumbBar", () => { expect(container.querySelector('a[href="/TES/issues"]')).toBeNull(); const identifier = container.querySelector('[data-slot="task-title-identifier"]'); expect(identifier?.textContent).toBe("TES-3"); - expect(identifier?.previousElementSibling?.textContent).toBe("Hire your first engineer and create a hiring plan"); + expect(identifier?.previousElementSibling?.textContent).toBe("Hire your first engineer and create a hiring pl…"); + expect(identifier?.previousElementSibling?.getAttribute("title")).toBe("Hire your first engineer and create a hiring plan"); expect(identifier?.previousElementSibling?.className).toContain("truncate"); expect(container.querySelector('button[aria-label="Open sidebar"]')).not.toBeNull(); }); @@ -259,6 +267,38 @@ describe("BreadcrumbBar", () => { expect(onOpen).toHaveBeenCalledOnce(); }); + it.each([false, true])("keeps the parent linked beside the current task (mobile=%s)", async (mobile) => { + viewport.isMobile = mobile; + await act(async () => root.render( + , + )); + const parent = container.querySelector('a[href="/issues/PAP-120"]'); + expect(parent?.textContent).toContain("Prepare the engineering team"); + expect(parent?.getAttribute("aria-label")).toBe("Prepare the engineering team PAP-120"); + expect(container.querySelector('[aria-current="page"]')?.textContent).toContain("PAP-16679"); + if (mobile) { + expect(container.querySelector('a[href="/issues"]')).toBeNull(); + expect(parent?.textContent).not.toContain("PAP-120"); + } + }); + + it.each([false, true])("caps each task breadcrumb at 48 characters and preserves full names (mobile=%s)", async (mobile) => { + viewport.isMobile = mobile; + const parentTitle = "Plan the engineering team and hiring process for the next product launch"; + const title = "🚀".repeat(60); + await act(async () => root.render( + , + )); + const parent = container.querySelector('a[href="/issues/PAP-120"]'); + const page = container.querySelector('[aria-current="page"]'); + expect(parent?.textContent).toContain(`${Array.from(parentTitle).slice(0, 47).join("")}…`); + expect(parent?.title).toBe(parentTitle); + const label = page?.querySelector('[title]'); + expect(label?.textContent).toBe(`${"🚀".repeat(47)}…`); + expect(label?.getAttribute("title")).toBe(title); + expect(page?.getAttribute("aria-label")).toContain(title); + }); + it("appends the task identifier to the task title in the task-detail header", async () => { await act(async () => { root.render( @@ -276,7 +316,7 @@ describe("BreadcrumbBar", () => { expect(identifier?.closest(".relative")?.className).toContain("h-(--sz-60px)"); const title = Array.from(container.querySelectorAll("span")) - .find((element) => element.textContent === "Hire your first engineer and create a hiring plan"); + .find((element) => element.getAttribute("title") === "Hire your first engineer and create a hiring plan"); const tasksLink = container.querySelector('a[href="/issues"]'); const header = tasksLink?.closest(".relative"); expect(header?.classList).toContain("px-4"); diff --git a/ui/src/components/BreadcrumbBar.tsx b/ui/src/components/BreadcrumbBar.tsx index a7d7ffd41ee..9633d7e630b 100644 --- a/ui/src/components/BreadcrumbBar.tsx +++ b/ui/src/components/BreadcrumbBar.tsx @@ -21,6 +21,14 @@ import { useCoordinatorPresentation } from "./coordinator/CoordinatorPresentatio type GlobalToolbarContext = { companyId: string | null; companyPrefix: string | null }; +const MAX_TASK_TITLE_CHARACTERS = 48; +function taskTitleLabel(title: string) { + const characters = Array.from(title); + return characters.length > MAX_TASK_TITLE_CHARACTERS + ? `${characters.slice(0, MAX_TASK_TITLE_CHARACTERS - 1).join("")}…` + : title; +} + /** Task identifier rendered in gray monospace beside its breadcrumb label. */ function CrumbIdentifier({ identifier }: { identifier?: string }) { if (!identifier) return null; @@ -123,7 +131,9 @@ export function BreadcrumbBar({ taskDetailLayout = false, showPropertiesToggle = ); const currentCrumb = breadcrumbs[breadcrumbs.length - 1]; - if (isMobile && breadcrumbs[0]?.label === "Tasks" && currentCrumb.identifier) { + const isTaskTrail = Boolean(currentCrumb.identifier); + const visibleBreadcrumbs = isMobile && isTaskTrail && breadcrumbs.length > 1 ? breadcrumbs.slice(1) : breadcrumbs; + if (isMobile && isTaskTrail && visibleBreadcrumbs.length === 1) { return (
{menuButton} @@ -131,7 +141,7 @@ export function BreadcrumbBar({ taskDetailLayout = false, showPropertiesToggle = {currentCrumb.leading ? ( {currentCrumb.leading} ) : null} - {currentCrumb.label} + {taskTitleLabel(currentCrumb.label)} {globalToolbarSlots} @@ -143,52 +153,57 @@ export function BreadcrumbBar({ taskDetailLayout = false, showPropertiesToggle =
- {breadcrumbs.map((crumb, i) => { - const isLast = i === breadcrumbs.length - 1; - const pinnedIdentifier = taskDetailLayout && isLast && Boolean(crumb.identifier); + {visibleBreadcrumbs.map((crumb, i) => { + const isLast = i === visibleBreadcrumbs.length - 1; + const isTask = isTaskTrail && (isMobile || i > 0 || visibleBreadcrumbs.length === 1); + const pinnedIdentifier = (taskDetailLayout || isMobile) && isLast && Boolean(crumb.identifier); + const label = isTask ? taskTitleLabel(crumb.label) : crumb.label; + const accessibleLabel = [crumb.label, crumb.identifier].filter(Boolean).join(" "); return ( {i > 0 && } - + {crumb.control ?? (isLast || !crumb.href ? ( crumb.leading || crumb.identifier ? ( - + {crumb.leading && ( {crumb.leading} )} - {!taskDetailLayout ? : null} - {crumb.label} - {taskDetailLayout && isLast ? : null} + {!taskDetailLayout && !isMobile ? : null} + {label} + {(taskDetailLayout || isMobile) && isLast ? : null} ) : ( - {crumb.label} + {label} ) ) : ( {crumb.leading || crumb.identifier ? ( {crumb.leading && ( {crumb.leading} )} - {!taskDetailLayout ? : null} - {crumb.label} - {taskDetailLayout && isLast ? : null} + {!isMobile ? : null} + {label} ) : ( - {crumb.label} + {label} )} @@ -218,7 +233,7 @@ export function BreadcrumbBar({ taskDetailLayout = false, showPropertiesToggle = {breadcrumbs[0].leading} )} - {breadcrumbs[0].label} + {isTaskTrail ? taskTitleLabel(breadcrumbs[0].label) : breadcrumbs[0].label} ) : (

@@ -246,7 +261,7 @@ export function BreadcrumbBar({ taskDetailLayout = false, showPropertiesToggle = {menuButton} {breadcrumbTrail} {globalToolbarSlots} - {propertiesToggle} + {isMobile && isTaskTrail ? null : propertiesToggle}

); } diff --git a/ui/src/index.css b/ui/src/index.css index 83bcbe9b2a1..3e56f07489f 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -209,6 +209,7 @@ } :root { + --breadcrumb-parent-max-width: 40%; color-scheme: light; --font-paper-mono: "Paper Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; --file-tree-indent-base: calc(var(--spacing) * 2); diff --git a/ui/src/pages/IssueDetail.test.tsx b/ui/src/pages/IssueDetail.test.tsx index 3167dc1bb31..f4509ea83fc 100644 --- a/ui/src/pages/IssueDetail.test.tsx +++ b/ui/src/pages/IssueDetail.test.tsx @@ -1,6 +1,8 @@ import { TaskChatPausedTakeover, type TaskComposerPause } from "../components/task-chat/TaskChatPausedTakeover"; // @vitest-environment jsdom +import type { Breadcrumb } from "../context/BreadcrumbContext"; + import { DispositionRecoveryNotice, type DispositionRecoverySnapshot } from "../components/DispositionRecoveryNotice"; import { RichWorkProductCard } from "../components/task-chat/RichWorkProductCard"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; @@ -46,6 +48,8 @@ import { taskPluginPanelActivationKey } from "@/hooks/useTaskPluginPanels"; import { armIssueDetailInboxQuickArchive, createIssueDetailLocationState, + readIssueDetailBreadcrumb, + rememberIssueDetailLocationState, } from "../lib/issueDetailBreadcrumb"; import { getRecentTasksStorageKey, readRecentTasks } from "../lib/recent-tasks"; import { getLastProjectId, trackRecentProject } from "../lib/recent-projects"; @@ -2882,7 +2886,7 @@ describe("IssueDetail", () => { }, ); - it("keeps hierarchy breadcrumbs and label chips out of the Streamlined task header", async () => { + it("includes the parent in the global Streamlined breadcrumbs while keeping label chips out of the task header", async () => { mockIssuesApi.get.mockResolvedValue( createIssue({ ancestors: [ @@ -2891,6 +2895,7 @@ describe("IssueDetail", () => { identifier: "PAP-0", title: "Parent task visible in Properties", }, + { id: "root-1", identifier: "PAP-99", title: "Root task" }, ] as Issue["ancestors"], labels: [ { @@ -2916,6 +2921,90 @@ describe("IssueDetail", () => { ); expect(container.textContent).not.toContain("Quick win"); expect(container.textContent).toContain("Issue detail smoke"); + expect(mockSetBreadcrumbs).toHaveBeenLastCalledWith([ + { label: "Tasks", href: "/issues" }, + expect.objectContaining({ label: "Parent task visible in Properties", identifier: "PAP-0", href: "/issues/PAP-0" }), + expect.objectContaining({ label: "Issue detail smoke" }), + ]); + }); + + it.each(["location", "stored", "query"] as const)("preserves the %s collection context when opening the breadcrumb parent", async (source) => { + const state = createIssueDetailLocationState("Mine", "/issues?view=mine&status=in_progress", "inbox"); + if (source === "location") mockLocation.state = state; + if (source === "stored") rememberIssueDetailLocationState("PAP-1", state); + if (source === "query") mockLocation.search = "?from=inbox&fromHref=%2Fissues%3Fview%3Dmine%26status%3Din_progress"; + mockIssuesApi.get.mockResolvedValue(createIssue({ + ancestors: [{ id: "parent-1", identifier: "PAP-0", title: "Parent task" }] as Issue["ancestors"], + })); + await act(async () => { renderIssueView(); }); + await flushReact(); + const parentCrumb = mockSetBreadcrumbs.mock.calls.at(-1)![0][1]; + expect(parentCrumb.onClick).toBeTypeOf("function"); + parentCrumb.onClick(); + const expected = { label: source === "query" ? "Inbox" : "Mine", href: state.issueDetailBreadcrumb!.href }; + expect(readIssueDetailBreadcrumb("PAP-0", null)).toEqual(expected); + + mockRouteParams.issueId = "PAP-0"; + mockLocation.pathname = "/issues/PAP-0"; + mockLocation.state = null; + mockLocation.search = ""; + mockIssuesApi.get.mockResolvedValue(createIssue({ id: "parent-1", identifier: "PAP-0", title: "Parent task" })); + await act(async () => { renderIssueView(); }); + await waitForAssertion(() => { + expect(mockSetBreadcrumbs.mock.calls.at(-1)![0][0]).toEqual(expected); + }); + }); + + it.each([false, true])("renders a noninteractive private parent with mobile=%s", async (isMobile) => { + mockSidebarState.isMobile = isMobile; + mockIssuesApi.get.mockResolvedValue(createIssue({ + ancestors: [{ id: "private-parent", identifier: "PAP-0", locked: true }] as unknown as Issue["ancestors"], + })); + await act(async () => { renderIssueView(); }); + await flushReact(); + expect(mockSetBreadcrumbs).toHaveBeenLastCalledWith([ + { label: "Tasks", href: "/issues" }, + expect.objectContaining({ label: "Private task", identifier: "PAP-0", href: undefined }), + expect.objectContaining({ label: "Issue detail smoke" }), + ]); + const parentCrumb = mockSetBreadcrumbs.mock.calls.at(-1)![0][1]; + expect(parentCrumb.onClick).toBeUndefined(); + await act(async () => { renderIssueView(parentCrumb.control); }); + const chip = container.querySelector('[data-testid="locked-issue-chip"]'); + expect(chip).not.toBeNull(); + expect(chip?.textContent).toBe(isMobile ? "Private" : "PAP-0"); + expect(container.querySelector('a, [role="link"]')).toBeNull(); + }); + + it("updates the private parent label when an open task changes viewport", async () => { + mockIssuesApi.get.mockResolvedValue(createIssue({ + ancestors: [{ id: "private-parent", identifier: "PAP-0", locked: true }] as unknown as Issue["ancestors"], + })); + const captured: Breadcrumb[][] = []; + for (const isMobile of [false, true, false]) { + mockSidebarState.isMobile = isMobile; + await act(async () => { renderIssueView(); }); + await flushReact(); + captured.push(mockSetBreadcrumbs.mock.calls.at(-1)![0]); + } + + const { BreadcrumbProvider, useBreadcrumbs } = await vi.importActual< + typeof import("../context/BreadcrumbContext") + >("../context/BreadcrumbContext"); + let updateBreadcrumbs: (crumbs: Breadcrumb[]) => void = () => {}; + function RenderBreadcrumbControl() { + const { breadcrumbs, setBreadcrumbs } = useBreadcrumbs(); + updateBreadcrumbs = setBreadcrumbs; + return breadcrumbs[1]?.control; + } + await act(async () => { + renderIssueView(); + }); + for (const [index, expected] of ["PAP-0", "Private", "PAP-0"].entries()) { + await act(async () => { updateBreadcrumbs(captured[index]); }); + expect(container.querySelector('[data-testid="locked-issue-chip"]')?.textContent).toBe(expected); + expect(container.querySelector('a, [role="link"]')).toBeNull(); + } }); it("preserves hierarchy breadcrumbs and label chips when Streamlined UI is off", async () => { diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 578791fe7e3..d710a3229ae 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -5951,6 +5951,18 @@ export function TaskDetailSurface({ conversation, tasksTab }: { tasksTab?: TaskS }); const conversationAgent = conversation?.agent ?? agents?.find(agent => agent.id === issue?.conversationAgentId); + const breadcrumbParent = issue?.ancestors?.[0]; + const breadcrumbParentRef = breadcrumbParent && !isLockedIssueStub(breadcrumbParent) + ? breadcrumbParent.identifier ?? breadcrumbParent.id + : null; + const rememberBreadcrumbParent = useCallback(() => { + if (!breadcrumbParentRef) return; + rememberIssueDetailLocationState( + breadcrumbParentRef, + resolvedIssueDetailState ?? location.state, + location.search, + ); + }, [breadcrumbParentRef, resolvedIssueDetailState, location.state, location.search]); useEffect(() => { if (conversationAgent) { setBreadcrumbs([{ label: "Chat" }]); @@ -5958,6 +5970,18 @@ export function TaskDetailSurface({ conversation, tasksTab }: { tasksTab?: TaskS } setBreadcrumbs([ sourceBreadcrumb, + ...(breadcrumbParent ? [{ + label: isLockedIssueStub(breadcrumbParent) ? "Private task" : breadcrumbParent.title, + identifier: breadcrumbParent.identifier ?? undefined, + href: isLockedIssueStub(breadcrumbParent) + ? undefined + : createIssueDetailPath(breadcrumbParent.identifier ?? breadcrumbParent.id), + onClick: isLockedIssueStub(breadcrumbParent) ? undefined : rememberBreadcrumbParent, + control: isLockedIssueStub(breadcrumbParent) + ? + : undefined, + controlKey: isLockedIssueStub(breadcrumbParent) ? `private-parent:${isMobile}` : undefined, + }] : []), { // The status glyph (leading) already conveys in-progress/live state; // no redundant 🔵 emoji prefix on the title. @@ -5972,6 +5996,9 @@ export function TaskDetailSurface({ conversation, tasksTab }: { tasksTab?: TaskS coordinatorWorkspaceEnabled, breadcrumbTitle, breadcrumbIdentifier, + breadcrumbParent, + isMobile, + rememberBreadcrumbParent, hasLiveRuns, setBreadcrumbs, sourceBreadcrumb.href, diff --git a/ui/storybook/stories/task-coordination/task-breadcrumbs.stories.tsx b/ui/storybook/stories/task-coordination/task-breadcrumbs.stories.tsx new file mode 100644 index 00000000000..384793e537f --- /dev/null +++ b/ui/storybook/stories/task-coordination/task-breadcrumbs.stories.tsx @@ -0,0 +1,125 @@ +import { useEffect } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, within } from "storybook/test"; +import { BreadcrumbBar } from "@/components/BreadcrumbBar"; +import { LockedIssueChip } from "@/components/LockedIssueChip"; +import { StatusIcon } from "@/components/StatusIcon"; +import { useBreadcrumbs } from "@/context/BreadcrumbContext"; +import { useSidebar } from "@/context/SidebarContext"; +import { useLocation } from "@/lib/router"; +import { PluginLauncherProvider } from "@/plugins/launchers"; + +function TaskBreadcrumbPreview({ + parentTitle, + title, + showParent = true, + privateParent = false, + source = "Tasks", +}: { + parentTitle: string; + title: string; + showParent?: boolean; + privateParent?: boolean; + source?: "Tasks" | "Mine"; +}) { + const { setBreadcrumbs } = useBreadcrumbs(); + const { isMobile } = useSidebar(); + const location = useLocation(); + useEffect(() => { + setBreadcrumbs([ + { label: source, href: source === "Mine" ? "/inbox" : "/issues" }, + ...(showParent ? [{ + label: privateParent ? "Private task" : parentTitle, + identifier: "PAP-120", + href: privateParent ? undefined : "/issues/PAP-120", + control: privateParent ? : undefined, + controlKey: privateParent ? `private-parent:${isMobile}` : undefined, + }] : []), + { label: title, identifier: "PAP-121", leading: , leadingKey: "in_progress" }, + ]); + }, [parentTitle, title, showParent, privateParent, source, isMobile, setBreadcrumbs]); + + return ( +
+ +
+

Task breadcrumb preview

+

{title}

+ {showParent && !privateParent ?

Parent: {parentTitle}

: null} +

Breadcrumb titles show up to 48 characters, including the ellipsis. Narrow screens shorten both titles further. Hover a breadcrumb to read its full title; screen readers retain the full name.

+ Current route: {location.pathname} +
+
+ ); +} + +const meta = { + id: "task-parent-breadcrumbs", + title: "Tasks and conversations/Delegation, dependencies, and signoff/Parent task breadcrumbs", + component: TaskBreadcrumbPreview, + decorators: [(Story) => ], + parameters: { + layout: "fullscreen", + initialEntries: ["/PAP/issues/PAP-121"], + waitForViewport: true, + paperclip: { + capabilities: ["task-coordination/delegation", "navigation-preferences/navigation"], + status: "current", + implementation: "production", + boundary: "fixtures", + notes: "Mounts the shipped BreadcrumbBar with fixture breadcrumb context. Covers linked immediate parent, long titles, private parent, standalone task, Mine source, light/dark themes and responsive widths. Page data loading and parent authorization are outside this isolated header story.", + }, + }, + args: { parentTitle: "Prepare the product launch", title: "Review the release checklist" }, + globals: { theme: "light" }, +} satisfies Meta; +export default meta; +type Story = StoryObj; + +export const Subtask: Story = { + play: async ({ canvasElement }) => { + const nav = within(await within(canvasElement).findByRole("navigation", { name: "breadcrumb" })); + await expect(nav.getByRole("link", { name: "Prepare the product launch PAP-120" })).toHaveAttribute("href", "/PAP/issues/PAP-120"); + await expect(nav.getByRole("link", { name: "Review the release checklist PAP-121" })).toHaveAttribute("aria-current", "page"); + }, +}; +export const LongTitles: Story = { + args: { + parentTitle: "Prepare the international product launch and coordinate release readiness across all teams", + title: "Review the release checklist and verify every customer migration before the launch window opens", + }, + play: async ({ canvasElement }) => { + const nav = await within(canvasElement).findByRole("navigation", { name: "breadcrumb" }); + for (const title of nav.querySelectorAll('[data-slot="breadcrumb-item"] .truncate')) { + await expect(Array.from(title.textContent ?? "").length).toBeLessThanOrEqual(48); + } + await expect(within(nav).getByRole("link", { name: /Prepare the international product launch/ })).toHaveAttribute("title", "Prepare the international product launch and coordinate release readiness across all teams"); + }, +}; +export const Mobile: Story = { ...LongTitles, globals: { viewport: { value: "mobile", isRotated: false } } }; +export const SmallMobile: Story = { ...LongTitles, globals: { viewport: { value: "mobile1", isRotated: false } } }; +export const Tablet: Story = { ...LongTitles, globals: { viewport: { value: "tablet", isRotated: false } } }; +export const Dark: Story = { ...LongTitles, globals: { theme: "dark" } }; +export const StandaloneTask: Story = { args: { showParent: false } }; +export const FromMine: Story = { args: { source: "Mine" } }; +export const PrivateParent: Story = { + args: { privateParent: true }, + play: async ({ canvasElement }) => { + const nav = await within(canvasElement).findByRole("navigation", { name: "breadcrumb" }); + await expect(within(nav).queryByRole("link", { name: /Prepare the product launch/ })).not.toBeInTheDocument(); + await expect(within(nav).queryByRole("link", { name: /private/i })).not.toBeInTheDocument(); + await expect(within(nav).getByTestId("locked-issue-chip")).toBeVisible(); + await expect(nav.querySelectorAll('[aria-current="page"]')).toHaveLength(1); + }, +}; +export const PrivateParentMobile: Story = { + ...PrivateParent, + globals: { viewport: { value: "mobile1", isRotated: false } }, + play: async (context) => { + await PrivateParent.play!(context); + const nav = await within(context.canvasElement).findByRole("navigation", { name: "breadcrumb" }); + await expect(within(nav).getByTestId("locked-issue-chip")).toHaveTextContent("Private"); + await expect(nav).not.toHaveTextContent("PAP-120"); + await expect(nav).toHaveTextContent("PAP-121"); + }, +};