Skip to content
170 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import {
5 FALLBACK_FOCUS_MS,
6 FALLBACK_POLL_MS,
7 HIDDEN_MIN_MS,
8 LIVE_DEBOUNCE_MS,
9 LIVE_HOLD_CHECK_MS,
10 LIVE_MAX_WAIT_MS,
11 type RepoChange,
12 atBottom,
13 branchOf,
14 fallbackDue,
15 gate,
16 isTyping,
17 newCommits,
18 repoPages,
19 tagOf,
20} from "./repo-live.ts";
21
22const AT = "2026-10-11T10:00:00Z";
23const NOW = Date.parse(AT);
24
25function change(kind: string, more: Partial<RepoChange> = {}): RepoChange {
26 return { repo_id: "rep_1", repo: "acme/rocket", kind, at: AT, ...more };
27}
28
29const push = (branch: string, commits?: number) => change("git.push", { ref: `refs/heads/${branch}`, sha: "abc", commits: commits ?? null });
30
31test("refs name their branch or tag", () => {
32 assert.equal(branchOf("refs/heads/feature/x"), "feature/x");
33 assert.equal(branchOf("refs/tags/v1"), null);
34 assert.equal(tagOf("refs/tags/v1.2"), "v1.2");
35 assert.equal(tagOf(null), null);
36});
37
38test("the files and overview pages follow refs, checks, releases and deployments", () => {
39 const files = repoPages.files();
40 for (const c of [push("main"), push("other"), change("git.deleted", { ref: "refs/heads/old" }), change("status.created", { sha: "abc" }), change("release.published"), change("deployment_status.created")]) {
41 assert.equal(files(c), true, c.kind);
42 }
43 assert.equal(files(change("issue.opened", { number: 3 })), false);
44 assert.equal(files(change("comment.created", { number: 3 })), false);
45});
46
47test("the commits page hears pushes to its own branch only", () => {
48 const commits = repoPages.commits("main");
49 assert.equal(commits(push("main")), true);
50 assert.equal(commits(push("feature")), false);
51 assert.equal(commits(change("git.push", { ref: "refs/tags/v1" })), false);
52 assert.equal(commits(change("workflow.completed", { sha: "abc", run: "run_1" })), true, "checks on its commits");
53 assert.equal(commits(change("pull.opened", { number: 1 })), false);
54});
55
56test("branches, tags, and the lists of pull requests and issues", () => {
57 assert.equal(repoPages.refs()(push("x")), true);
58 assert.equal(repoPages.refs()(change("pull.opened", { number: 1 })), true);
59 assert.equal(repoPages.refs()(change("issue.opened", { number: 1 })), false);
60 assert.equal(repoPages.tags()(change("git.push", { ref: "refs/tags/v2" })), true);
61 assert.equal(repoPages.tags()(push("main")), false);
62 assert.equal(repoPages.tags()(change("release.published")), true);
63 assert.equal(repoPages.pulls()(change("pull.updated", { number: 4 })), true);
64 assert.equal(repoPages.pulls()(change("review.completed", { number: 4 })), true);
65 assert.equal(repoPages.pulls()(change("issue.opened", { number: 5 })), false);
66 assert.equal(repoPages.issues()(change("issue.closed", { number: 5 })), true);
67 assert.equal(repoPages.issues()(change("comment.created", { number: 5 })), true);
68 assert.equal(repoPages.issues()(change("pull.merged", { number: 6 })), true, "a merge may close issues");
69 assert.equal(repoPages.issues()(push("main")), false);
70});
71
72test("a pull request hears its own changes, its head branch and its head's checks", () => {
73 const pull = repoPages.pull(7, { branch: "feature", sha: "f00d" });
74 assert.equal(pull(change("pull.updated", { number: 7 })), true);
75 assert.equal(pull(change("comment.created", { number: 7 })), true);
76 assert.equal(pull(change("review.completed", { number: 7 })), true);
77 assert.equal(pull(change("checks.completed", { number: 7, sha: "f00d" })), true);
78 assert.equal(pull(change("pull.merged", { number: 7 })), true);
79 assert.equal(pull(push("feature")), true, "new commits");
80 assert.equal(pull(change("status.created", { sha: "f00d" })), true);
81 assert.equal(pull(change("run.updated", { run: "run_1", number: 7 })), true, "a run for it");
82 assert.equal(pull(change("comment.created", { number: 8 })), false);
83 assert.equal(pull(push("main")), false);
84 assert.equal(pull(change("status.created", { sha: "beef" })), false);
85});
86
87test("an issue hears its own changes and its linked pull requests", () => {
88 const issue = repoPages.issue(3, [9]);
89 assert.equal(issue(change("comment.created", { number: 3 })), true);
90 assert.equal(issue(change("issue.closed", { number: 3 })), true);
91 assert.equal(issue(change("pull.merged", { number: 9 })), true);
92 assert.equal(issue(change("comment.created", { number: 4 })), false);
93 assert.equal(issue(push("main")), false);
94});
95
96test("runs, one run, and security", () => {
97 assert.equal(repoPages.runs()(change("run.updated", { run: "run_1" })), true);
98 assert.equal(repoPages.runs()(change("status.created", { sha: "abc" })), true, "a new run reports pending on its commit");
99 assert.equal(repoPages.runs()(change("issue.opened", { number: 1 })), false);
100 const run = repoPages.run("run_1", "abc");
101 assert.equal(run(change("run.updated", { run: "run_1" })), true);
102 assert.equal(run(change("workflow.completed", { run: "run_1" })), true);
103 assert.equal(run(change("run.updated", { run: "run_2" })), false);
104 assert.equal(run(change("status.created", { sha: "abc" })), true);
105 assert.equal(repoPages.security()(change("secret_scanning_alert.created")), true);
106 assert.equal(repoPages.security()(change("code_scanning_alert.fixed", { number: 2 })), true);
107 assert.equal(repoPages.security()(change("issue.opened", { number: 1 })), false);
108});
109
110test("new commits are counted, and uncounted when a push was not", () => {
111 assert.equal(newCommits([push("main", 2), push("main", 3), push("other", 9)], "main"), 5);
112 assert.equal(newCommits([push("main", 2), push("main")], "main"), null);
113 assert.equal(newCommits([change("status.created")], "main"), 0);
114});
115
116test("a change waits a second for more, five at most, then refreshes", () => {
117 const idle = { now: NOW, visible: true, busy: false, navigating: false };
118 assert.deepEqual(gate({ pendingSince: null, lastChangeAt: null, lastRunAt: null }, idle), { action: "idle" });
119 assert.deepEqual(gate({ pendingSince: NOW, lastChangeAt: NOW, lastRunAt: null }, idle), { action: "wait", ms: LIVE_DEBOUNCE_MS });
120 assert.deepEqual(gate({ pendingSince: NOW, lastChangeAt: NOW, lastRunAt: null }, { ...idle, now: NOW + LIVE_DEBOUNCE_MS }), { action: "run" });
121 // A steady stream: refreshed after five seconds anyway.
122 const stream = { pendingSince: NOW, lastChangeAt: NOW + LIVE_MAX_WAIT_MS - 100, lastRunAt: null };
123 assert.deepEqual(gate(stream, { ...idle, now: NOW + LIVE_MAX_WAIT_MS - 50 }), { action: "wait", ms: 50 });
124 assert.deepEqual(gate(stream, { ...idle, now: NOW + LIVE_MAX_WAIT_MS }), { action: "run" });
125});
126
127test("never under someone typing, a dialog, or a navigation: it waits for them", () => {
128 const due = { pendingSince: NOW - 10_000, lastChangeAt: NOW - 10_000, lastRunAt: null };
129 for (const held of [{ busy: true, navigating: false }, { busy: false, navigating: true }]) {
130 assert.deepEqual(gate(due, { now: NOW, visible: true, ...held }), { action: "wait", ms: LIVE_HOLD_CHECK_MS });
131 }
132 assert.deepEqual(gate(due, { now: NOW, visible: true, busy: false, navigating: false }), { action: "run" });
133});
134
135test("a tab out of view refreshes at most once a minute", () => {
136 const hidden = { now: NOW, visible: false, busy: false, navigating: false };
137 const ranLately = { pendingSince: NOW - 5000, lastChangeAt: NOW - 5000, lastRunAt: NOW - 20_000 };
138 assert.deepEqual(gate(ranLately, hidden), { action: "wait", ms: HIDDEN_MIN_MS - 20_000 });
139 assert.deepEqual(gate({ ...ranLately, lastRunAt: NOW - HIDDEN_MIN_MS }, hidden), { action: "run" });
140 // Back in view: at once.
141 assert.deepEqual(gate(ranLately, { ...hidden, visible: true }), { action: "run" });
142});
143
144test("without the socket: back in view every 30 s at most, a poll every minute in view, never with it", () => {
145 const base = { live: false, visible: true, now: NOW };
146 assert.equal(fallbackDue({ ...base, lastRunAt: null, reason: "focus" }), true);
147 assert.equal(fallbackDue({ ...base, lastRunAt: NOW - FALLBACK_FOCUS_MS + 1, reason: "focus" }), false);
148 assert.equal(fallbackDue({ ...base, lastRunAt: NOW - FALLBACK_FOCUS_MS, reason: "focus" }), true);
149 assert.equal(fallbackDue({ ...base, lastRunAt: NOW - FALLBACK_FOCUS_MS, reason: "poll" }), false);
150 assert.equal(fallbackDue({ ...base, lastRunAt: NOW - FALLBACK_POLL_MS, reason: "poll" }), true);
151 assert.equal(fallbackDue({ ...base, visible: false, lastRunAt: null, reason: "poll" }), false, "a hidden tab is not polled");
152 assert.equal(fallbackDue({ ...base, live: true, lastRunAt: null, reason: "focus" }), false);
153});
154
155test("typing is in text boxes, text areas and editors", () => {
156 assert.equal(isTyping({ tagName: "TEXTAREA" }), true);
157 assert.equal(isTyping({ tagName: "input", type: "text" }), true);
158 assert.equal(isTyping({ tagName: "INPUT", type: "search" }), true);
159 assert.equal(isTyping({ tagName: "INPUT" }), true, "an input is text unless it says otherwise");
160 assert.equal(isTyping({ tagName: "DIV", isContentEditable: true }), true);
161 assert.equal(isTyping({ tagName: "INPUT", type: "checkbox" }), false);
162 assert.equal(isTyping({ tagName: "BUTTON" }), false);
163 assert.equal(isTyping({ tagName: "TEXTAREA", readOnly: true }), false);
164 assert.equal(isTyping(null), false);
165});
166
167test("the bottom of the page, with some slack", () => {
168 assert.equal(atBottom({ top: 880, height: 2000, viewport: 1000 }), true);
169 assert.equal(atBottom({ top: 500, height: 2000, viewport: 1000 }), false);
170});