Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 90 additions & 20 deletions apps/web/src/components/pullRequest/PullRequestCodeTab.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CodeViewDiffItem } from "@pierre/diffs/react";
import { EnvironmentId, ProjectId, type PullRequestDetailView } from "@t3tools/contracts";
import { act, type ComponentProps, type ReactNode } from "react";
import { create, type ReactTestRenderer } from "react-test-renderer";
Expand All @@ -16,19 +17,42 @@ vi.mock("~/state/pullRequests", () => ({
vi.mock("@effect/atom-react", () => ({ useAtomRefresh: () => refresh }));
vi.mock("~/hooks/useTheme", () => ({ useTheme: () => ({ resolvedTheme: "dark" }) }));
vi.mock("~/hooks/useSettings", () => ({
useClientSettings: () => ({ diffLayout: "unified", diffFilesCollapsed: false, wordWrap: false }),
useClientSettings: () => ({ diffLayout: "unified", wordWrap: false }),
useUpdateClientSettings: () => command,
}));
vi.mock("~/hooks/useLocalStorage", () => ({
useLocalStorage: (_key: string, initial: unknown) => [initial, command],
}));
vi.mock("../diffs/EditableDiffCodeView", () => ({
EditableDiffCodeView: ({ renderCodeViewFooter }: { renderCodeViewFooter: () => ReactNode }) =>
renderCodeViewFooter(),
EditableDiffCodeView: ({
items,
renderHeaderPrefix,
renderCodeViewFooter,
}: {
items: readonly CodeViewDiffItem[];
renderHeaderPrefix: (item: CodeViewDiffItem) => ReactNode;
renderCodeViewFooter: () => ReactNode;
}) => (
<>
{items.map((item) => (
<section key={item.id}>
{renderHeaderPrefix(item)}
{item.collapsed ? null : <pre>{item.fileDiff.additionLines.join("")}</pre>}
</section>
))}
{renderCodeViewFooter()}
</>
),
}));
vi.mock("../ui/button", () => ({
Button: (props: ComponentProps<"button">) => <button {...props} />,
}));
vi.mock("../ui/menu", () => ({
DropdownMenu: ({ children }: { children: ReactNode }) => children,
DropdownMenuContent: ({ children }: { children: ReactNode }) => children,
DropdownMenuTrigger: (props: ComponentProps<"button">) => <button {...props} />,
DropdownMenuItem: (props: ComponentProps<"button">) => <button {...props} />,
}));
vi.mock("../ui/tooltip", () => ({
Tooltip: ({ children }: { children: ReactNode }) => children,
TooltipTrigger: ({ render, children }: { render?: ReactNode; children?: ReactNode }) => (
Expand All @@ -54,6 +78,23 @@ vi.mock("../ui/toggle-group", () => ({
import PullRequestCodeTab from "./PullRequestCodeTab";

let renderer: ReactTestRenderer;
const detail = {
body: "",
number: 1,
updatedAt: "2026-09-13",
commits: [],
reviewThreads: [],
capabilities: { review: { inlineComment: false, reply: false, resolve: false, verdicts: [] } },
viewerPermissions: { comment: false, resolve: false, verdicts: [] },
} as unknown as PullRequestDetailView;
const click = async (label: string) => {
await act(async () =>
renderer.root
.findAllByType("button")
.find((button) => button.props["aria-label"] === label)!
.props.onClick(new Event("click")),
);
};
afterEach(async () => {
await act(async () => renderer?.unmount());
vi.unstubAllGlobals();
Expand Down Expand Up @@ -87,15 +128,6 @@ it("loads guide pages only on request and resumes scroll loading outside the gui
isPending: false,
refresh,
}));
const detail = {
body: "",
number: 1,
updatedAt: "2026-09-13",
commits: [],
reviewThreads: [],
capabilities: { review: { inlineComment: false, reply: false, resolve: false, verdicts: [] } },
viewerPermissions: { comment: false, resolve: false, verdicts: [] },
} as unknown as PullRequestDetailView;
await act(async () => {
renderer = create(
<PullRequestCodeTab
Expand All @@ -109,14 +141,6 @@ it("loads guide pages only on request and resumes scroll loading outside the gui
{ createNodeMock: () => ({}) },
);
});
const click = async (label: string) => {
await act(async () =>
renderer.root
.findAllByType("button")
.find((button) => button.props["aria-label"] === label)!
.props.onClick(),
);
};
expect(observers.size).toBe(1);
await click("Guided review");
await act(async () => {
Expand All @@ -134,3 +158,49 @@ it("loads guide pages only on request and resumes scroll loading outside the gui
});
expect(query.mock.lastCall?.[0].input.cursor).toBe("2");
});

it("opens the guide file in each commit scope and keeps manual collapses within that scope", async () => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
const pages = ["all", "commit"].map((contents) => ({
patch: `diff --git a/file.ts b/file.ts\n--- a/file.ts\n+++ b/file.ts\n@@ -1 +1 @@\n-old\n+${contents}\n`,
truncated: false,
nextCursor: null,
omittedFileStats: [],
}));
query.mockImplementation(({ input }: { input: { commit?: string } }) => ({
data: pages[input.commit ? 1 : 0],
error: null,
isPending: false,
refresh,
}));
const view = (commit: string | null, actionPending = false) => (
<PullRequestCodeTab
environmentId={EnvironmentId.make("test")}
reference={{ projectId: ProjectId.make("project"), repository: "owner/repo", number: 1 }}
detail={{
...detail,
commits: [{ oid: "commit", messageHeadline: "Commit", committedDate: "2026-09-13" }],
}}
selectedCommitOid={commit}
onSelectedCommitChange={command}
onRefresh={refresh}
actionPending={actionPending}
/>
);
await act(async () => {
renderer = create(view(null));
});
await click("Collapse all files");
expect(renderer.root.findAllByType("pre")).toHaveLength(0);
await click("Guided review");
expect(renderer.root.findByType("pre").children).toEqual(["all"]);
await click("Collapse diff");
expect(renderer.root.findAllByType("pre")).toHaveLength(0);
await act(async () => renderer.update(view("commit")));
expect(renderer.root.findByType("pre").children).toEqual(["commit"]);
await click("Collapse all files");
await act(async () => renderer.update(view("commit", true)));
expect(renderer.root.findAllByType("pre")).toHaveLength(0);
await act(async () => renderer.update(view(null)));
expect(renderer.root.findByType("pre").children).toEqual(["all"]);
});
18 changes: 16 additions & 2 deletions apps/web/src/components/pullRequest/PullRequestCodeTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ import {
} from "lucide-react";
import { useAtomRefresh } from "@effect/atom-react";
import * as Schema from "effect/Schema";
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import {
useCallback,
useEffect,
useEffectEvent,
useMemo,
useRef,
useState,
type ReactNode,
} from "react";

import { useLocalStorage } from "~/hooks/useLocalStorage";
import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings";
Expand Down Expand Up @@ -668,9 +676,15 @@ function PullRequestCodeTab({
if (item) revealFile(resolveFileDiffPath(item.fileDiff));
else if (nextCursor !== null) loadNextSlice();
};
const expandGuideFile = useEffectEvent(() => {
if (guideItem?.collapsed) toggleFile(guideItem.id);
});
const guideFileId = guideItem?.id;
useEffect(() => {
if (guided && guideFileId) requestTreeReveal(guideFileId);
if (guided && guideFileId) {
expandGuideFile();
requestTreeReveal(guideFileId);
}
}, [guided, guideFileId, requestTreeReveal]);

const treeRef = useRef<DiffFileTreeHandle>(null);
Expand Down
Loading