Files
kandev/apps/web/components/task/changes-panel-file-row.test.tsx
T
José Almeida 54ddb0aa1e feat(web): highlight active-tab file row in Changes panel (#954)
* feat(web): track active file path for diff panels

Extend resolveFilePath so activeFilePath updates when a file-diff panel
(preview:file-diff or diff:file:<path>) becomes the active dockview
panel. Also re-subscribe onDidParametersChange for the preview file-diff
slot so swapping the preview to a different file refreshes the path.

* feat(web): highlight active-tab file row in Changes panel

When a file's diff tab is the active dockview panel, mark the matching
row in unstaged, staged, and PR Changes lists with data-active="true"
and reuse the existing bg-accent/60 highlight (same as multi-select).
PRFileRow also gains data-changes-file so it shares the existing row
selector with FileRow.

* test(web): cover active-tab highlight unit + E2E

Unit test in FileRow guards the data-active attribute and bg-accent/60
class when isActive (with and without isSelected). New E2E spec drives
the full flow: open file-a diff, switch to file-b, close the preview,
and verify highlight follows when a file moves from unstaged to staged.

* fix(web): use distinct e2e task titles for active-tab highlight spec

clickTab("Changes") fails strict-mode when the task title also contains
the word "Changes" — Playwright matches both the session tab and the
dockview Changes tab. Renames to "Active Highlight (Staged) Test".

* test(web): close pinned diff:file panels in closeFileDiffPreview

The helper previously closed only the preview slot, leaving pinned
diff:file:<path> panels (created when a preview is promoted) active.
Sweeps every diff:file: panel so the row settles to data-active=false
regardless of how the diff was opened.

* test(web): unit-cover resolveFilePath panel-id branches

Exercises every branch of the dockview-store resolveFilePath closure:
file:, diff:file:, preview:file-editor, and preview:file-diff (the
last two via params.path), plus the null-out paths. Catches regressions
in the panel-id mapping without paying for an E2E round-trip.

* fix(web): strip "changes" from active-tab spec profile names

The agent profile name becomes the dockview session tab title, so a
profile named "changes-active-highlight" makes clickTab("Changes")
match two .dv-default-tab elements (session tab + Changes tab) and
trip strict-mode. Rename to "active-highlight" / "active-highlight-
staged" to keep the locator unambiguous.

* docs(web): note multi-repo path collision in PRFilesRepoGroup

Mirrors the FileListBody comment so future authors see the same caveat
in both file-list components: activeFilePath carries the path but not
the repo, so identical paths across repos light up both rows.

* fix(web): snapshot panels before closing in closeFileDiffPreview

dockview's api.panels is a live reference; calling panel.api.close()
removes the panel in place, so iterating the live array skipped every
other diff:file:* panel when two or more were open. Filter into a
local array before closing.
2026-05-19 15:53:48 +01:00

165 lines
5.5 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { render, fireEvent } from "@testing-library/react";
import { TooltipProvider } from "@kandev/ui/tooltip";
import { FileRow } from "./changes-panel-file-row";
const noop = () => {};
const noopSelect = () => false;
const baseFile = {
status: "modified" as const,
staged: false,
plus: 18,
minus: 3,
oldPath: undefined,
};
function renderRow(path: string) {
return render(
<TooltipProvider>
<ul>
<FileRow
file={{ ...baseFile, path }}
isPending={false}
onSelect={noopSelect}
onOpenDiff={noop}
onStage={noop}
onUnstage={noop}
onDiscard={noop}
onEditFile={noop}
/>
</ul>
</TooltipProvider>,
);
}
describe("FileRow truncation (regression: path overlaps diff stats in narrow panel)", () => {
it("file name span allows truncation so a long name does not overflow visually", () => {
// Bug: when the panel is narrow, a long file name renders past its container
// and overlaps the diff counts (+/-) on the right. Fix: file name span must be
// truncatable (truncate + min-w-0), not whitespace-nowrap+shrink-0.
const { container } = renderRow("deeply/nested/folder/render_text_to_image_controller_test.go");
const fileNameSpan = container.querySelector(
"span.font-medium.text-foreground",
) as HTMLElement | null;
expect(fileNameSpan).not.toBeNull();
expect(fileNameSpan!.className).toContain("truncate");
expect(fileNameSpan!.className).toContain("min-w-0");
// Must NOT prevent shrinking — that's what causes the overflow.
expect(fileNameSpan!.className).not.toContain("shrink-0");
});
it("folder span keeps truncate so the folder portion is shortened first", () => {
const { container } = renderRow("a/b/c/d/file.go");
const folderSpan = container.querySelector("span.text-foreground\\/60") as HTMLElement | null;
expect(folderSpan).not.toBeNull();
expect(folderSpan!.className).toContain("truncate");
});
it("right-side stats container has shrink-0 so it stays in place when path is long", () => {
// Without shrink-0 on the stats column, flex layout could squeeze the
// diff counts. Anchoring it ensures the file name truncates instead.
const { container } = renderRow("file.go");
// The stats container is the second direct child of the <li>
const li = container.querySelector("li") as HTMLElement | null;
expect(li).not.toBeNull();
const statsContainer = li!.children[1] as HTMLElement;
expect(statsContainer.className).toContain("shrink-0");
});
it("renders a file with no folder without crashing and exposes the file name", () => {
const { container } = renderRow("README.md");
const fileNameSpan = container.querySelector(
"span.font-medium.text-foreground",
) as HTMLElement | null;
expect(fileNameSpan?.textContent).toBe("README.md");
// No folder element when there's no slash in the path.
const folderSpan = container.querySelector("span.text-foreground\\/60");
expect(folderSpan).toBeNull();
});
it("passes diff source + repository context on open diff", () => {
const onOpenDiff = vi.fn();
const { container } = render(
<TooltipProvider>
<ul>
<FileRow
file={{ ...baseFile, path: "README.md", repositoryName: "frontend" }}
isPending={false}
onSelect={noopSelect}
onOpenDiff={onOpenDiff}
onStage={noop}
onUnstage={noop}
onDiscard={noop}
onEditFile={noop}
/>
</ul>
</TooltipProvider>,
);
const row = container.querySelector("[data-testid='file-row-README.md']") as HTMLElement | null;
expect(row).not.toBeNull();
fireEvent.click(row!);
expect(onOpenDiff).toHaveBeenCalledWith("README.md", {
source: "uncommitted",
repositoryName: "frontend",
});
});
});
describe("FileRow active-tab highlight", () => {
it("renders data-active='true' and bg-accent/60 when isActive", () => {
const { container } = render(
<TooltipProvider>
<ul>
<FileRow
file={{ ...baseFile, path: "active.ts" }}
isPending={false}
isActive
onSelect={noopSelect}
onOpenDiff={noop}
onStage={noop}
onUnstage={noop}
onDiscard={noop}
onEditFile={noop}
/>
</ul>
</TooltipProvider>,
);
const row = container.querySelector("[data-changes-file='active.ts']") as HTMLElement | null;
expect(row).not.toBeNull();
expect(row!.getAttribute("data-active")).toBe("true");
expect(row!.className).toContain("bg-accent/60");
});
it("keeps highlight when isActive and isSelected are both true", () => {
const { container } = render(
<TooltipProvider>
<ul>
<FileRow
file={{ ...baseFile, path: "both.ts" }}
isPending={false}
isActive
isSelected
onSelect={noopSelect}
onOpenDiff={noop}
onStage={noop}
onUnstage={noop}
onDiscard={noop}
onEditFile={noop}
/>
</ul>
</TooltipProvider>,
);
const row = container.querySelector("[data-changes-file='both.ts']") as HTMLElement | null;
expect(row).not.toBeNull();
expect(row!.getAttribute("data-active")).toBe("true");
expect(row!.getAttribute("data-selected")).toBe("true");
expect(row!.className).toContain("bg-accent/60");
});
});