From b2bc97379f7284615da1e803cef9df9aaf07c141 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Fri, 4 Sep 2026 16:10:47 -0400 Subject: [PATCH 1/2] fix: keep the Activity commit detail pane open after browser Back --- ...activity-back-navigation.browser.svelte.ts | 172 ++++++++++++++++++ frontend/src/App.svelte | 53 +++++- .../src/lib/utils/activitySelection.test.ts | 76 ++++++++ frontend/src/lib/utils/activitySelection.ts | 61 ++++++- .../src/lib/views/ActivityFeedView.svelte | 30 ++- .../src/lib/views/ActivityFeedView.test.ts | 42 +++++ 6 files changed, 418 insertions(+), 16 deletions(-) create mode 100644 frontend/src/App.activity-back-navigation.browser.svelte.ts diff --git a/frontend/src/App.activity-back-navigation.browser.svelte.ts b/frontend/src/App.activity-back-navigation.browser.svelte.ts new file mode 100644 index 0000000000..4910976f20 --- /dev/null +++ b/frontend/src/App.activity-back-navigation.browser.svelte.ts @@ -0,0 +1,172 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; +import { page } from "vite-plus/test/browser"; + +import { mountBrowserApp, pressKey, resetKeyboardModuleState, type MountedBrowserApp } from "./test/browserAppHarness.js"; +import { jsonResponse, mockSettings, type MockRouteOverride } from "./test/mockApiFetch.js"; + +const WAIT = 10_000; + +const repo = { + provider: "github", + platform_host: "github.com", + owner: "acme", + name: "widgets", + repo_path: "acme/widgets", + capabilities: {}, +}; + +const activityItems = [ + { + id: "a1", + cursor: "a1", + activity_type: "comment", + author: "marius", + body_preview: "", + created_at: "2026-03-30T14:00:00Z", + item_number: 42, + item_state: "open", + item_title: "PR 42 title", + item_type: "pr", + item_url: "https://github.com/acme/widgets/pull/42", + repo, + }, + { + id: "b1", + cursor: "b1", + activity_type: "comment", + author: "marius", + body_preview: "", + created_at: "2026-03-30T13:00:00Z", + item_number: 55, + item_state: "open", + item_title: "Issue 55 title", + item_type: "issue", + item_url: "https://github.com/acme/widgets/issues/55", + repo, + }, + { + id: "c1", + cursor: "c1", + activity_type: "default_branch_commit", + author: "marius", + body_preview: "Bump dependency", + created_at: "2026-03-30T12:00:00Z", + item_number: 0, + item_state: "", + item_title: "", + item_type: "pr", + item_url: "https://github.com/acme/widgets/commit/abcdef1234567890", + branch_name: "main", + commit_sha: "abcdef1234567890", + repo, + }, +]; + +function activityOverrides(): MockRouteOverride[] { + return [ + (req) => { + if (req.method !== "GET" || req.url.pathname !== "/api/v1/settings") return null; + return jsonResponse({ + ...mockSettings, + activity: { + ...mockSettings.activity, + view_mode: "flat", + collapse_threads: false, + }, + }); + }, + (req) => { + if (req.method !== "GET" || req.url.pathname !== "/api/v1/activity") return null; + return jsonResponse({ capped: false, items: activityItems }); + }, + ]; +} + +function activityRow(text: string): Element { + return Array.from(document.querySelectorAll(".activity-table .activity-row")).find((row) => + (row.textContent ?? "").includes(text), + )!; +} + +async function openSelection(text: string): Promise { + await vi.waitFor(() => expect(document.querySelector(".activity-table .activity-row")).not.toBeNull(), WAIT); + const row = activityRow(text); + expect(row).not.toBeUndefined(); + await page.elementLocator(row).click(); + await vi.waitFor(() => expect(document.querySelector(".activity-detail")).not.toBeNull(), WAIT); + return window.location.pathname + window.location.search; +} + +async function leaveAndRestore(destination: "/pulls" | "/issues"): Promise { + const { navigate } = await import("./lib/stores/router.svelte.js"); + navigate(destination); + await vi.waitFor(() => expect(document.querySelector(".activity-detail")).toBeNull(), WAIT); + window.history.back(); + await vi.waitFor(() => expect(document.querySelector(".activity-detail")).not.toBeNull(), WAIT); +} + +describe("Activity detail restoration after browser Back", () => { + vi.setConfig({ testTimeout: 30_000 }); + + let mounted: MountedBrowserApp | null = null; + + beforeEach(async () => { + await page.viewport(1280, 900); + }); + + afterEach(async () => { + mounted?.unmount(); + mounted = null; + vi.restoreAllMocks(); + localStorage.clear(); + sessionStorage.clear(); + await resetKeyboardModuleState(); + }); + + it("restores the commit detail pane after leaving Activity and pressing Back", async () => { + mounted = await mountBrowserApp("/", { overrides: activityOverrides() }); + const activityUrl = await openSelection("Bump dependency"); + + await leaveAndRestore("/pulls"); + + expect(document.querySelector(".commit-diff-panel")).not.toBeNull(); + expect(document.querySelector(".activity-detail-header")?.textContent).toContain("acme/widgets"); + expect(document.querySelector(".activity-detail-header")?.textContent).toContain("main"); + const selected = new URL(activityUrl, window.location.origin).searchParams; + expect(selected.get("selected")).toBe("commit:abcdef1234567890"); + expect(selected.get("provider")).toBe("github"); + expect(selected.get("platform_host")).toBe("github.com"); + expect(selected.get("repo_path")).toBe("acme/widgets"); + expect(selected.get("branch")).toBe("main"); + }); + + it("restores a PR selection after leaving Activity and pressing Back", async () => { + mounted = await mountBrowserApp("/", { overrides: activityOverrides() }); + await openSelection("PR 42 title"); + + await leaveAndRestore("/pulls"); + + expect(document.querySelector(".activity-detail")).not.toBeNull(); + expect(document.querySelector(".activity-detail-header")?.textContent).toContain("acme/widgets#42"); + }); + + it("restores an issue selection after leaving Activity and pressing Back", async () => { + mounted = await mountBrowserApp("/", { overrides: activityOverrides() }); + await openSelection("Issue 55 title"); + + await leaveAndRestore("/issues"); + + expect(document.querySelector(".activity-detail")).not.toBeNull(); + expect(document.querySelector(".activity-detail-header")?.textContent).toContain("acme/widgets#55"); + }); + + it("Escape closes a restored commit pane", async () => { + mounted = await mountBrowserApp("/", { overrides: activityOverrides() }); + await openSelection("Bump dependency"); + await leaveAndRestore("/pulls"); + + pressKey("Escape"); + await vi.waitFor(() => expect(document.querySelector(".activity-detail")).toBeNull(), WAIT); + expect(new URL(window.location.href).searchParams.has("selected")).toBe(false); + }); +}); diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index c42a309a23..190e028445 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -127,7 +127,9 @@ import { buildActivitySelectionSearch, parseActivitySelection, + type ActivityCommitSelection, type ActivityDetailTab, + type ActivitySelection, } from "./lib/utils/activitySelection.js"; import { docsHref } from "./lib/api/docs/route.js"; import { @@ -899,17 +901,23 @@ const route = getRoute(); const page = route.page; - if (page !== "activity") { - drawerItem = null; - } else if (!stores.settings.hasConfiguredRepos()) { + if (page !== "activity" || !stores.settings.hasConfiguredRepos()) { drawerItem = null; + commitItem = null; } else { - const nextDrawer = parseActivitySelection( + const nextSelection = parseActivitySelection( window.location.search, ); + const nextDrawer = + nextSelection && nextSelection.itemType !== "commit" ? nextSelection : null; + const nextCommit = + nextSelection && nextSelection.itemType === "commit" ? nextSelection : null; if (!sameActivitySelection(drawerItem, nextDrawer)) { drawerItem = nextDrawer; } + if (!sameActivityCommitSelection(commitItem, nextCommit)) { + commitItem = nextCommit; + } } if (route.page === "pulls") { @@ -961,6 +969,9 @@ }; let drawerItem = $state(null); + // Owned here for the same reason drawerItem is: the Activity selection lives + // in the page's query string, and only this component writes it. + let commitItem = $state(null); function sameActivitySelection( left: DrawerItem | null, @@ -978,8 +989,26 @@ && left.detailTab === right.detailTab; } + function sameActivityCommitSelection( + left: ActivityCommitSelection | null, + right: ActivityCommitSelection | null, + ): boolean { + if (left === right) return true; + if (left === null || right === null) return false; + // `title` is display text the feed supplies and the URL does not carry, so + // it is deliberately not part of identity: comparing it would let a + // reparse downgrade a live commit subject to the short SHA. + return left.provider === right.provider + && left.platformHost === right.platformHost + && left.repoPath === right.repoPath + && left.owner === right.owner + && left.name === right.name + && left.branchName === right.branchName + && left.commitSha === right.commitSha; + } + function updateDrawerURL( - item: DrawerItem | null, + item: ActivitySelection | null, ): void { if (getPage() !== "activity") return; const sp = buildActivitySelectionSearch( @@ -1017,6 +1046,7 @@ ...selectedItem, detailTab: "conversation", }; + commitItem = null; updateDrawerURL(drawerItem); } @@ -1035,6 +1065,14 @@ updateDrawerURL(drawerItem); } + function handleActivityCommitSelect( + item: ActivityCommitSelection, + ): void { + drawerItem = null; + commitItem = item; + updateDrawerURL(commitItem); + } + function handleResponsiveStackMemberNavigate( ref: PullRequestRouteRef, ): boolean | void { @@ -1049,6 +1087,7 @@ function closeDrawer(): void { drawerItem = null; + commitItem = null; updateDrawerURL(null); } @@ -1076,7 +1115,7 @@ scope: "global", binding: { key: "Escape" }, priority: 50, - when: (ctx) => ctx.page === "activity" && drawerItem !== null, + when: (ctx) => ctx.page === "activity" && (drawerItem !== null || commitItem !== null), handler: () => closeDrawer(), }, ]); @@ -1452,6 +1491,8 @@ detailTab={drawerItem?.detailTab ?? "conversation"} onDetailTabChange={handleActivityDetailTabChange} onDrawerItemChange={handleActivityDrawerItemChange} + {commitItem} + onSelectCommit={handleActivityCommitSelect} inlineWorkspace={getInlineWorkspaceController("activity")} {workspacePaneControls} /> diff --git a/frontend/src/lib/utils/activitySelection.test.ts b/frontend/src/lib/utils/activitySelection.test.ts index 6d0de5cc84..dd1fde5537 100644 --- a/frontend/src/lib/utils/activitySelection.test.ts +++ b/frontend/src/lib/utils/activitySelection.test.ts @@ -14,6 +14,14 @@ const githubWidgets = { repoPath: "acme/widgets", } as const; +const githubCommit = { + itemType: "commit" as const, + ...githubWidgets, + branchName: "main", + commitSha: "abcdef1234567890", + title: "Bump dependency", +}; + describe("activity selection URL state", () => { it("parses PR conversation selection", () => { expect( @@ -67,6 +75,43 @@ describe("activity selection URL state", () => { }); }); + it("parses a commit selection with its branch", () => { + expect( + parseActivitySelection( + "?selected=commit:abcdef1234567890&provider=github&platform_host=github.com&repo_path=acme%2Fwidgets&branch=main", + ), + ).toEqual({ ...githubCommit, title: "abcdef123456" }); + }); + + it("uses the default branch when a commit selection has no branch", () => { + expect( + parseActivitySelection( + "?selected=commit:abcdef1234567890&provider=github&platform_host=github.com&repo_path=acme%2Fwidgets", + ), + ).toEqual({ ...githubCommit, branchName: "default branch", title: "abcdef123456" }); + }); + + it("rejects an invalid commit SHA", () => { + expect( + parseActivitySelection( + "?selected=commit:zzz&provider=github&platform_host=github.com&repo_path=acme%2Fwidgets", + ), + ).toBeNull(); + }); + + it("round-trips every commit identity field", () => { + const search = buildActivitySelectionSearch("?range=30d", githubCommit); + + expect(parseActivitySelection(`?${search.toString()}`)).toEqual({ ...githubCommit, title: "abcdef123456" }); + }); + + it("preserves filters when writing a commit selection", () => { + const search = buildActivitySelectionSearch("?item_types=commit&range=30d", githubCommit); + + expect(search.get("item_types")).toBe("commit"); + expect(search.get("range")).toBe("30d"); + }); + it("preserves existing Activity filters when writing selection", () => { const next = buildActivitySelectionSearch("?range=30d&view=threaded", { itemType: "pr", @@ -130,6 +175,32 @@ describe("activity selection URL state", () => { expect(next.has("selected_tab")).toBe(false); }); + it("replaces a PR selection with a commit without stale item fields", () => { + const next = buildActivitySelectionSearch( + "?selected=pr:1&selected_tab=files&provider=github&repo_path=acme%2Fwidgets", + githubCommit, + ); + + expect(next.get("selected")).toBe("commit:abcdef1234567890"); + expect(next.get("branch")).toBe("main"); + expect(next.has("selected_tab")).toBe(false); + }); + + it("replaces a commit selection with a PR without a stale branch", () => { + const next = buildActivitySelectionSearch( + "?selected=commit:abcdef1234567890&branch=main&provider=github&repo_path=acme%2Fwidgets", + { + itemType: "pr", + ...githubWidgets, + number: 1, + detailTab: "conversation", + }, + ); + + expect(next.get("selected")).toBe("pr:1"); + expect(next.has("branch")).toBe(false); + }); + it("overwrites an issue selection with a PR and drops platform host", () => { const next = buildActivitySelectionSearch( "?selected=issue:10&provider=github&platform_host=ghe.example.com&repo_path=acme%2Fwidgets&search=bug", @@ -174,6 +245,11 @@ describe("activity selection URL state", () => { expect(activitySelectionToRoute(issue, "pulls")).toBeNull(); }); + it("does not build destination routes for a commit selection", () => { + expect(activitySelectionToRoute(githubCommit, "pulls")).toBeNull(); + expect(activitySelectionToRoute(githubCommit, "issues")).toBeNull(); + }); + it.each([ "", "?selected=garbage", diff --git a/frontend/src/lib/utils/activitySelection.ts b/frontend/src/lib/utils/activitySelection.ts index 67ee4aa776..a36e315cd1 100644 --- a/frontend/src/lib/utils/activitySelection.ts +++ b/frontend/src/lib/utils/activitySelection.ts @@ -3,10 +3,33 @@ import { buildIssueRoute, buildPullRequestFilesRoute, buildPullRequestRoute, typ export type ActivitySelectionItemType = "pr" | "issue"; export type ActivityDetailTab = "conversation" | "files"; -export type ActivitySelection = RoutedItemRef & { +export type ActivityItemSelection = RoutedItemRef & { detailTab: ActivityDetailTab; }; +/** + * A default-branch commit selected from the Activity feed. It carries repo + * identity, the branch it landed on, and the commit SHA — everything + * CommitDiffPanel needs and nothing derived. `title` is display-only and is + * NOT serialized: putting the commit subject in the query string would make + * the URL a second owner of text the feed already supplies, and a restored + * pane falls back to the short SHA exactly as handleSelectBranchCommit does + * for a commit with no body preview. + */ +export type ActivityCommitSelection = { + itemType: "commit"; + provider: string; + platformHost?: string | undefined; + repoPath: string; + owner: string; + name: string; + branchName: string; + commitSha: string; + title: string; +}; + +export type ActivitySelection = ActivityItemSelection | ActivityCommitSelection; + type Destination = "pulls" | "issues"; function searchParams(search: string): URLSearchParams { @@ -23,15 +46,32 @@ export function parseActivitySelection(search: string): ActivitySelection | null const repoPath = sp.get("repo_path")?.replace(/^\/+|\/+$/g, ""); if (!provider || !repoPath) return null; + const pathParts = repoPath.split("/").filter(Boolean); + if (pathParts.length < 2) return null; + const name = pathParts[pathParts.length - 1]!; + const owner = pathParts.slice(0, -1).join("/"); + + const commitMatch = selected.match(/^commit:([0-9a-fA-F]{7,64})$/); + if (commitMatch) { + const commitSha = commitMatch[1]!.toLowerCase(); + return { + itemType: "commit", + owner, + name, + repoPath, + commitSha, + branchName: sp.get("branch")?.trim() || "default branch", + title: commitSha.slice(0, 12), + provider, + ...(platformHost && { platformHost }), + }; + } + const providerMatch = selected.match(/^(pr|issue):(\d+)$/); if (!providerMatch) return null; - const pathParts = repoPath.split("/").filter(Boolean); - if (pathParts.length < 2) return null; const itemType = providerMatch[1] as ActivitySelectionItemType; const number = parseInt(providerMatch[2]!, 10); - const name = pathParts[pathParts.length - 1]!; - const owner = pathParts.slice(0, -1).join("/"); const detailTab: ActivityDetailTab = itemType === "pr" && sp.get("selected_tab") === "files" ? "files" : "conversation"; @@ -58,9 +98,19 @@ export function buildActivitySelectionSearch( sp.delete("provider"); sp.delete("platform_host"); sp.delete("repo_path"); + sp.delete("branch"); if (!selection) return sp; + if (selection.itemType === "commit") { + sp.set("selected", `commit:${selection.commitSha}`); + sp.set("provider", selection.provider); + if (selection.platformHost) sp.set("platform_host", selection.platformHost); + sp.set("repo_path", selection.repoPath); + sp.set("branch", selection.branchName); + return sp; + } + sp.set("selected", `${selection.itemType}:${selection.number}`); sp.set("provider", selection.provider); if (selection.platformHost) sp.set("platform_host", selection.platformHost); @@ -73,6 +123,7 @@ export function buildActivitySelectionSearch( export function activitySelectionToRoute(selection: ActivitySelection | null, destination: Destination): string | null { if (!selection) return null; + if (selection.itemType === "commit") return null; if (destination === "pulls") { if (selection.itemType !== "pr") return null; return selection.detailTab === "files" ? buildPullRequestFilesRoute(selection) : buildPullRequestRoute(selection); diff --git a/frontend/src/lib/views/ActivityFeedView.svelte b/frontend/src/lib/views/ActivityFeedView.svelte index bff40e0476..ab7f5f6303 100644 --- a/frontend/src/lib/views/ActivityFeedView.svelte +++ b/frontend/src/lib/views/ActivityFeedView.svelte @@ -37,6 +37,7 @@ }; type CommitDrawerItem = { + itemType: "commit"; provider: string; platformHost?: string | undefined; repoPath: string; @@ -54,6 +55,8 @@ onCloseDrawer?: () => void; onDetailTabChange?: (tab: ActivityDetailTab, options?: { replace?: boolean }) => void; onDrawerItemChange?: (item: DrawerPRItem) => void; + commitItem?: CommitDrawerItem | null; + onSelectCommit?: (item: CommitDrawerItem) => void; phone?: boolean; inlineWorkspace?: InlineWorkspaceController | null; /** @@ -71,6 +74,8 @@ onCloseDrawer, onDetailTabChange, onDrawerItemChange, + commitItem: controlledCommitItem, + onSelectCommit, phone = false, inlineWorkspace = null, workspacePaneControls = undefined, @@ -108,7 +113,7 @@ // Internal state used when no controlled props are // provided (standalone usage). let internalDrawer = $state(null); - let commitDrawer = $state(null); + let internalCommitDrawer = $state(null); let internalDetailTab = $state( "conversation", ); @@ -161,6 +166,15 @@ const activeDrawer = $derived( controlled ? (controlledDrawer ?? null) : internalDrawer, ); + // Same seam shape as the item drawer above: a host that supplies either the + // value or the callback owns the commit selection and its URL round trip; + // standalone usage keeps it local. + const commitControlled = $derived( + controlledCommitItem !== undefined || onSelectCommit !== undefined, + ); + const commitDrawer = $derived( + commitControlled ? (controlledCommitItem ?? null) : internalCommitDrawer, + ); const hasActiveDetail = $derived( activeDrawer !== null || commitDrawer !== null, ); @@ -361,7 +375,7 @@ } function handleSelect(item: ActivityItem): void { - commitDrawer = null; + if (!commitControlled) internalCommitDrawer = null; if (!item.repo) { throw new Error("activity item missing provider repo identity"); } @@ -390,7 +404,8 @@ } if (!item.commit_sha) return; - commitDrawer = { + const entry: CommitDrawerItem = { + itemType: "commit", provider: item.repo.provider, platformHost: item.repo.platform_host, repoPath: item.repo.repo_path, @@ -400,16 +415,21 @@ commitSha: item.commit_sha, title: item.body_preview || item.commit_sha.slice(0, 12), }; + if (commitControlled) { + onSelectCommit?.(entry); + } else { + internalCommitDrawer = entry; + } if (!controlled) { internalDrawer = null; - } else if (activeDrawer !== null) { + } else if (!commitControlled && activeDrawer !== null) { onCloseDrawer?.(); } } function handleClose(): void { activityPaneCollapsed = false; - commitDrawer = null; + if (!commitControlled) internalCommitDrawer = null; if (!controlled) { internalDrawer = null; } diff --git a/frontend/src/lib/views/ActivityFeedView.test.ts b/frontend/src/lib/views/ActivityFeedView.test.ts index 72b67f1b61..1c6e2f38a0 100644 --- a/frontend/src/lib/views/ActivityFeedView.test.ts +++ b/frontend/src/lib/views/ActivityFeedView.test.ts @@ -39,6 +39,16 @@ function issueDrawer(number = 9) { return { ...repo, itemType: "issue" as const, number, detailTab: "conversation" as const }; } +function commitDrawer() { + return { + ...repo, + itemType: "commit" as const, + branchName: "main", + commitSha: "abcdef1234567890", + title: "Fix the thing", + }; +} + function pullDetailFixture(number: number, workspace?: { id: string; status: string }) { return { repo_owner: repo.owner, @@ -61,6 +71,8 @@ function issueDetailFixture(number: number, workspace?: { id: string; status: st interface RenderOptions { drawerItem?: unknown; + commitItem?: unknown; + onSelectCommit?: (item: unknown) => void; inlineWorkspace?: InlineWorkspaceController | null; pullDetail?: unknown; issueDetail?: unknown; @@ -87,6 +99,8 @@ function renderActivity(options: RenderOptions = {}) { ...render(ActivityFeedView, { props: { drawerItem: options.drawerItem ?? null, + ...(options.commitItem !== undefined ? { commitItem: options.commitItem } : {}), + ...(options.onSelectCommit !== undefined ? { onSelectCommit: options.onSelectCommit } : {}), ...(options.inlineWorkspace !== undefined ? { inlineWorkspace: options.inlineWorkspace } : {}), ...(options.workspacePaneControls !== undefined ? { workspacePaneControls: options.workspacePaneControls } @@ -195,6 +209,34 @@ describe("ActivityFeedView detail panes", () => { expect(commitPanel.dataset.inputActive).toBe("true"); }); + it("renders a controlled commit pane", () => { + renderActivity({ commitItem: commitDrawer() }); + + expect(screen.getByRole("tab", { name: "Commit" })).toBeTruthy(); + expect(screen.getByTestId("commit-diff-panel")).toBeTruthy(); + }); + + it("sends a branch-commit click to the controlled callback", async () => { + const onSelectCommit = vi.fn(); + renderActivity({ onSelectCommit }); + + screen.getByTestId("select-branch-commit").click(); + await tick(); + + expect(onSelectCommit).toHaveBeenCalledWith({ + itemType: "commit", + provider: "github", + platformHost: "github.com", + repoPath: "acme/widgets", + owner: "acme", + name: "widgets", + branchName: "main", + commitSha: "abcdef1234567890", + title: "Fix the thing", + }); + expect(screen.queryByRole("tab", { name: "Commit" })).toBeNull(); + }); + it("renders a promoted session's pane in the activity drawer", () => { const layout = getPaneLayoutStore("activity"); const paneKey = sessionPaneKey("ws-1", undefined, "ws-1:helper"); From f6aab4070a79d354d60ae9d90ea2de7b08aaa557 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Fri, 4 Sep 2026 16:43:35 -0400 Subject: [PATCH 2/2] test: cover Activity selection consumers --- ...App.activity-back-navigation.browser.svelte.ts | 15 +++++++++++++-- frontend/src/lib/stores/keyboard/actions.test.ts | 12 ++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.activity-back-navigation.browser.svelte.ts b/frontend/src/App.activity-back-navigation.browser.svelte.ts index 4910976f20..173ca2fded 100644 --- a/frontend/src/App.activity-back-navigation.browser.svelte.ts +++ b/frontend/src/App.activity-back-navigation.browser.svelte.ts @@ -83,13 +83,13 @@ function activityOverrides(): MockRouteOverride[] { } function activityRow(text: string): Element { - return Array.from(document.querySelectorAll(".activity-table .activity-row")).find((row) => + return Array.from(document.querySelectorAll(".activity-row")).find((row) => (row.textContent ?? "").includes(text), )!; } async function openSelection(text: string): Promise { - await vi.waitFor(() => expect(document.querySelector(".activity-table .activity-row")).not.toBeNull(), WAIT); + await vi.waitFor(() => expect(document.querySelector(".activity-row")).not.toBeNull(), WAIT); const row = activityRow(text); expect(row).not.toBeUndefined(); await page.elementLocator(row).click(); @@ -132,6 +132,7 @@ describe("Activity detail restoration after browser Back", () => { expect(document.querySelector(".commit-diff-panel")).not.toBeNull(); expect(document.querySelector(".activity-detail-header")?.textContent).toContain("acme/widgets"); expect(document.querySelector(".activity-detail-header")?.textContent).toContain("main"); + expect(document.querySelector(".activity-detail-header")?.textContent).toContain("abcdef123456"); const selected = new URL(activityUrl, window.location.origin).searchParams; expect(selected.get("selected")).toBe("commit:abcdef1234567890"); expect(selected.get("provider")).toBe("github"); @@ -169,4 +170,14 @@ describe("Activity detail restoration after browser Back", () => { await vi.waitFor(() => expect(document.querySelector(".activity-detail")).toBeNull(), WAIT); expect(new URL(window.location.href).searchParams.has("selected")).toBe(false); }); + + it("replaces a commit selection when an item row is selected", async () => { + mounted = await mountBrowserApp("/", { overrides: activityOverrides() }); + await openSelection("Bump dependency"); + + await page.getByText("PR 42 title").click(); + await vi.waitFor(() => expect(document.querySelector(".activity-detail-header")?.textContent).toContain("acme/widgets#42"), WAIT); + expect(new URL(window.location.href).searchParams.get("selected")).toBe("pr:42"); + expect(document.querySelector(".commit-diff-panel")).toBeNull(); + }); }); diff --git a/frontend/src/lib/stores/keyboard/actions.test.ts b/frontend/src/lib/stores/keyboard/actions.test.ts index 88af6098c7..8a377fd9bc 100644 --- a/frontend/src/lib/stores/keyboard/actions.test.ts +++ b/frontend/src/lib/stores/keyboard/actions.test.ts @@ -335,6 +335,18 @@ describe("defaultActions", () => { expect(locationPath()).toBe( "/repo/browser?provider=gitlab&platform_host=gitlab.example.com&repo_path=group%2Fproject", ); + + window.history.replaceState( + null, + "", + "/?selected=commit:abcdef1234567890&provider=github&platform_host=github.com&repo_path=acme%2Fwidgets&branch=main", + ); + const commitContext = ctx("activity", { selectedPR: staleSelected }); + expect(action.when(commitContext)).toBe(true); + action.handler(commitContext); + expect(locationPath()).toBe( + "/repo/browser?provider=github&platform_host=github.com&repo_path=acme%2Fwidgets", + ); }); it("opens the repo browser from the route-selected issue before stale issue store state", () => {