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

A test pins opening a pull from the terminal: a fork's base defaults to its parent #6999

Merged opened this

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

Merged

scout/forge:scout/6983-a-test-pins-opening-a-pull-from-the-term → main 2 commits fork 1 thread · 1 unresolved

  • Merged. scout/6983-a-test-pins-opening-a-pull-from-the-term was merged into main as d5d9371.

Review

Sign in to review this change.
1 file changed +164 −0
test/tui-newpull.test.ts added +164
@@ -0,0 +1,164 @@
1 /** Opening a pull from the terminal: a fork's base is its parent; bad input is refused on the screen. */
2
3 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
4 import { mkdtemp, rm } from "node:fs/promises";
5 import { tmpdir } from "node:os";
6 import { join } from "node:path";
7 import { commitToBranch, createRepo, forkRepo } from "../src/actions/repos.ts";
8 import { createApp, type App as Forge } from "../src/app.ts";
9 import { listPulls } from "../src/core/pulls.ts";
10 import type { Repo } from "../src/core/repos.ts";
11 import { createUser, type User } from "../src/core/users.ts";
12 import { createForgeApp } from "../src/tui/forge.ts";
13 import { newPullScreen } from "../src/tui/screens/newpull.ts";
14 import { headlessTerminal } from "../src/tui/term/terminal.ts";
15 import { quietLogs } from "./helpers/globals.ts";
16 import { settleApp } from "./helpers/tui.ts";
17 import { promote } from "./helpers/members.ts";
18
19 quietLogs();
20
21 const must = <T>(r: { ok: true; value: T } | { ok: false; error: unknown }): T => {
22 if (!r.ok) throw new Error(JSON.stringify(r.error));
23 return r.value;
24 };
25
26 describe("the New pull screen", () => {
27 let dataDir: string;
28 let forge: Forge;
29 let alice: User;
30 let bob: User;
31 let upstream: Repo;
32 let fork: Repo;
33
34 function open(user: User, repo: Repo) {
35 const terminal = headlessTerminal(120, 40);
36 const { app, session } = createForgeApp(
37 forge,
38 { sshUsername: user.username, remoteAddress: "127.0.0.1", fingerprint: "SHA256:test", user },
39 { terminal, exitHooks: false },
40 );
41 app.start();
42 app.ctx.push(newPullScreen(session, app.ctx, repo));
43 const press = async (...keys: string[]): Promise<void> => {
44 for (const key of keys) app.dispatch({ type: "key", key, ctrl: false, alt: false, shift: false });
45 await settleApp(app);
46 };
47 const submit = async (): Promise<void> => {
48 app.dispatch({ type: "key", key: "d", ctrl: true, alt: false, shift: false });
49 await settleApp(app);
50 };
51 return { app, press, submit, text: () => app.screen.toText() };
52 }
53
54 const pullsOf = (repo: Repo) => listPulls(forge.db, repo.id, { state: "all" });
55
56 beforeAll(async () => {
57 dataDir = await mkdtemp(join(tmpdir(), "forge-tui-newpull-"));
58 forge = createApp({ dataDir, port: 0 });
59 alice = must(
60 await createUser(forge.db, {
61 origin: "seed",
62 trusted: true,
63 username: "alice",
64 email: "[email protected]",
65 password: "a-long-passphrase",
66 adminIfFirst: true,
67 }),
68 );
69 bob = must(
70 await createUser(forge.db, {
71 origin: "seed",
72 trusted: true,
73 username: "bob",
74 email: "[email protected]",
75 password: "a-long-passphrase",
76 }),
77 );
78 promote(forge.db, alice);
79 promote(forge.db, bob);
80 upstream = must(await createRepo(forge, alice, { name: "armature", visibility: "public" }));
81 must(
82 await commitToBranch(forge, alice, upstream, {
83 branch: upstream.defaultBranch,
84 message: "start",
85 files: [{ path: "README.md", content: "hi" }],
86 }),
87 );
88 fork = must(await forkRepo(forge, bob, upstream));
89 // The head defaults to the fork's default branch; give it something to propose.
90 must(
91 await commitToBranch(forge, bob, fork, {
92 branch: fork.defaultBranch,
93 message: "on the fork",
94 files: [{ path: "own.txt", content: "y" }],
95 }),
96 );
97 }, 60_000);
98
99 afterAll(async () => {
100 forge.close();
101 await rm(dataDir, { recursive: true, force: true });
102 });
103
104 test("a fork's base is its parent; submitting opens the pull there from the fork's branch", async () => {
105 const h = open(bob, fork);
106 try {
107 await settleApp(h.app);
108 expect(h.text()).toContain("alice/armature");
109 expect(h.text()).toContain(`bob/armature:${fork.defaultBranch}`);
110 await h.press("tab", "tab", "tab");
111 for (const ch of "Fix it") await h.press(ch);
112 await h.submit();

This asserts four spaces of column padding. Match the label and the value separately, or with a \s+ regex, so a layout change does not fail the test.

113 const made = pullsOf(upstream);
114 expect(made.length).toBe(1);
115 expect(made[0]!.title).toBe("Fix it");
116 expect(made[0]!.baseBranch).toBe(upstream.defaultBranch);
117 expect(made[0]!.headBranch).toBe(fork.defaultBranch);
118 expect(made[0]!.headRepoId).toBe(fork.id);
119 expect(pullsOf(fork).length).toBe(0);
120 } finally {
121 h.app.stop();
122 }
123 });
124
125 test("on a repository that is not a fork the base is its own default branch", async () => {
126 const h = open(alice, upstream);
127 try {
128 await settleApp(h.app);
129 expect(h.text()).toContain("alice/armature");
130 expect(h.text()).toContain(`Base branch ${upstream.defaultBranch}`);
131 expect(h.text()).toContain(`alice/armature:${upstream.defaultBranch}`);
132 } finally {
133 h.app.stop();
134 }
135 });
136
137 test("an empty title is refused on the screen and no pull is made", async () => {
138 const before = pullsOf(upstream).length;
139 const h = open(bob, fork);
140 try {
141 await settleApp(h.app);
142 await h.submit();
143 expect(h.text()).toContain("a title is required");
144 expect(pullsOf(upstream).length).toBe(before);
145 } finally {
146 h.app.stop();
147 }
148 });
149
150 test("head equal to base is refused on the screen and no pull is made", async () => {
151 const before = pullsOf(upstream).length;
152 const h = open(alice, upstream);
153 try {
154 await settleApp(h.app);
155 await h.press("tab", "tab", "tab");
156 for (const ch of "Same") await h.press(ch);
157 await h.submit();
158 expect(h.text()).toContain("the base and head branches are the same");
159 expect(pullsOf(upstream).length).toBe(before);
160 } finally {
161 h.app.stop();
162 }
163 });
164 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs