Файли
openidea/src/features/todo/todo.test.ts
T

132 рядки
5.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { ensureSyntaxTree } from "@codemirror/language";
import { languages } from "@codemirror/language-data";
import type { FileMatches } from "@/platform/ipc";
import {
buildTodoQuery,
countItems,
DEFAULT_TODO_SETTINGS,
groupTodos,
itemsFromMatches,
mergeLive,
scanText,
todoRegexSource,
type TodoItem,
type TodoSettings,
} from "./model";
import { todoDecorations } from "./highlight";
const S = DEFAULT_TODO_SETTINGS;
describe("query", () => {
it("builds the keyword regex from enabled patterns", () => {
expect(todoRegexSource(S)).toBe("\\b(TODO|FIXME|HACK|XXX)\\b.*");
const q = buildTodoQuery({ ...S, caseSensitive: false }, { kind: "files", paths: ["/a"] })!;
expect(q).toMatchObject({ isRegex: true, caseSensitive: false, scope: { kind: "files", paths: ["/a"] } });
const none: TodoSettings = { ...S, patterns: S.patterns.map((p) => ({ ...p, enabled: false })) };
expect(buildTodoQuery(none)).toBeNull();
});
});
describe("matches → items", () => {
const fm: FileMatches = {
path: "/p/src/a.ts",
relPath: "src/a.ts",
matches: [
{ line: 3, text: " // TODO: fix this", ranges: [[5, 19]], textOffset: 0 },
{ line: 9, text: "const TODO_LIST = 1; /* FIXME later */", ranges: [[24, 38]], textOffset: 0 },
{ line: 12, text: 'const s = "TODO nope";', ranges: [[11, 21]], textOffset: 0 },
{ line: 20, text: "…x # HACK around", ranges: [[5, 16]], textOffset: 100 },
],
};
it("keeps comment matches with keyword, position and clean text", () => {
const items = itemsFromMatches(fm, S);
expect(items).toEqual([
{ path: "/p/src/a.ts", line: 3, col: 5, keyword: "TODO", text: "TODO: fix this" },
{ path: "/p/src/a.ts", line: 9, col: 24, keyword: "FIXME", text: "FIXME later" },
{ path: "/p/src/a.ts", line: 20, col: 105, keyword: "HACK", text: "HACK around" },
]);
});
it("includes non-comment matches when configured", () => {
expect(itemsFromMatches(fm, { ...S, commentsOnly: false })).toHaveLength(4);
});
});
describe("local scan", () => {
it("finds items with 0-based lines and columns", () => {
const text = "fn a() {}\n// todo lower\n // TODO: one\n/* XXX two */\nlet TODO = 1;";
expect(scanText("/f.rs", text, S)).toEqual([
{ path: "/f.rs", line: 2, col: 7, keyword: "TODO", text: "TODO: one" },
{ path: "/f.rs", line: 3, col: 3, keyword: "XXX", text: "XXX two" },
]);
const ci = scanText("/f.rs", text, { ...S, caseSensitive: false });
expect(ci.map((i) => [i.line, i.keyword])).toEqual([
[1, "TODO"],
[2, "TODO"],
[3, "XXX"],
]);
});
});
describe("grouping and filters", () => {
const item = (path: string, line: number, keyword: string): TodoItem => ({
path,
line,
col: 0,
keyword,
text: keyword,
});
const map = new Map<string, TodoItem[]>([
["/p/b.ts", [item("/p/b.ts", 5, "TODO"), item("/p/b.ts", 1, "FIXME")]],
["/p/a.ts", [item("/p/a.ts", 2, "HACK")]],
["/p/c.ts", []],
]);
it("groups by file sorted by path, items by line", () => {
const groups = groupTodos(map, { keywords: null });
expect(groups.map((g) => g.path)).toEqual(["/p/a.ts", "/p/b.ts"]);
expect(groups[1]!.items.map((i) => i.line)).toEqual([1, 5]);
expect(countItems(groups)).toBe(3);
});
it("filters by keyword and current file", () => {
expect(countItems(groupTodos(map, { keywords: new Set(["TODO"]) }))).toBe(1);
expect(groupTodos(map, { keywords: null, path: "/p/b.ts" }).map((g) => g.path)).toEqual(["/p/b.ts"]);
expect(groupTodos(map, { keywords: null, path: null })).toEqual([]);
expect(groupTodos(map, { keywords: new Set(["HACK"]), path: "/p/b.ts" })).toEqual([]);
});
it("layers live editor results over the scan", () => {
const merged = mergeLive(map, new Map([["/p/a.ts", []]]));
expect(countItems(groupTodos(merged, { keywords: null }))).toBe(2);
});
});
describe("editor highlighting", () => {
it("decorates TODO comments only", async () => {
const desc = languages.find((l) => l.name === "JavaScript")!;
const doc = 'const TODO = "TODO";\n// TODO: refactor\n/* FIXME now */';
const state = EditorState.create({ doc, extensions: [await desc.load()] });
ensureSyntaxTree(state, doc.length, 5000);
const view = new EditorView({ state, parent: document.body });
const ranges: Array<[number, number, string]> = [];
todoDecorations(view, S).between(0, doc.length, (from, to, d) => {
ranges.push([from, to, String(d.spec.class)]);
});
const line2 = doc.indexOf("TODO: refactor");
const fixme = doc.indexOf("FIXME");
ranges.sort((a, b) => a[0] - b[0] || a[2].localeCompare(b[2]));
expect(ranges).toEqual([
[line2, line2 + "TODO: refactor".length, "cm-openidea-todo"],
[line2, line2 + 4, "cm-openidea-todo-keyword"],
[fixme, fixme + "FIXME now".length, "cm-openidea-todo"],
[fixme, fixme + 5, "cm-openidea-todo-keyword"],
]);
view.destroy();
});
});