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

The new-forge repository picker stays fast and usable with 150 repositories #9063

Merged opened this

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

Merged

torque/forge:torque/8968-the-new-forge-repository-picker-stays-fa → main 2 commits fork

  • Merged. torque/8968-the-new-forge-repository-picker-stays-fa was merged into main as d31c475.

Review

Sign in to review this change.
1 file changed +95 −0
test/new-forge-pick-150.test.ts added +95
@@ -0,0 +1,95 @@
1 /** /forges/new with 150 repositories (#8968): search narrows, picks survive filtering, the page costs a bounded, counted amount of work (#7824). */
2
3 import { afterAll, beforeAll, 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 { createRepo } from "../src/actions/repos.ts";
8 import { createApp, type App } from "../src/app.ts";
9 import { createUser } from "../src/core/users.ts";
10 import { serve, type Servers } from "../src/http/server.ts";
11 import { pickJs } from "../src/web/pick.ts";
12 import { quietLogs } from "./helpers/globals.ts";
13 import { expectWork, requestStatements } from "./helpers/work.ts";
14
15 quietLogs();
16
17 const PASSWORD = "a-long-pick-150-passphrase";
18 let dir = "";
19 let app: App;
20 let servers: Servers;
21 let cookie = "";
22
23 beforeAll(async () => {
24 dir = await mkdtemp(join(tmpdir(), "forge-pick-150-"));
25 app = createApp({ dataDir: dir, url: "https://forge.test", port: 0 });
26 const u = await createUser(app.db, {
27 origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: PASSWORD, isAdmin: true,
28 });
29 if (!u.ok) throw new Error("user");
30 for (let i = 0; i < 150; i++) {
31 const r = await createRepo(app, u.value, { name: `app${i}` });
32 if (!r.ok) throw new Error("repo");
33 }
34 servers = await serve(app);
35 const si = await fetch(`${servers.url}/signin`, {
36 method: "POST",
37 body: new URLSearchParams({ username: "alice", password: PASSWORD }),
38 redirect: "manual",
39 });
40 cookie = (si.headers.get("set-cookie") ?? "").split(";")[0]!;
41 }, 120_000);
42
43 afterAll(async () => {
44 servers.stop();
45 app.close();
46 await rm(dir, { recursive: true, force: true });
47 });
48
49 test("the page with 150 repositories costs a handful of statements, not one per repository", async () => {
50 const url = `${servers.url}/forges/new`;
51 const text = await (await fetch(url, { headers: { cookie } })).text();
52 expect(text.match(/name="apps"/g)?.length).toBe(150);
53 const n = await requestStatements(app.db, url, { headers: { cookie } });
54 expectWork(n, { max: 20 });
55 });
56
57 test("picks made before a re-render stay ticked among the 150, and the count says so", async () => {
58 const res = await fetch(`${servers.url}/forges/new`, {
59 method: "POST",
60 headers: { cookie, origin: "https://forge.test" },
61 body: new URLSearchParams([["name", "kid"], ["apps", "alice/app3"], ["apps", "alice/app120"], ["do", "plan"]]),
62 });
63 const text = await res.text();
64 expect(text.match(/name="apps"[^>]*checked|checked[^>]*name="apps"/g)?.length).toBe(2);
65 expect(text).toContain("2 of 150 repositories picked");
66 });
67
68 test("searching narrows the 150 while the ticked ones stay ticked, shown as chips and still submitted", async () => {
69 const { Window } = await import("happy-dom");
70 const text = await (await fetch(`${servers.url}/forges/new`, { headers: { cookie } })).text();
71 const body = text.slice(text.indexOf('<fieldset class="field pick"'), text.indexOf("</fieldset>", text.indexOf('<fieldset class="field pick"')) + 11);
72 const doc = new Window().document as any;
73 doc.body.innerHTML = `<form>${body.replace('value="alice/app3"', 'value="alice/app3" checked').replace('value="alice/app120"', 'value="alice/app120" checked')}</form>`;
74 new Function("document", pickJs)(doc);
75 const Ev = doc.defaultView.Event;
76 const shown = () => [...doc.querySelectorAll("[data-pick-item]")].filter((e: any) => !e.hidden).length;
77 const type = (v: string) => {
78 const f = doc.querySelector("[data-pick-filter]");
79 f.value = v;
80 f.dispatchEvent(new Ev("input", { bubbles: true }));
81 };
82 type("app12"); // app12, app120..app129
83 expect(shown()).toBe(11);
84 type("app3"); // app3, app30..app39
85 expect(shown()).toBe(11);
86 type("app12");
87 // app3 is hidden by the search but stays ticked and is in the form's data.
88 const ticked = [...doc.querySelectorAll("input[name=apps]")].filter((i: any) => i.checked).map((i: any) => i.value);
89 expect(ticked.sort()).toEqual(["alice/app120", "alice/app3"]);
90 expect(doc.querySelectorAll(".pick-chip").length).toBe(2);
91 expect(doc.querySelector("[data-pick-count]").textContent).toBe("2 of 150 repositories picked; 11 shown");
92 type("");
93 expect(shown()).toBe(150);
94 expect(doc.querySelectorAll(".pick-chip").length).toBe(2);
95 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs