diff --git a/.eslintrc.yml b/.eslintrc.yml index dc3a4bef..6c291f74 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,6 +1,6 @@ env: node: true - es6: true + es2022: true globals: Atomics: readonly diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..7323f8ca --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: Test + +on: + pull_request: + merge_group: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test diff --git a/src/display.test.ts b/src/display.test.ts new file mode 100644 index 00000000..954c9696 --- /dev/null +++ b/src/display.test.ts @@ -0,0 +1,135 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import * as core from "@actions/core"; +import { Display } from "./display"; +import { RelevantCheckRuns } from "./relevant-check-runs"; +import type { CheckRun } from "./fetch-check-runs"; + +vi.mock("@actions/core"); + +const colors = { + reset: "\x1b[0m", + red: "\x1b[31m", + green: "\x1b[32m", +} as const; + +describe("Display", () => { + let consoleInfo: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {}); + }); + + afterEach(() => { + consoleInfo.mockRestore(); + }); + + describe("timedOut", () => { + it("prints timeout message", () => { + Display.timedOut(); + expect(consoleInfo).toHaveBeenNthCalledWith(1, ""); + expect(consoleInfo).toHaveBeenNthCalledWith( + 2, + `⏰ ${colors.red}Timed out!${colors.reset}`, + ); + }); + }); + + describe("delaying", () => { + it("prints delay message with the given seconds", () => { + Display.delaying(10); + expect(consoleInfo).toHaveBeenCalledWith("🦥 Inspecting again in 10s..."); + }); + }); + + describe("overallFailure", () => { + it("prints failure message", () => { + Display.overallFailure(); + expect(consoleInfo).toHaveBeenNthCalledWith(1, ""); + expect(consoleInfo).toHaveBeenNthCalledWith( + 2, + `❗ ${colors.red}Failure!${colors.reset}`, + ); + }); + }); + + describe("overallSuccess", () => { + it("prints success message", () => { + Display.overallSuccess(); + expect(consoleInfo).toHaveBeenNthCalledWith(1, ""); + expect(consoleInfo).toHaveBeenNthCalledWith( + 2, + `🚀 ${colors.green}Success!${colors.reset}`, + ); + }); + }); + + describe("startingIteration", () => { + it("prints an empty line", () => { + Display.startingIteration(); + expect(consoleInfo).toHaveBeenCalledWith(""); + }); + }); + + describe("ignoredCheckPatterns", () => { + it("groups and prints patterns when present", () => { + Display.ignoredCheckPatterns(["check1", "check2"]); + expect(core.startGroup).toHaveBeenCalledWith("Ignored check patterns"); + expect(consoleInfo).toHaveBeenNthCalledWith(1, "check1"); + expect(consoleInfo).toHaveBeenNthCalledWith(2, "check2"); + expect(core.endGroup).toHaveBeenCalled(); + }); + + it("prints nothing when the list is empty", () => { + Display.ignoredCheckPatterns([]); + expect(core.startGroup).not.toHaveBeenCalled(); + expect(consoleInfo).not.toHaveBeenCalled(); + expect(core.endGroup).not.toHaveBeenCalled(); + }); + }); + + describe("relevantCheckRuns", () => { + function stubRun(name: string, conclusion: string | null): CheckRun { + return { name, conclusion } as CheckRun; + } + + it("groups each non-empty category with counts and icons", () => { + const runs = new RelevantCheckRuns([ + stubRun("success-check", "success"), + stubRun("failed-check", "failure"), + stubRun("pending-check", null), + ]); + + Display.relevantCheckRuns(runs); + + expect(core.startGroup).toHaveBeenNthCalledWith( + 1, + `✅ ${colors.green}1${colors.reset}`, + ); + expect(consoleInfo).toHaveBeenNthCalledWith(1, "success-check"); + + expect(core.startGroup).toHaveBeenNthCalledWith( + 2, + `❌ ${colors.red}1${colors.reset}`, + ); + expect(consoleInfo).toHaveBeenNthCalledWith(2, "failed-check"); + + expect(core.startGroup).toHaveBeenNthCalledWith( + 3, + `⏳ ${colors.reset}1${colors.reset}`, + ); + expect(consoleInfo).toHaveBeenNthCalledWith(3, "pending-check"); + }); + + it("skips empty categories", () => { + const runs = new RelevantCheckRuns([stubRun("success-check", "success")]); + + Display.relevantCheckRuns(runs); + + expect(core.startGroup).toHaveBeenCalledTimes(1); + expect(core.startGroup).toHaveBeenCalledWith( + `✅ ${colors.green}1${colors.reset}`, + ); + }); + }); +}); diff --git a/src/fetch-check-runs.test.ts b/src/fetch-check-runs.test.ts new file mode 100644 index 00000000..b8154535 --- /dev/null +++ b/src/fetch-check-runs.test.ts @@ -0,0 +1,173 @@ +import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest"; +import type { Octokit } from "@octokit/core"; +import { IgnoreMatcher } from "./ignore-matcher"; + +const mockPaginateIterator = vi.fn(); +const mockOctokit = { + paginate: { iterator: mockPaginateIterator }, + rest: { checks: { listForRef: vi.fn() } }, +} as unknown as Octokit; + +vi.mock("@actions/github", () => ({ + getOctokit: vi.fn(() => mockOctokit), + context: { repo: { owner: "test-owner", repo: "test-repo" } }, +})); + +vi.mock("./inputs", () => ({ + inputs: { + token: "fake-token", + name: "sloth", + ignored: new IgnoreMatcher(["ignored-check"]), + ref: "main", + }, +})); + +let fetchCheckRuns: Awaited< + typeof import("./fetch-check-runs") +>["fetchCheckRuns"]; + +beforeAll(async () => { + ({ fetchCheckRuns } = await import("./fetch-check-runs")); +}); + +describe("fetchCheckRuns", () => { + beforeEach(() => { + mockPaginateIterator.mockReset(); + }); + + it("filters out the action's own check run and ignored checks", async () => { + mockPaginateIterator.mockImplementation(async function* () { + yield { + data: [ + { + name: "test-check-1", + status: "completed", + conclusion: "success", + completed_at: "2024-01-01", + started_at: null, + }, + { + name: "sloth", + status: "completed", + conclusion: "success", + completed_at: "2024-01-01", + started_at: null, + }, + { + name: "ignored-check", + status: "completed", + conclusion: "success", + completed_at: "2024-01-01", + started_at: null, + }, + { + name: "test-check-2", + status: "in_progress", + conclusion: null, + completed_at: null, + started_at: "2024-01-01", + }, + ], + }; + }); + + const result = await fetchCheckRuns(); + + expect(result.total()).toBe(2); + expect(result.succeeded).toEqual(["test-check-1"]); + expect(result.pending).toEqual(["test-check-2"]); + expect(result.failed).toEqual([]); + }); + + it("paginates across multiple pages", async () => { + mockPaginateIterator.mockImplementation(async function* () { + yield { + data: [ + { + name: "check-1", + status: "completed", + conclusion: "success", + completed_at: "2024-01-01", + started_at: null, + }, + ], + }; + yield { + data: [ + { + name: "check-2", + status: "completed", + conclusion: "success", + completed_at: "2024-01-01", + started_at: null, + }, + ], + }; + }); + + const result = await fetchCheckRuns(); + expect(result.total()).toBe(2); + expect(result.succeeded).toEqual(["check-1", "check-2"]); + }); + + it("returns empty results when no check runs are found", async () => { + mockPaginateIterator.mockImplementation(async function* () { + yield { data: [] }; + }); + + const result = await fetchCheckRuns(); + expect(result.total()).toBe(0); + }); + + it("identifies overall failure when any run has failed", async () => { + mockPaginateIterator.mockImplementation(async function* () { + yield { + data: [ + { + name: "test-1", + status: "completed", + conclusion: "failure", + completed_at: "2024-01-01", + started_at: null, + }, + { + name: "test-2", + status: "completed", + conclusion: "success", + completed_at: "2024-01-01", + started_at: null, + }, + ], + }; + }); + + const result = await fetchCheckRuns(); + expect(result.isOverallFailure()).toBe(true); + }); + + it("identifies overall success when all runs have passed", async () => { + mockPaginateIterator.mockImplementation(async function* () { + yield { + data: [ + { + name: "test-1", + status: "completed", + conclusion: "success", + completed_at: "2024-01-01", + started_at: null, + }, + { + name: "test-2", + status: "completed", + conclusion: "success", + completed_at: "2024-01-01", + started_at: null, + }, + ], + }; + }); + + const result = await fetchCheckRuns(); + expect(result.isOverallSuccess()).toBe(true); + }); +});