6b40d5b2ec
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
147 lines
5.2 KiB
TypeScript
147 lines
5.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import type { RunStatus } from "@/lib/types/automation";
|
|
import {
|
|
ALL_STATUSES,
|
|
groupRunsByState,
|
|
isOpenRun,
|
|
RUN_STATUS_LABEL_KEY,
|
|
STATUS_FILTER_OPTIONS,
|
|
statusDotClass,
|
|
statusLabelKey,
|
|
} from "./run-status";
|
|
import { ANY_AUTOMATION, isDefaultFilters } from "./run-filters";
|
|
|
|
const EVERY_STATUS: RunStatus[] = [
|
|
"triggered",
|
|
"task_created",
|
|
"succeeded",
|
|
"failed",
|
|
"skipped",
|
|
"archived",
|
|
"cancelled",
|
|
];
|
|
|
|
describe("run status presentation", () => {
|
|
it("labels and colours every status the backend can derive", () => {
|
|
// The status set includes two read-time-derived values (archived,
|
|
// cancelled). Missing either one renders a blank label on a real run.
|
|
for (const status of EVERY_STATUS) {
|
|
expect(statusLabelKey(status), status).toBeTruthy();
|
|
expect(statusDotClass(status), status).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
it("calls a task_created run Running, not by its storage name", () => {
|
|
expect(statusLabelKey("task_created")).toBe("automations:runRunning");
|
|
});
|
|
|
|
it("falls back rather than rendering blank for an unknown status", () => {
|
|
const unknown = "not-a-status" as RunStatus;
|
|
expect(statusLabelKey(unknown)).toBe(RUN_STATUS_LABEL_KEY.triggered);
|
|
expect(statusDotClass(unknown)).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("status filter options", () => {
|
|
it("offers Skipped, which is otherwise invisible", () => {
|
|
// A scheduled firing turned away by the concurrency cap writes a run row
|
|
// and nothing else, so without this filter a jammed automation looks
|
|
// identical to one that was never due.
|
|
expect(STATUS_FILTER_OPTIONS.map((o) => o.value)).toContain("skipped");
|
|
});
|
|
|
|
it("offers the derived terminal statuses too", () => {
|
|
const values = STATUS_FILTER_OPTIONS.map((o) => o.value);
|
|
expect(values).toContain("archived");
|
|
expect(values).toContain("cancelled");
|
|
});
|
|
|
|
it("offers every status the feed can display", () => {
|
|
// A status that renders but cannot be filtered leaves the reader unable to
|
|
// narrow to it, so the option list tracks the label map rather than a
|
|
// hand-picked subset.
|
|
const values = STATUS_FILTER_OPTIONS.map((o) => o.value);
|
|
for (const status of EVERY_STATUS) {
|
|
expect(values, status).toContain(status);
|
|
}
|
|
});
|
|
|
|
it("leads with All and never repeats a value", () => {
|
|
expect(STATUS_FILTER_OPTIONS[0].value).toBe(ALL_STATUSES);
|
|
const values = STATUS_FILTER_OPTIONS.map((o) => o.value);
|
|
expect(new Set(values).size).toBe(values.length);
|
|
});
|
|
|
|
it("labels each option the same way the feed labels the run", () => {
|
|
// Compared against the literal rather than ALL_STATUSES: that const is
|
|
// widened to RunStatusFilter, so it does not narrow the union here.
|
|
for (const option of STATUS_FILTER_OPTIONS) {
|
|
if (option.value === "all") continue;
|
|
expect(option.labelKey).toBe(RUN_STATUS_LABEL_KEY[option.value]);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("isOpenRun", () => {
|
|
it("counts only the two statuses that mean the run has not finished", () => {
|
|
expect(isOpenRun("triggered")).toBe(true);
|
|
expect(isOpenRun("task_created")).toBe(true);
|
|
for (const status of EVERY_STATUS) {
|
|
if (status === "triggered" || status === "task_created") continue;
|
|
expect(isOpenRun(status), status).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("treats the read-time-derived terminals as finished", () => {
|
|
// A cancelled or archived run is over. Counting either as open would hold
|
|
// an automation at its concurrency cap forever.
|
|
expect(isOpenRun("archived")).toBe(false);
|
|
expect(isOpenRun("cancelled")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("groupRunsByState", () => {
|
|
const RUNS = [
|
|
{ id: "a", status: "succeeded" as RunStatus },
|
|
{ id: "b", status: "task_created" as RunStatus },
|
|
{ id: "c", status: "failed" as RunStatus },
|
|
{ id: "d", status: "triggered" as RunStatus },
|
|
];
|
|
|
|
it("separates what is happening from what already happened", () => {
|
|
const { running, completed } = groupRunsByState(RUNS);
|
|
|
|
expect(running.map((run) => run.id)).toEqual(["b", "d"]);
|
|
expect(completed.map((run) => run.id)).toEqual(["a", "c"]);
|
|
});
|
|
|
|
it("keeps every run — this groups, it does not filter", () => {
|
|
const { running, completed } = groupRunsByState(RUNS);
|
|
|
|
expect(running.length + completed.length).toBe(RUNS.length);
|
|
});
|
|
|
|
it("preserves the order it was given within each group", () => {
|
|
const { completed } = groupRunsByState(RUNS);
|
|
|
|
expect(completed.map((run) => run.id)).toEqual(["a", "c"]);
|
|
});
|
|
|
|
it("handles an empty history", () => {
|
|
expect(groupRunsByState([])).toEqual({ running: [], completed: [] });
|
|
});
|
|
});
|
|
|
|
describe("isDefaultFilters", () => {
|
|
it("is true only when neither filter narrows anything", () => {
|
|
// The "N of M" counter and the clear-filters affordance both hang off this,
|
|
// so a wrong answer either hides the fact that a filter is on or shows the
|
|
// counter permanently.
|
|
expect(isDefaultFilters(ALL_STATUSES, ANY_AUTOMATION)).toBe(true);
|
|
expect(isDefaultFilters("failed", ANY_AUTOMATION)).toBe(false);
|
|
expect(isDefaultFilters(ALL_STATUSES, "automation-1")).toBe(false);
|
|
expect(isDefaultFilters("failed", "automation-1")).toBe(false);
|
|
});
|
|
});
|