Files
Robert Brennanandopenhands 0068b5f465 chore: address post-merge feedback on #284 + small cleanups (#288)
Three of the four post-merge review comments on #284 are still real:

- `src/utils/file-priority.ts` — `pathDepth` used `split('/').length - 1`
  without filtering empty segments, so a leading slash, trailing slash
  or double slash silently inflated the computed depth and dropped a
  top-level file behind genuinely-nested ones in the Files tab pill row.
  Aligned with the `.filter(Boolean)` convention already used by
  `buildFileTree`, with regression tests for both the leading-slash and
  double-slash cases.

- `src/utils/conversation-local-storage.ts` — `filesTabContentViewMode`
  is typed `ViewMode` ("rich" | "plain") but came from
  `JSON.parse(localStorage)`, so a corrupt / hand-edited value would
  leak past TypeScript into the UI. `sanitizeStoredState` now drops
  unknown values so the typed default re-applies, with a test that
  asserts `{ filesTabContentViewMode: 'fancy' }` falls back to 'rich'.

- `src/routes/files-tab.tsx` — the toolbar refresh button used
  `aria-label={t(I18nKey.COMMON$FILES)}` which translates to just
  "Files". Screen-reader users would hear the button as
  "Files button" instead of "Refresh files". Added a dedicated
  `FILES$REFRESH` translation ("Refresh files" + the 14 sibling
  locales), wired the button's aria-label and title to it, and updated
  the existing test to assert the new label.

The fourth comment (id-less events re-processed on every effect run in
`use-auto-refresh-files-on-edit`) was already addressed in a follow-up
commit via a `WeakSet<OHEvent>` keyed by object identity, so no change
needed there.

Also: `src/components/shared/buttons/refresh-button.tsx` and its
companion `src/icons/refresh.svg` had zero references in the codebase
(the only refresh button left in use is the inline one in
`routes/files-tab.tsx` with `u-refresh.svg`). Deleted.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-10 19:57:28 -07:00

94 lines
3.3 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { sortFilesByPriority, filePriorityScore } from "#/utils/file-priority";
describe("file-priority", () => {
it("places index.html before other files", () => {
const sorted = sortFilesByPriority([
"src/utils/helpers.ts",
"src/index.html",
"src/components/widget.tsx",
]);
expect(sorted[0]).toBe("src/index.html");
});
it("places README.md before generic source files", () => {
const sorted = sortFilesByPriority([
"src/components/widget.tsx",
"README.md",
"src/utils/helpers.ts",
]);
expect(sorted[0]).toBe("README.md");
});
it("prefers top-level index.html over a nested one", () => {
const sorted = sortFilesByPriority(["src/nested/index.html", "index.html"]);
expect(sorted[0]).toBe("index.html");
expect(sorted[1]).toBe("src/nested/index.html");
});
it("prefers a shallower path even when the deeper one is more 'important'", () => {
// README.md (depth 0) outranks foo/bar/index.html (depth 2) despite
// index.html being a higher-priority basename — the user almost always
// cares more about top-level files first.
const sorted = sortFilesByPriority(["foo/bar/index.html", "README.md"]);
expect(sorted[0]).toBe("README.md");
expect(sorted[1]).toBe("foo/bar/index.html");
});
it("ranks index.html above README.md at the same depth", () => {
const sorted = sortFilesByPriority(["README.md", "index.html"]);
expect(sorted[0]).toBe("index.html");
expect(sorted[1]).toBe("README.md");
});
it("falls back to alphabetical order for unimportant files", () => {
const sorted = sortFilesByPriority([
"src/zeta.ts",
"src/alpha.ts",
"src/mu.ts",
]);
expect(sorted).toEqual(["src/alpha.ts", "src/mu.ts", "src/zeta.ts"]);
});
it("scores high-priority basenames lower than generic files", () => {
expect(filePriorityScore("package.json")).toBeLessThan(
filePriorityScore("src/some-helper.ts"),
);
});
it("does not mutate the input array", () => {
const input = ["b.ts", "a.ts"];
const original = [...input];
sortFilesByPriority(input);
expect(input).toEqual(original);
});
// Regression for the depth-calculation bug where empty path segments
// produced by leading / trailing / double slashes (e.g. agent-server
// payloads that happen to start with `/`) inflated the computed depth
// and caused a top-level file to sort below an actually-nested one.
describe("depth calculation tolerates non-canonical path forms", () => {
it("treats a leading-slash path the same as the unprefixed form", () => {
const sorted = sortFilesByPriority([
"src/nested/index.html",
"/index.html",
]);
expect(sorted[0]).toBe("/index.html");
expect(sorted[1]).toBe("src/nested/index.html");
});
it("collapses double slashes when computing depth", () => {
// `src//index.html` should sort identically to `src/index.html` (both
// depth 1 — one nesting level under root), beating a genuinely
// deeper path even though `index.html` is high-priority either way.
const sorted = sortFilesByPriority([
"deeply/nested/index.html",
"src//index.html",
]);
expect(sorted[0]).toBe("src//index.html");
expect(sorted[1]).toBe("deeply/nested/index.html");
});
});
});