File size: 4,988 Bytes
fd2c364
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import { CommitList } from "#/components/features/diff-viewer/commit-list";
import type { GitCommit } from "#/api/open-hands.types";

vi.mock("react-i18next", () => ({
  useTranslation: () => ({
    t: (key: string, options?: { count?: number }) => {
      if (
        key === "DIFF_VIEWER$UNCOMMITTED_FILE_COUNT" &&
        typeof options?.count === "number"
      ) {
        return options.count === 1
          ? `${options.count} file`
          : `${options.count} files`;
      }
      return key;
    },
  }),
}));

vi.mock("#/hooks/query/use-commit-changes", () => ({
  useCommitChanges: () => ({
    data: undefined,
    isLoading: false,
    isSuccess: false,
  }),
}));

vi.mock("#/components/features/diff-viewer/diff-change-list", () => ({
  DiffChangeList: ({
    changes,
  }: {
    changes: Array<{ path: string; status: string }>;
  }) => (
    <div data-testid="diff-change-list">
      {changes.map((change) => (
        <div key={change.path}>{change.path}</div>
      ))}
    </div>
  ),
}));

const makeCommit = (overrides: Partial<GitCommit> = {}): GitCommit => ({
  sha: "a".repeat(40),
  shortSha: "aaaaaaa",
  subject: "add logging",
  author: "Agent",
  timestamp: "2026-07-10T12:00:00+07:00",
  ...overrides,
});

describe("CommitList", () => {
  it("renders an Uncommitted accordion row above the commit rows", () => {
    // Arrange / Act
    render(
      <CommitList
        commits={[makeCommit()]}
        hasMore={false}
        uncommittedChanges={[{ path: "src/a.ts", status: "M" }]}
      />,
    );

    // Assert
    expect(screen.getByTestId("uncommitted-changes-row")).toBeInTheDocument();
    expect(screen.getByText("DIFF_VIEWER$UNCOMMITTED")).toBeInTheDocument();
    expect(screen.getByTestId("uncommitted-changes-count")).toHaveTextContent(
      "1 file",
    );
    const rows = screen.getAllByTestId(/^(uncommitted-changes-row|commit-row)$/);
    expect(rows[0]).toHaveAttribute("data-testid", "uncommitted-changes-row");
  });

  it("pluralizes the Uncommitted file count", () => {
    // Arrange / Act
    render(
      <CommitList
        commits={[makeCommit()]}
        hasMore={false}
        uncommittedChanges={[
          { path: "src/a.ts", status: "M" },
          { path: "src/b.ts", status: "A" },
        ]}
      />,
    );

    // Assert
    expect(screen.getByTestId("uncommitted-changes-count")).toHaveTextContent(
      "2 files",
    );
  });

  it("expands Uncommitted into the working-tree file list", async () => {
    // Arrange
    const user = userEvent.setup();
    render(
      <CommitList
        commits={[makeCommit()]}
        hasMore={false}
        uncommittedChanges={[{ path: "src/a.ts", status: "M" }]}
      />,
    );

    // Act
    await user.click(screen.getByTestId("uncommitted-changes-row-toggle"));

    // Assert
    expect(await screen.findByText("src/a.ts")).toBeInTheDocument();
  });

  it("collapses Uncommitted when a commit row is expanded", async () => {
    // Arrange
    const user = userEvent.setup();
    render(
      <CommitList
        commits={[makeCommit()]}
        hasMore={false}
        uncommittedChanges={[{ path: "src/a.ts", status: "M" }]}
      />,
    );
    const uncommittedToggle = screen.getByTestId(
      "uncommitted-changes-row-toggle",
    );
    await user.click(uncommittedToggle);
    expect(uncommittedToggle).toHaveAttribute("aria-expanded", "true");
    expect(await screen.findByText("src/a.ts")).toBeInTheDocument();

    // Act
    await user.click(screen.getByTestId("commit-row-toggle"));

    // Assert — single-open accordion: Uncommitted collapses when a commit opens.
    expect(uncommittedToggle).toHaveAttribute("aria-expanded", "false");
    expect(screen.getByTestId("commit-row-toggle")).toHaveAttribute(
      "aria-expanded",
      "true",
    );
  });

  it("expands Uncommitted on request and clears the request", () => {
    // Arrange
    const onAutoExpandHandled = vi.fn();

    // Act
    render(
      <CommitList
        commits={[makeCommit()]}
        hasMore={false}
        uncommittedChanges={[{ path: "src/a.ts", status: "M" }]}
        autoExpandUncommitted
        onAutoExpandHandled={onAutoExpandHandled}
      />,
    );

    // Assert
    expect(
      screen.getByTestId("uncommitted-changes-row-toggle"),
    ).toHaveAttribute("aria-expanded", "true");
    expect(screen.getByText("src/a.ts")).toBeInTheDocument();
    expect(onAutoExpandHandled).toHaveBeenCalled();
  });

  it("still renders Uncommitted when there are no working-tree changes", () => {
    // Arrange / Act
    render(
      <CommitList
        commits={[makeCommit()]}
        hasMore={false}
        uncommittedChanges={[]}
      />,
    );

    // Assert
    expect(screen.getByTestId("uncommitted-changes-row")).toBeInTheDocument();
  });
});