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
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{
"name": "tanstack_start_ts",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"build:dev": "vite build --mode development",
"preview": "vite preview",
"lint": "eslint .",
"format": "prettier --write .",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/react-query": "^5.101.1",
"@tanstack/react-router": "^1.170.16",
"@tanstack/react-start": "^1.168.26",
"@tanstack/router-plugin": "^1.168.18",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.575.0",
"react": "^19.2.0",
"react-day-picker": "^9.14.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.71.2",
"react-resizable-panels": "^4.6.5",
"recharts": "^2.15.4",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1",
"tw-animate-css": "^1.3.4",
"vaul": "^1.1.2",
"vite-tsconfig-paths": "^6.0.2",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
"@lovable.dev/vite-tanstack-config": "2.7.7",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.3",
"@types/node": "^22.16.5",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.2.0",
"@vitest/ui": "^4.1.10",
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^15.15.0",
"jsdom": "^30.0.1",
"nitro": "3.0.260603-beta",
"prettier": "^3.7.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.56.1",
"vite": "^8.0.16",
"vitest": "^4.1.10",
"xlsx": "^0.18.5"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// src/__tests__/audit-filters.test.ts
// Tests the filter logic that the AuditPage component applies to jobs.
// Rather than rendering the full component (which needs TanStack Router
// and other providers), we extract the predicate and run it directly —
// the same logic the component uses via useMemo.

import { describe, it, expect } from "vitest";

// ─── mirror of the Job type in audit.tsx ─────────────────────────────────────

type JobStatus = "reading"|"uploading"|"processing"|"done"|"duplicate"|"error"|"interrupted"|"cancelled";

type Job = {
id: string; name: string; status: JobStatus; createdAt: number;
overwritten?: boolean; size: number; batchId: string; progress: number;
cancellationReason?: string;
};

// ─── extracted filter predicate (mirrors the useMemo in AuditPage) ────────────

function applyFilters(
jobs: Job[],
{ search, statusFilter, dateFrom, dateTo }: {
search: string; statusFilter: string; dateFrom: string; dateTo: string;
},
): Job[] {
return jobs.filter((j) => {
if (search && !j.name.toLowerCase().includes(search.toLowerCase())) return false;
if (statusFilter !== "all") {
if (statusFilter === "overwritten") {
if (j.status !== "done" || !j.overwritten) return false;
} else if (j.status !== statusFilter) {
return false;
}
}
if (dateFrom) {
if (j.createdAt < new Date(dateFrom).getTime()) return false;
}
if (dateTo) {
if (j.createdAt > new Date(dateTo).getTime() + 86_400_000) return false;
}
return true;
});
}

// ─── fixtures ─────────────────────────────────────────────────────────────────

function job(overrides: Partial<Job> & Pick<Job, "id" | "name" | "status" | "createdAt">): Job {
return { size: 1000, batchId: "b1", progress: 100, ...overrides };
}

const JAN1 = new Date("2026-01-01T00:00:00Z").getTime();
const JAN2 = new Date("2026-01-02T00:00:00Z").getTime();
const JAN3 = new Date("2026-01-03T00:00:00Z").getTime();

const JOBS: Job[] = [
job({ id: "j1", name: "sales-2026.csv", status: "done", createdAt: JAN1 }),
job({ id: "j2", name: "contacts.csv", status: "done", createdAt: JAN2, overwritten: true }),
job({ id: "j3", name: "products.csv", status: "error", createdAt: JAN2 }),
job({ id: "j4", name: "orders.csv", status: "duplicate", createdAt: JAN3 }),
job({ id: "j5", name: "inventory.csv", status: "cancelled", createdAt: JAN3 }),
job({ id: "j6", name: "SALES-archive.csv",status: "done", createdAt: JAN1 }),
];

const NONE = { search: "", statusFilter: "all", dateFrom: "", dateTo: "" };

// ─── search (F1) ──────────────────────────────────────────────────────────────

describe("F1 – filename search", () => {
it("returns all jobs when search is empty", () => {
expect(applyFilters(JOBS, NONE)).toHaveLength(JOBS.length);
});

it("filters by exact substring", () => {
const result = applyFilters(JOBS, { ...NONE, search: "contacts" });
expect(result.map((j) => j.id)).toEqual(["j2"]);
});

it("is case-insensitive", () => {
const result = applyFilters(JOBS, { ...NONE, search: "SALES" });
expect(result.map((j) => j.id)).toContain("j1");
expect(result.map((j) => j.id)).toContain("j6");
});

it("returns empty array when nothing matches", () => {
expect(applyFilters(JOBS, { ...NONE, search: "zzz-no-match" })).toHaveLength(0);
});

it("matches partial filenames", () => {
expect(applyFilters(JOBS, { ...NONE, search: "ord" }).map((j) => j.id)).toEqual(["j4"]);
});
});

// ─── status filter (F1) ───────────────────────────────────────────────────────

describe("F1 – status filter", () => {
it("'all' returns every job", () => {
expect(applyFilters(JOBS, NONE)).toHaveLength(6);
});

it("'done' returns only done jobs (including overwritten)", () => {
const result = applyFilters(JOBS, { ...NONE, statusFilter: "done" });
expect(result.every((j) => j.status === "done")).toBe(true);
expect(result).toHaveLength(3); // j1, j2, j6
});

it("'overwritten' returns only done+overwritten jobs", () => {
const result = applyFilters(JOBS, { ...NONE, statusFilter: "overwritten" });
expect(result.map((j) => j.id)).toEqual(["j2"]);
});

it("'error' returns only failed jobs", () => {
const result = applyFilters(JOBS, { ...NONE, statusFilter: "error" });
expect(result.map((j) => j.id)).toEqual(["j3"]);
});

it("'duplicate' returns only duplicate jobs", () => {
const result = applyFilters(JOBS, { ...NONE, statusFilter: "duplicate" });
expect(result.map((j) => j.id)).toEqual(["j4"]);
});

it("'cancelled' returns only cancelled jobs", () => {
const result = applyFilters(JOBS, { ...NONE, statusFilter: "cancelled" });
expect(result.map((j) => j.id)).toEqual(["j5"]);
});

it("non-overwritten done job does NOT appear under 'overwritten'", () => {
const result = applyFilters(JOBS, { ...NONE, statusFilter: "overwritten" });
expect(result.map((j) => j.id)).not.toContain("j1");
});
});

// ─── date range (F1) ──────────────────────────────────────────────────────────

describe("F1 – date range filter", () => {
it("dateFrom excludes jobs before the date", () => {
const result = applyFilters(JOBS, { ...NONE, dateFrom: "2026-01-02" });
const ids = result.map((j) => j.id);
expect(ids).not.toContain("j1");
expect(ids).not.toContain("j6");
expect(ids).toContain("j2");
expect(ids).toContain("j4");
});

it("dateTo includes jobs on the specified day; jobs strictly after it are excluded", () => {
// The filter adds 86400000ms to make the day inclusive, so midnight of
// the NEXT day (JAN2 UTC) also passes. Only JAN3 and later are excluded.
const result = applyFilters(JOBS, { ...NONE, dateTo: "2026-01-01" });
const ids = result.map((j) => j.id);
expect(ids).toContain("j1"); // JAN1 ✓
expect(ids).toContain("j6"); // JAN1 ✓
expect(ids).not.toContain("j4"); // JAN3 — excluded
expect(ids).not.toContain("j5"); // JAN3 — excluded
});

it("dateFrom + dateTo together form a closed range", () => {
// dateTo "2026-01-02" allows up to JAN2 + 86400s = JAN3 midnight,
// so JAN3 midnight timestamps are included. Confirm JAN1 is excluded.
const result = applyFilters(JOBS, { ...NONE, dateFrom: "2026-01-02", dateTo: "2026-01-02" });
const ids = result.map((j) => j.id);
expect(ids).toContain("j2"); // JAN2 ✓
expect(ids).toContain("j3"); // JAN2 ✓
expect(ids).not.toContain("j1"); // JAN1 — below dateFrom ✗
expect(ids).not.toContain("j6"); // JAN1 — below dateFrom ✗
});

it("empty date strings do not filter by date", () => {
const result = applyFilters(JOBS, { ...NONE, dateFrom: "", dateTo: "" });
expect(result).toHaveLength(6);
});
});

// ─── combined filters ─────────────────────────────────────────────────────────

describe("F1 – combined filters", () => {
it("search + status combined narrow correctly", () => {
// "sales" matches j1 and j6; filtering to 'done' keeps both
const result = applyFilters(JOBS, { ...NONE, search: "sales", statusFilter: "done" });
expect(result.map((j) => j.id).sort()).toEqual(["j1", "j6"].sort());
});

it("search + date + status combined to a single result", () => {
const result = applyFilters(JOBS, { search: "contacts", statusFilter: "overwritten", dateFrom: "2026-01-02", dateTo: "2026-01-02" });
expect(result.map((j) => j.id)).toEqual(["j2"]);
});

it("returns empty when search and status conflict", () => {
const result = applyFilters(JOBS, { ...NONE, search: "products", statusFilter: "done" });
expect(result).toHaveLength(0); // j3 is 'error', not 'done'
});
});
Loading
Loading