Public: anyone can read this; the crew can change it.

A test pins the terminal's history: the log pages through a real "load more" row, and a commit opens its diff #7003

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6982-a-test-pins-the-terminal-s-history-the-l → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6982-a-test-pins-the-terminal-s-history-the-l was merged into main as 95c3243.

Review

Sign in to review this change.
1 file changed +176 −0
test/tui-commits.test.ts added +176
@@ -0,0 +1,176 @@
1 /**
2 * The terminal's history, against a real repository: the log pages through
3 * a real "Load more…" row, a commit opens its message and diff, and another
4 * branch shows its own log.
5 */
6
7 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8 import { mkdtemp, rm } from "node:fs/promises";
9 import { tmpdir } from "node:os";
10 import { join } from "node:path";
11 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
12 import { createApp, type App } from "../src/app.ts";
13 import { getRepo, type Repo } from "../src/core/repos.ts";
14 import type { User } from "../src/core/users.ts";
15 import type { Ctx, Screen } from "../src/tui/app/app.ts";
16 import { commitsScreen } from "../src/tui/screens/commits.ts";
17 import { local } from "../src/tui/screens/where.ts";
18 import { Session } from "../src/tui/session.ts";
19 import { Screen as Surface } from "../src/tui/term/screen.ts";
20 import { theme } from "../src/tui/ui/theme.ts";
21 import { seedAdmin } from "./helpers/admin.ts";
22 import { quietLogs } from "./helpers/globals.ts";
23
24 quietLogs();
25
26 /** The log's page size in src/view/local.ts. */
27 const PAGE = 50;
28 const TOTAL = PAGE + 7;
29
30 let dir: string;
31 let app: App;
32 let alice: User;
33 let repo: Repo;
34
35 const W = 110;
36 const H = 80;
37
38 /** A stand-in app: records pushed screens, lets the test await loads. */
39 function harness() {
40 const pending: Promise<unknown>[] = [];
41 const pushed: Screen[] = [];
42 const toasts: string[] = [];
43 const ctx = {
44 theme: theme("dark"),
45 push: (s: Screen) => void pushed.push(s),
46 pop: () => {},
47 replace: () => {},
48 invalidate: () => {},
49 toast: (t: string) => void toasts.push(t),
50 hit: () => {},
51 track: <T>(p: Promise<T>) => {
52 pending.push(p);
53 return p;
54 },
55 } as unknown as Ctx;
56 const f = new Session(app, {
57 sshUsername: "t",
58 remoteAddress: "127.0.0.1",
59 user: alice,
60 });
61 const settle = async () => {
62 while (pending.length > 0) await pending.shift();
63 };
64 const draw = (screen: Screen): string => {
65 const s = new Surface(W, H);
66 screen.render({ x: 0, y: 0, w: W, h: H }, s);
67 return s.toText();
68 };
69 const press = (screen: Screen, key: string, n = 1) => {
70 for (let i = 0; i < n; i++)
71 screen.key({ type: "key", key, ctrl: false, alt: false, shift: false });
72 };
73 const open = async (ref: string) => {
74 const screen = commitsScreen(f, ctx, local(f, repo), ref);
75 await screen.load!(new AbortController().signal);
76 return screen;
77 };
78 return { ctx, f, pushed, toasts, settle, draw, press, open };
79 }
80
81 const subjects = (text: string): number[] =>
82 [...text.matchAll(/change #(\d+)/g)].map((m) => Number(m[1]));
83
84 beforeAll(async () => {
85 dir = await mkdtemp(join(tmpdir(), "forge-tui-commits-"));
86 app = createApp({ dataDir: dir, port: 0 });
87 const a = await seedAdmin(app, {
88 username: "alice",
89 email: "[email protected]",
90 password: "a-long-passphrase",
91 });
92 if (!a.ok) throw new Error("signup failed");
93 alice = a.value;
94 const made = await createRepo(app, alice, { name: "log", visibility: "public" });
95 if (!made.ok) throw new Error("repo creation failed");
96 repo = getRepo(app.db, "alice", "log")!;
97 for (let i = 1; i <= TOTAL; i++) {
98 const r = await commitToBranch(app, alice, repo, {
99 message: `change #${i}`,
100 files: [{ path: "counter.txt", content: `count ${i}\n` }],
101 });
102 if (!r.ok) throw new Error(`commit ${i} failed: ${JSON.stringify(r.error)}`);
103 }
104 const side = await commitToBranch(app, alice, repo, {
105 branch: "side",
106 message: "side only work",
107 files: [{ path: "side.txt", content: "on the side\n" }],
108 });
109 if (!side.ok) throw new Error("side branch failed");
110 }, 120_000);
111
112 afterAll(async () => {
113 app.close();
114 await rm(dir, { recursive: true, force: true });
115 });

Vacuous risk: if toText pads rows with spaces, \n right after the number never matches. Use expect(seen).toHaveLength(PAGE).

116
117 describe("the log pages through a real row", () => {
118 test("the first page is newest first and ends in 'Load more…'", async () => {
119 const h = harness();
120 const screen = await h.open(repo.defaultBranch);
121 const text = h.draw(screen);
122 const seen = subjects(text);
123 expect(seen[0]).toBe(TOTAL);
124 expect(seen).toEqual([...seen].sort((a, b) => b - a));
125 expect(text).toContain("Load more…");
126 expect(text).not.toContain(`change #${TOTAL - PAGE}\n`);
127 });
128
129 test("the cursor lands on it and choosing it appends the rest, none twice, none missing", async () => {
130 const h = harness();
131 const screen = await h.open(repo.defaultBranch);
132 const first = subjects(h.draw(screen));
133 // PAGE commits, then the row: moving down PAGE times is the row.
134 h.press(screen, "j", PAGE);
135 h.press(screen, "enter");
136 await h.settle();
137 expect(h.pushed).toHaveLength(0);
138 const text = h.draw(screen);
139 const all = subjects(text);
140 expect(first.length).toBeLessThan(all.length);
141 expect(all).toHaveLength(new Set(all).size);
142 expect(all.length).toBeGreaterThanOrEqual(TOTAL - 1); // the initial commit has another subject
143 const want = Array.from({ length: all.length }, (_, i) => TOTAL - i);
144 expect(all).toEqual(want);
145 expect(text).not.toContain("Load more…");
146 });
147 });
148
149 describe("a commit opens", () => {
150 test("choosing it shows its message and its diff", async () => {
151 const h = harness();
152 const screen = await h.open(repo.defaultBranch);
153 h.press(screen, "j", 2);
154 h.press(screen, "enter");
155 expect(h.pushed).toHaveLength(1);
156 const detail = h.pushed[0]!;
157 await detail.load!(new AbortController().signal);
158 const text = h.draw(detail);
159 expect(text).toContain(`change #${TOTAL - 2}`);
160 expect(text).toContain("counter.txt");
161 expect(text).toContain(`- count ${TOTAL - 3}`);
162 expect(text).toContain(`+ count ${TOTAL - 2}`);
163 });
164 });
165
166 describe("a branch other than the default", () => {
167 test("shows its own log", async () => {
168 const h = harness();
169 const screen = await h.open("side");
170 const text = h.draw(screen);
171 expect(text).toContain("side only work");
172 expect(text).toContain(`change #${TOTAL}`);
173 const main = h.draw(await h.open(repo.defaultBranch));
174 expect(main).not.toContain("side only work");
175 });
176 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs