Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,9 @@ vi.mock("../../state/use-atom-command", () => ({

import {
buildComposerSlashCommandItems,
composerSelectionAtEnd,
resolveComposerCommandSelection,
} from "./use-composer-command-menu";

describe("composerSelectionAtEnd", () => {
it("resets a changed draft owner to the new draft end", () => {
expect(composerSelectionAtEnd("queued task 🧪")).toEqual({ start: 14, end: 14 });
});
});

describe("mobile slash commands", () => {
const antigravity = {
driver: ProviderDriverKind.make("antigravity"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { matchesSlashSkillQuery } from "./composerSlashSkillSearch";

const WORKSPACE_SNAPSHOT_RETRY_COOLDOWN_MS = 10_000;

export function composerSelectionAtEnd(draftMessage: string): ComposerEditorSelection {
function composerSelectionAtEnd(draftMessage: string): ComposerEditorSelection {
return { start: draftMessage.length, end: draftMessage.length };
}

Expand Down
69 changes: 0 additions & 69 deletions apps/web/src/components/chat/changedFilesPresentation.test.ts

This file was deleted.

102 changes: 0 additions & 102 deletions apps/web/src/components/chat/changedFilesPresentation.ts

This file was deleted.

193 changes: 193 additions & 0 deletions apps/web/src/components/diffs/DiffFileTree.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import type { CodeViewScrollTarget } from "@pierre/diffs";
import type { FileTree as FileTreeModel } from "@pierre/trees";
import { FileTree } from "@pierre/trees/react";
import { act, type MouseEvent, type ReactNode } from "react";
import { create, type ReactTestRenderer } from "react-test-renderer";
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";

import { DiffFileTree, type DiffFileTreeEntry } from "./DiffFileTree";
import { useCodeViewFileReveal } from "./useCodeViewFileReveal";

vi.mock("../../hooks/useTheme", () => ({ useTheme: () => ({ resolvedTheme: "dark" }) }));
// Tooltip positioning is unrelated to the tree's actual model and activation path.
vi.mock("../ui/tooltip", () => ({
Tooltip: ({ children }: { children: ReactNode }) => children,
TooltipTrigger: ({ render }: { render: ReactNode }) => render,
TooltipPopup: () => null,
}));

const entries: DiffFileTreeEntry[] = [
{ path: "01-tall.ts", status: "modified" },
{ path: "02-short.ts", status: "modified" },
{ path: "03-medium.ts", status: "modified" },
];

class TreeRow {
constructor(readonly path: string) {}

getAttribute(name: string) {
return name === "data-item-path" ? this.path : null;
}
}

describe("diff tree file activation", () => {
let renderer: ReactTestRenderer | undefined;
const targets: CodeViewScrollTarget[] = [];
const viewer = {
getInstance: () => viewer,
scrollTo: (target: CodeViewScrollTarget) => targets.push(target),
};

function Panel({
files = entries,
selectedPath = null,
}: {
files?: DiffFileTreeEntry[];
selectedPath?: string | null;
}) {
const reveal = useCodeViewFileReveal(viewer, "working-tree");
return (
<DiffFileTree
entries={files}
ariaLabel="Working tree files"
selectedPath={selectedPath}
onSelectFile={(path) => reveal(`${path}\0${path}`)}
/>
);
}

const model = (): FileTreeModel => renderer!.root.findByType(FileTree).props.model;

async function mount(props: Parameters<typeof Panel>[0] = {}) {
await act(async () => {
renderer = create(<Panel {...props} />);
});
}

// Exercise T3's capture handler before the real Pierre model's selection transition.
// Only DOM hit testing is represented here; native pointer/keyboard dispatch and diff
// geometry are verified separately in the integrated client.
async function activate(path: string, modifiers: Partial<MouseEvent<HTMLElement>> = {}) {
const event = {
button: 0,
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
defaultPrevented: false,
nativeEvent: { composedPath: () => [{}, new TreeRow(path), {}] },
...modifiers,
} as MouseEvent<HTMLElement>;
await act(async () => {
renderer!.root
.find((node) => String(node.type) === "file-tree-container")
.props.onClickCapture?.(event);
const tree = model();
const item = tree.getItem(path)!;
if (event.ctrlKey || event.metaKey) {
item.toggleSelect();
} else {
for (const selected of tree.getSelectedPaths()) {
if (selected !== path) tree.getItem(selected)?.deselect();
}
item.select();
}
item.focus();
if ("toggle" in item && !event.ctrlKey && !event.metaKey && !event.shiftKey) item.toggle();
});
}

beforeEach(() => {
targets.length = 0;
vi.useFakeTimers();
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
vi.stubGlobal("HTMLElement", TreeRow);
});

afterEach(async () => {
await act(async () => renderer?.unmount());
renderer = undefined;
vi.runOnlyPendingTimers();
vi.useRealTimers();
vi.unstubAllGlobals();
});

it("reissues the reveal when the sole selected file is activated again", async () => {
await mount();
await activate("02-short.ts");
expect(model().getSelectedPaths()).toEqual(["02-short.ts"]);
await activate("02-short.ts");
expect(targets).toEqual([
{ type: "item", id: "02-short.ts\u000002-short.ts", align: "start" },
{ type: "item", id: "02-short.ts\u000002-short.ts", align: "start" },
]);
});

it("reveals newly selected files once in either direction", async () => {
await mount();
await activate("02-short.ts");
await activate("01-tall.ts");
await activate("03-medium.ts");
expect(targets.map((target) => ("id" in target ? target.id : null))).toEqual(
["02-short.ts", "01-tall.ts", "03-medium.ts"].map((path) => `${path}\0${path}`),
);
});

it("keeps focus-only navigation separate from button activation", async () => {
await mount();
await activate("02-short.ts");
await act(async () => model().getItem("01-tall.ts")!.focus());
expect(model().getSelectedPaths()).toEqual(["02-short.ts"]);
expect(targets).toHaveLength(1);
await activate("01-tall.ts", { detail: 0 });
await activate("01-tall.ts", { detail: 0 });
expect(targets).toHaveLength(3);
});

it.each(["ctrlKey", "metaKey"] as const)(
"does not reveal a selected file that a %s click deselects",
async (modifier) => {
await mount();
await activate("02-short.ts");
await activate("02-short.ts", { [modifier]: true });
expect(model().getSelectedPaths()).toEqual([]);
expect(targets).toHaveLength(1);
},
);

it("lets a click narrow multiple selected files without a second reveal", async () => {
await mount();
await activate("02-short.ts");
await act(async () => model().getItem("01-tall.ts")!.select());
expect(model().getSelectedPaths()).toHaveLength(2);
targets.length = 0;
await activate("02-short.ts");
expect(model().getSelectedPaths()).toEqual(["02-short.ts"]);
expect(targets).toHaveLength(1);
});

it("leaves directory selection and expansion to the tree", async () => {
await mount({ files: [{ path: "src/app.ts", status: "modified" }] });
const directory = model().getItem("src/")!;
if (!("isExpanded" in directory)) throw new Error("Expected the directory handle");
expect(directory.isExpanded()).toBe(true);
await activate("src/");
expect(directory.isExpanded()).toBe(false);
await activate("src/");
expect(directory.isExpanded()).toBe(true);
expect(targets).toEqual([]);
});

it("does not echo controlled selection, but lets the reader activate it", async () => {
await mount({ selectedPath: "02-short.ts" });
expect(model().getSelectedPaths()).toEqual(["02-short.ts"]);
expect(targets).toEqual([]);
await activate("02-short.ts");
expect(targets).toHaveLength(1);
await act(async () => {
renderer!.update(<Panel selectedPath="03-medium.ts" />);
});
expect(model().getSelectedPaths()).toEqual(["03-medium.ts"]);
expect(targets).toHaveLength(1);
});
});
Loading
Loading