diff --git a/ui/src/components/IssueDocumentsSection.test.tsx b/ui/src/components/IssueDocumentsSection.test.tsx index 99f0cbd157f..6c3ec8ab66f 100644 --- a/ui/src/components/IssueDocumentsSection.test.tsx +++ b/ui/src/components/IssueDocumentsSection.test.tsx @@ -119,6 +119,35 @@ vi.mock("@/components/ui/dropdown-menu", async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; +const localStorageEntries = new Map(); + +function ensureLocalStorageMock() { + if ( + typeof window.localStorage?.getItem === "function" + && typeof window.localStorage?.setItem === "function" + && typeof window.localStorage?.removeItem === "function" + && typeof window.localStorage?.clear === "function" + ) { + return; + } + + Object.defineProperty(window, "localStorage", { + configurable: true, + value: { + getItem: (key: string) => localStorageEntries.get(key) ?? null, + setItem: (key: string, value: string) => { + localStorageEntries.set(key, value); + }, + removeItem: (key: string) => { + localStorageEntries.delete(key); + }, + clear: () => { + localStorageEntries.clear(); + }, + }, + }); +} + function deferred() { let resolve!: (value: T) => void; const promise = new Promise((res) => { @@ -221,6 +250,7 @@ describe("IssueDocumentsSection", () => { beforeEach(() => { container = document.createElement("div"); document.body.appendChild(container); + ensureLocalStorageMock(); window.localStorage.clear(); vi.clearAllMocks(); markdownEditorMockState.emitMountEmptyChange = false; @@ -311,6 +341,79 @@ describe("IssueDocumentsSection", () => { queryClient.clear(); }); + it("returns from a historical preview when the current revision only exists in derived state", async () => { + const currentDocument = createIssueDocument({ + body: "Current plan body", + latestRevisionId: "revision-4", + latestRevisionNumber: 4, + updatedAt: new Date("2026-03-31T12:05:00.000Z"), + }); + const issue = createIssue(); + const root = createRoot(container); + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + mutations: { + retry: false, + }, + }, + }); + + mockIssuesApi.listDocuments.mockResolvedValue([currentDocument]); + queryClient.setQueryData( + queryKeys.issues.documentRevisions(issue.id, "plan"), + [ + createRevision({ + id: "revision-3", + revisionNumber: 3, + body: "Historical plan body", + createdAt: new Date("2026-03-31T11:00:00.000Z"), + }), + ], + ); + + await act(async () => { + root.render( + + + , + ); + }); + await flush(); + await flush(); + + expect(container.textContent).toContain("Current plan body"); + + const revisionButtons = Array.from(container.querySelectorAll("button")); + const historicalRevisionButton = revisionButtons.find((button) => button.textContent?.includes("rev 3")); + expect(historicalRevisionButton).toBeTruthy(); + + await act(async () => { + historicalRevisionButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + expect(container.textContent).toContain("Viewing revision 3"); + expect(container.textContent).toContain("Historical plan body"); + + const currentRevisionButton = Array.from(container.querySelectorAll("button")) + .find((button) => button.textContent?.includes("rev 4")); + expect(currentRevisionButton).toBeTruthy(); + + await act(async () => { + currentRevisionButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + expect(container.textContent).not.toContain("Viewing revision 3"); + expect(container.textContent).toContain("Current plan body"); + + await act(async () => { + root.unmount(); + }); + queryClient.clear(); + }); + it("ignores mount-time editor change noise before a document is actively being edited", async () => { markdownEditorMockState.emitMountEmptyChange = true; diff --git a/ui/src/components/IssueDocumentsSection.tsx b/ui/src/components/IssueDocumentsSection.tsx index 44de75192af..fe8b65a11ad 100644 --- a/ui/src/components/IssueDocumentsSection.tsx +++ b/ui/src/components/IssueDocumentsSection.tsx @@ -537,13 +537,13 @@ export function IssueDocumentsSection({ }, []); const previewRevision = useCallback((doc: IssueDocument, revisionId: string) => { - const revisions = getDocumentRevisions(doc.key); - const selectedRevision = revisions.find((revision) => revision.id === revisionId); - if (!selectedRevision) return; - if (selectedRevision.id === doc.latestRevisionId) { + if (revisionId === doc.latestRevisionId) { returnToLatestRevision(doc.key); return; } + const revisions = getDocumentRevisions(doc.key); + const selectedRevision = revisions.find((revision) => revision.id === revisionId); + if (!selectedRevision) return; if (documentConflict?.key === doc.key || documentHasUnsavedChanges(doc, draft)) { setError("Save or cancel your local changes before viewing an older revision."); return;