| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { |
| 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 | |
| 22 | const AT = "2026-10-11T10:00:00Z"; |
| 23 | const NOW = Date.parse(AT); |
| 24 | |
| 25 | function change(kind: string, more: Partial<RepoChange> = {}): RepoChange { |
| 26 | return { repo_id: "rep_1", repo: "acme/rocket", kind, at: AT, ...more }; |
| 27 | } |
| 28 | |
| 29 | const push = (branch: string, commits?: number) => change("git.push", { ref: `refs/heads/${branch}`, sha: "abc", commits: commits ?? null }); |
| 30 | |
| 31 | test("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 | |
| 38 | test("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 | |
| 47 | test("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 | |
| 56 | test("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 | |
| 72 | test("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 | |
| 87 | test("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 | |
| 96 | test("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 | |
| 110 | test("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 | |
| 116 | test("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 | |
| 127 | test("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 | |
| 135 | test("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 | |
| 144 | test("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 | |
| 155 | test("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 | |
| 167 | test("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 | }); |