Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions src/app/components/shared/SizeBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface SizeBadgeProps {
additions: number;
deletions: number;
changedFiles: number;
category?: "XS" | "S" | "M" | "L" | "XL";
category?: "XS" | "S" | "M" | "L" | "XL" | "XXL";
filesUrl?: string;
}

Expand All @@ -16,14 +16,16 @@ const SIZE_CONFIG = {
M: "badge badge-warning badge-sm",
L: "badge badge-error badge-sm",
XL: "badge badge-error badge-sm",
XXL: "badge badge-error badge-sm",
} as const;

const SIZE_TOOLTIP: Record<"XS" | "S" | "M" | "L" | "XL", string> = {
const SIZE_TOOLTIP: Record<"XS" | "S" | "M" | "L" | "XL" | "XXL", string> = {
XS: "XS: <10 lines changed",
S: "S: 10–99 lines changed",
M: "M: 100–499 lines changed",
L: "L: 500–999 lines changed",
XL: "XL: 1000+ lines changed",
S: "S: 10–29 lines changed",
M: "M: 30–99 lines changed",
L: "L: 100–499 lines changed",
XL: "XL: 500–999 lines changed",
XXL: "XXL: 1000+ lines changed",
};

export default function SizeBadge(props: SizeBadgeProps) {
Expand Down
1 change: 1 addition & 0 deletions src/app/components/shared/filterTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const prFilterGroups: FilterChipGroupDef[] = [
{ value: "M", label: "M" },
{ value: "L", label: "L" },
{ value: "XL", label: "XL" },
{ value: "XXL", label: "XXL" },
],
},
];
Expand Down
2 changes: 1 addition & 1 deletion src/app/stores/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const PullRequestFiltersSchema = z.object({
reviewDecision: z.enum(["all", "APPROVED", "CHANGES_REQUESTED", "REVIEW_REQUIRED", "mergeable"]).default("all"),
draft: z.enum(["all", "draft", "ready"]).default("all"),
checkStatus: z.enum(["all", "success", "failure", "pending", "conflict", "blocked", "none"]).default("all"),
sizeCategory: z.enum(["all", "XS", "S", "M", "L", "XL"]).default("all"),
sizeCategory: z.enum(["all", "XS", "S", "M", "L", "XL", "XXL"]).default("all"),
user: z.enum(["all"]).or(z.string()).default("all"),
});

Expand Down
11 changes: 6 additions & 5 deletions src/shared/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,14 @@ export function formatDuration(startedAt: string, completedAt: string | null): s
/**
* Categorizes a PR by size based on total lines changed.
*/
export function prSizeCategory(additions: number, deletions: number): "XS" | "S" | "M" | "L" | "XL" {
export function prSizeCategory(additions: number, deletions: number): "XS" | "S" | "M" | "L" | "XL" | "XXL" {
const total = (additions || 0) + (deletions || 0);
if (total < 10) return "XS";
if (total < 100) return "S";
if (total < 500) return "M";
if (total < 1000) return "L";
return "XL";
if (total < 30) return "S";
if (total < 100) return "M";
if (total < 500) return "L";
if (total < 1000) return "XL";
return "XXL";
}

/**
Expand Down
19 changes: 15 additions & 4 deletions tests/components/PullRequestsTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,9 @@ describe("PullRequestsTab", () => {
const pr = makePullRequest({ id: 1, title: "Big PR", additions: 300, deletions: 100, repoFullName: "org/repo-a" });
setAllExpanded("pullRequests", ["org/repo-a"], true);
render(() => <PullRequestsTab pullRequests={[pr]} userLogin="" />);
// prSizeCategory(300, 100) = 400 total -> M
// "M" appears as a size badge
const mEls = screen.getAllByText("M");
const badgeEl = mEls.find((el) => el.tagName.toLowerCase() === "span");
// prSizeCategory(300, 100) = 400 total -> L
const lEls = screen.getAllByText("L");
const badgeEl = lEls.find((el) => el.tagName.toLowerCase() === "span");
expect(badgeEl).toBeDefined();
});

Expand Down Expand Up @@ -245,6 +244,18 @@ describe("PullRequestsTab", () => {
expect(screen.queryByText("Large PR")).toBeNull();
});

it("filters by sizeCategory 'XXL' tab filter", () => {
const prs = [
makePullRequest({ id: 1, title: "Huge PR", additions: 800, deletions: 500, repoFullName: "org/repo-a" }),
makePullRequest({ id: 2, title: "Medium PR", additions: 30, deletions: 20, repoFullName: "org/repo-a" }),
];
viewStore.setTabFilter("pullRequests", "sizeCategory", "XXL");
setAllExpanded("pullRequests", ["org/repo-a"], true);
render(() => <PullRequestsTab pullRequests={prs} userLogin="" />);
screen.getByText("Huge PR");
expect(screen.queryByText("Medium PR")).toBeNull();
});

it("groups PRs by repo with collapsible headers", () => {
const prs = [
makePullRequest({ id: 1, title: "PR in repo A", repoFullName: "org/repo-a" }),
Expand Down
81 changes: 65 additions & 16 deletions tests/components/shared-badges.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent } from "@solidjs/testing-library";
import RoleBadge from "../../src/app/components/shared/RoleBadge";
import ReviewBadge from "../../src/app/components/shared/ReviewBadge";
Expand Down Expand Up @@ -67,9 +67,29 @@ describe("SizeBadge", () => {
screen.getByText("1 file");
});

it("renders XL badge for large changes", () => {
render(() => <SizeBadge additions={800} deletions={500} changedFiles={42} />);
it("renders S badge for small-medium changes", () => {
render(() => <SizeBadge additions={15} deletions={14} changedFiles={2} />);
screen.getByText("S");
});

it("renders M badge for medium changes", () => {
render(() => <SizeBadge additions={15} deletions={15} changedFiles={3} />);
screen.getByText("M");
});

it("renders L badge for large changes", () => {
render(() => <SizeBadge additions={200} deletions={100} changedFiles={10} />);
screen.getByText("L");
});

it("renders XL badge for extra-large changes", () => {
render(() => <SizeBadge additions={400} deletions={200} changedFiles={20} />);
screen.getByText("XL");
});

it("renders XXL badge for large changes", () => {
render(() => <SizeBadge additions={800} deletions={500} changedFiles={42} />);
screen.getByText("XXL");
screen.getByText("+800");
screen.getByText("-500");
screen.getByText("42 files");
Expand All @@ -85,18 +105,47 @@ describe("SizeBadge", () => {
screen.getByText("XS");
});

it("shows tooltip with size description on hover", () => {
vi.useFakeTimers();
const { container } = render(() => (
<SizeBadge additions={3} deletions={2} changedFiles={1} />
));
const trigger = container.querySelector("span.inline-flex");
expect(trigger).not.toBeNull();
fireEvent.pointerEnter(trigger!);
vi.advanceTimersByTime(300);
expect(document.body.textContent).toContain("XS: <10 lines changed");
fireEvent.pointerLeave(trigger!);
vi.advanceTimersByTime(500);
vi.useRealTimers();
describe("tooltips", () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());

it("shows tooltip with size description on hover", () => {
const { container } = render(() => (
<SizeBadge additions={3} deletions={2} changedFiles={1} />
));
const trigger = container.querySelector("span.inline-flex");
expect(trigger).not.toBeNull();
fireEvent.pointerEnter(trigger!);
vi.advanceTimersByTime(300);
expect(document.body.textContent).toContain("XS: <10 lines changed");
fireEvent.pointerLeave(trigger!);
vi.advanceTimersByTime(500);
});

it("shows tooltip with S size description on hover", () => {
const { container } = render(() => (
<SizeBadge additions={10} deletions={5} changedFiles={2} />
));
const trigger = container.querySelector("span.inline-flex");
expect(trigger).not.toBeNull();
fireEvent.pointerEnter(trigger!);
vi.advanceTimersByTime(300);
expect(document.body.textContent).toContain("S: 10–29 lines changed");
fireEvent.pointerLeave(trigger!);
vi.advanceTimersByTime(500);
});

it("shows tooltip with XXL size description on hover", () => {
const { container } = render(() => (
<SizeBadge additions={800} deletions={500} changedFiles={42} />
));
const trigger = container.querySelector("span.inline-flex");
expect(trigger).not.toBeNull();
fireEvent.pointerEnter(trigger!);
vi.advanceTimersByTime(300);
expect(document.body.textContent).toContain("XXL: 1000+ lines changed");
fireEvent.pointerLeave(trigger!);
vi.advanceTimersByTime(500);
});
});
});
52 changes: 40 additions & 12 deletions tests/lib/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,20 +238,24 @@ describe("prSizeCategory", () => {
expect(prSizeCategory(3, 2)).toBe("XS");
});

it("returns S for total 10-99", () => {
expect(prSizeCategory(50, 30)).toBe("S");
it("returns S for total 10-29", () => {
expect(prSizeCategory(10, 5)).toBe("S");
});

it("returns M for total 100-499", () => {
expect(prSizeCategory(200, 100)).toBe("M");
it("returns M for total 30-99", () => {
expect(prSizeCategory(50, 30)).toBe("M");
});

it("returns L for total 500-999", () => {
expect(prSizeCategory(600, 200)).toBe("L");
it("returns L for total 100-499", () => {
expect(prSizeCategory(200, 100)).toBe("L");
});

it("returns XL for total >= 1000", () => {
expect(prSizeCategory(800, 500)).toBe("XL");
it("returns XL for total 500-999", () => {
expect(prSizeCategory(600, 200)).toBe("XL");
});

it("returns XXL for total >= 1000", () => {
expect(prSizeCategory(800, 500)).toBe("XXL");
});

it("returns XS for (0, 0)", () => {
Expand All @@ -266,12 +270,36 @@ describe("prSizeCategory", () => {
expect(prSizeCategory(5, 5)).toBe("S");
});

it("returns L for total 999 (boundary below 1000)", () => {
expect(prSizeCategory(500, 499)).toBe("L");
it("returns S for total 29 (boundary below 30)", () => {
expect(prSizeCategory(15, 14)).toBe("S");
});

it("returns M for total 30 (boundary at 30)", () => {
expect(prSizeCategory(15, 15)).toBe("M");
});

it("returns M for total 99 (boundary below 100)", () => {
expect(prSizeCategory(50, 49)).toBe("M");
});

it("returns L for total 100 (boundary at 100)", () => {
expect(prSizeCategory(50, 50)).toBe("L");
});

it("returns L for total 499 (boundary below 500)", () => {
expect(prSizeCategory(250, 249)).toBe("L");
});

it("returns XL for total 500 (boundary at 500)", () => {
expect(prSizeCategory(250, 250)).toBe("XL");
});

it("returns XL for total 999 (boundary below 1000)", () => {
expect(prSizeCategory(500, 499)).toBe("XL");
});

it("returns XL for total 1000 (boundary at 1000)", () => {
expect(prSizeCategory(500, 500)).toBe("XL");
it("returns XXL for total 1000 (boundary at 1000)", () => {
expect(prSizeCategory(500, 500)).toBe("XXL");
});

it("handles NaN/undefined gracefully — defaults to XS", () => {
Expand Down