Files
scan-receipts/tests/spotlight_adversarial.test.ts
Timo 84b9987c49 Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing,
Docker/deploy config, tests, docs) into version control on top of the
placeholder initial commit, and adds account self-deletion (Danger Zone
in Settings, password + typed-email confirmation, cascading DB cleanup,
Stripe cancellation) per GDPR right-to-erasure.

Excludes local build caches, node_modules, and internal agent scratch
files; .gitignore hardened to keep those out going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 20:59:04 +02:00

791 lines
28 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Adversarial Spotlight & Interaction Contract Verification Suite
* Challenger M1_2 Empirical Harness
*/
import fs from "fs";
import path from "path";
import { ProcessedReceipt } from "../src/lib/schema/receipt";
// Simple assertion helper
function assert(condition: boolean, message: string) {
if (!condition) {
throw new Error(`[ASSERTION FAILED]: ${message}`);
}
}
function assertEquals<T>(actual: T, expected: T, message: string) {
if (actual !== expected) {
throw new Error(`[ASSERTION FAILED]: ${message} | Expected: ${expected}, Got: ${actual}`);
}
}
// Mock test receipt records
const mockReceipts: ProcessedReceipt[] = [
{
id: "rec-001",
merchant: { name: "Aral Tankstelle", address: "München", taxId: "DE12345", confidence: 0.99 },
date: { isoDate: "2026-08-15", time: "08:42", confidence: 0.98 },
documentType: "KASSENBON",
receiptNumber: "ARAL-9988",
currency: "EUR",
totalAmount: { value: 68.45, confidence: 0.99 },
netAmount: 57.52,
taxBreakdown: [{ ratePercent: 19, taxAmount: 10.93, netAmount: 57.52 }],
lineItems: [{ description: "Super Plus", quantity: 35.1, unitPrice: 1.95, price: 68.45, taxRate: 19 }],
suggestedCategory: "Tanken & KFZ",
validation: {
isMathValid: true,
isDuplicateSuspected: false,
needsUserReview: false,
reviewField: "none",
reviewReason: null,
},
imageHash: "hash-aral-001",
originalFileName: "aral.jpg",
fileSizeBytes: 184000,
createdAt: "2026-08-15T08:42:00.000Z",
updatedAt: "2026-08-15T08:42:00.000Z",
status: "ready",
},
{
id: "rec-002",
merchant: { name: "Trattoria Bella Vista", address: "Berlin", taxId: "DE98765", confidence: 0.95 },
date: { isoDate: "2026-08-14", time: "20:15", confidence: 0.95 },
documentType: "BEWIRTUNGSBELEG",
receiptNumber: "TRAT-1002",
currency: "EUR",
totalAmount: { value: 84.50, confidence: 0.96 },
netAmount: 70.97,
taxBreakdown: [
{ ratePercent: 7, taxAmount: 3.08, netAmount: 44.0 },
{ ratePercent: 19, taxAmount: 10.45, netAmount: 26.97 },
],
lineItems: [
{ description: "Pasta Tartufo", quantity: 2, unitPrice: 22.0, price: 44.0, taxRate: 7 },
{ description: "Vino Rosso", quantity: 1, unitPrice: 28.0, price: 28.0, taxRate: 19 },
{ description: "Trinkgeld Bewirtung", quantity: 1, unitPrice: 12.5, price: 12.5, taxRate: 0 },
],
suggestedCategory: "Bewirtung",
validation: {
isMathValid: true,
isDuplicateSuspected: false,
needsUserReview: false,
reviewField: "none",
reviewReason: null,
},
imageHash: "hash-trattoria-002",
originalFileName: "trattoria.jpg",
fileSizeBytes: 210000,
createdAt: "2026-08-14T20:15:00.000Z",
updatedAt: "2026-08-14T20:15:00.000Z",
status: "ready",
},
{
id: "rec-003",
merchant: { name: "REWE City", address: "Köln", taxId: "DE554433", confidence: 0.94 },
date: { isoDate: "2026-08-13", time: "17:30", confidence: 0.95 },
documentType: "KASSENBON",
receiptNumber: "REWE-7766",
currency: "EUR",
totalAmount: { value: 24.80, confidence: 0.95 },
netAmount: 22.73,
taxBreakdown: [
{ ratePercent: 7, taxAmount: 1.31, netAmount: 18.71 },
{ ratePercent: 19, taxAmount: 0.76, netAmount: 4.02 },
],
lineItems: [
{ description: "Bio Milch", quantity: 2, unitPrice: 1.49, price: 2.98, taxRate: 7 },
{ description: "Dinkelbrot", quantity: 1, unitPrice: 3.49, price: 3.49, taxRate: 7 },
{ description: "Küchenrolle", quantity: 1, unitPrice: 4.78, price: 4.78, taxRate: 19 },
],
suggestedCategory: "Material & Einkauf",
validation: {
isMathValid: false,
isDuplicateSuspected: false,
needsUserReview: true,
reviewField: "taxBreakdown",
reviewReason: "Abweichung",
},
imageHash: "hash-rewe-003",
originalFileName: "rewe.jpg",
fileSizeBytes: 165000,
createdAt: "2026-08-13T17:30:00.000Z",
updatedAt: "2026-08-13T17:30:00.000Z",
status: "needs_review",
},
];
// Replicate Spotlight Items generation logic for rigorous unit & contract testing
function buildCommandItems(options: {
isDe: boolean;
localReceipts: ProcessedReceipt[];
onClose: () => void;
onTriggerScan?: () => void;
onLoadDemo?: () => void;
onSelectReceipt?: (receipt: ProcessedReceipt) => void;
routerPush: (path: string) => void;
}) {
const { isDe, localReceipts, onClose, onTriggerScan, onLoadDemo, onSelectReceipt, routerPush } = options;
const actionItems = [
{
id: "action-upload",
title: isDe ? "+ Neuen Beleg hochladen / scannen" : "+ Upload / scan new receipt",
subtitle: isDe ? "Öffnet Datei-Dialog für Kassenbon-Upload" : "Opens file picker for receipt upload",
category: "actions" as const,
badge: "SCAN",
action: () => {
onClose();
if (onTriggerScan) {
onTriggerScan();
} else {
routerPush("/dashboard");
}
},
},
{
id: "action-demo",
title: isDe ? "Demo-Belege laden" : "Load demo receipts",
subtitle: isDe ? "Generiert Aral, Rewe & Trattoria Musterbelege" : "Seeds Aral, Rewe & Trattoria sample receipts",
category: "actions" as const,
badge: "DEMO",
action: () => {
onClose();
if (onLoadDemo) {
onLoadDemo();
} else {
routerPush("/dashboard");
}
},
},
{
id: "action-excel",
title: isDe ? "Excel-Export (.xlsx) erstellen" : "Generate Excel export (.xlsx)",
subtitle: isDe ? "Dual-Sheet Arbeitsmappe mit dynamischen =SUM() Formeln" : "Dual-sheet workbook with dynamic =SUM() formulas",
category: "actions" as const,
badge: "XLSX",
action: () => {
onClose();
routerPush("/dashboard/export");
},
},
{
id: "action-csv",
title: isDe ? "Buchhaltungs-CSV exportieren" : "Export accounting CSV",
subtitle: isDe ? "Semikolon, Dezimalkomma, UTF-8 BOM öffnet in Excel" : "Semicolon, decimal commas, UTF-8 BOM opens in Excel",
category: "actions" as const,
badge: "CSV",
action: () => {
onClose();
routerPush("/dashboard/export");
},
},
];
const receiptItems = localReceipts.map((r) => ({
id: `receipt-${r.id}`,
title: `${r.merchant?.name || "Unbekannter Händler"}${r.totalAmount?.value ? r.totalAmount.value.toFixed(2) + " €" : ""}`,
subtitle: `${r.date?.isoDate || "Kein Datum"}${r.suggestedCategory || "Beleg"} ${r.receiptNumber ? "• #" + r.receiptNumber : ""}`,
category: "receipts" as const,
badge: r.validation?.isMathValid ? "VALID" : "REVIEW",
action: () => {
onClose();
if (onSelectReceipt) {
onSelectReceipt(r);
} else {
routerPush("/dashboard/activity");
}
},
}));
const navItems = [
{
id: "nav-overview",
title: isDe ? "Dashboard Übersicht" : "Dashboard Overview",
subtitle: "/dashboard",
category: "navigation" as const,
badge: "GOTO",
action: () => {
onClose();
routerPush("/dashboard");
},
},
{
id: "nav-activity",
title: isDe ? "Scan-Archiv & Belege" : "Scan Activity & Archive",
subtitle: "/dashboard/activity",
category: "navigation" as const,
badge: "GOTO",
action: () => {
onClose();
routerPush("/dashboard/activity");
},
},
{
id: "nav-export",
title: isDe ? "Export-Zentrale (Excel & CSV)" : "Export Dispatcher (Excel & CSV)",
subtitle: "/dashboard/export",
category: "navigation" as const,
badge: "GOTO",
action: () => {
onClose();
routerPush("/dashboard/export");
},
},
{
id: "nav-settings",
title: isDe ? "System- & KI-Einstellungen" : "System & AI Settings",
subtitle: "/dashboard/settings",
category: "navigation" as const,
badge: "GOTO",
action: () => {
onClose();
routerPush("/dashboard/settings");
},
},
{
id: "nav-landing",
title: isDe ? "Landing Page & Hero Scanner" : "Landing Page & Hero Scanner",
subtitle: "/",
category: "navigation" as const,
badge: "HOME",
action: () => {
onClose();
routerPush("/");
},
},
];
return { actionItems, receiptItems, navItems };
}
function filterCommandItems(
items: { actionItems: any[]; receiptItems: any[]; navItems: any[] },
query: string
) {
const q = query.toLowerCase().trim();
const filteredActions = items.actionItems.filter(
(item) => item.title.toLowerCase().includes(q) || item.subtitle?.toLowerCase().includes(q)
);
const filteredReceipts = items.receiptItems.filter(
(item) => item.title.toLowerCase().includes(q) || item.subtitle?.toLowerCase().includes(q)
);
const filteredNav = items.navItems.filter(
(item) => item.title.toLowerCase().includes(q) || item.subtitle?.toLowerCase().includes(q)
);
return {
filteredActions,
filteredReceipts,
filteredNav,
filteredItems: [...filteredActions, ...filteredReceipts, ...filteredNav],
};
}
// ----------------- TEST SUITE -----------------
console.log("================================================================");
console.log("ADVERSARIAL CHALLENGER M1_2: SPOTLIGHT & INTERACTION CONTRACTS");
console.log("================================================================\n");
let passed = 0;
let total = 0;
function runTest(name: string, fn: () => void) {
total++;
try {
fn();
console.log(`${name}`);
passed++;
} catch (err: any) {
console.error(`${name}`);
console.error(` ${err.message}`);
throw err;
}
}
// 1. Keyboard Shortcut Listener Tests (TopNav.tsx & Global contracts)
runTest("TopNav keydown: Cmd+k triggers modal toggle on macOS", () => {
let isOpen = false;
const toggle = () => { isOpen = !isOpen; };
const eventMac = { metaKey: true, ctrlKey: false, key: "k", defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
if ((eventMac.metaKey || eventMac.ctrlKey) && eventMac.key.toLowerCase() === "k") {
eventMac.preventDefault();
toggle();
}
assertEquals(isOpen, true, "Spotlight should open on metaKey + k");
assertEquals(eventMac.defaultPrevented, true, "Default event should be prevented");
});
runTest("TopNav keydown: Ctrl+k triggers modal toggle on Windows/Linux", () => {
let isOpen = false;
const toggle = () => { isOpen = !isOpen; };
const eventWin = { metaKey: false, ctrlKey: true, key: "k", defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
if ((eventWin.metaKey || eventWin.ctrlKey) && eventWin.key.toLowerCase() === "k") {
eventWin.preventDefault();
toggle();
}
assertEquals(isOpen, true, "Spotlight should open on ctrlKey + k");
assertEquals(eventWin.defaultPrevented, true, "Default event should be prevented");
});
runTest("TopNav keydown: Case-insensitive 'K' (Caps Lock or Shift) is recognized", () => {
let isOpen = false;
const toggle = () => { isOpen = !isOpen; };
const eventShift = { metaKey: true, ctrlKey: false, key: "K", defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
if ((eventShift.metaKey || eventShift.ctrlKey) && eventShift.key.toLowerCase() === "k") {
eventShift.preventDefault();
toggle();
}
assertEquals(isOpen, true, "Spotlight should open on metaKey + uppercase K");
});
runTest("TopNav keydown: Unrelated keys (e.g. Cmd+S, Ctrl+P, 'k' alone) do NOT trigger Spotlight", () => {
let isOpen = false;
const toggle = () => { isOpen = !isOpen; };
const tests = [
{ metaKey: true, ctrlKey: false, key: "s" },
{ metaKey: false, ctrlKey: true, key: "p" },
{ metaKey: false, ctrlKey: false, key: "k" },
{ metaKey: true, ctrlKey: false, key: "Meta" },
];
for (const e of tests) {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
toggle();
}
}
assertEquals(isOpen, false, "Spotlight should not open on non-K shortcuts or K without modifier");
});
// 2. SpotlightDialog Arrow Navigation & Selection
runTest("SpotlightDialog keydown: ArrowDown navigates cyclically", () => {
const items = [1, 2, 3, 4];
let selectedIndex = 0;
const navigateDown = () => {
selectedIndex = items.length > 0 ? (selectedIndex + 1) % items.length : 0;
};
navigateDown(); // 1
assertEquals(selectedIndex, 1, "Should move to index 1");
navigateDown(); // 2
assertEquals(selectedIndex, 2, "Should move to index 2");
navigateDown(); // 3
assertEquals(selectedIndex, 3, "Should move to index 3");
navigateDown(); // 0 (wrap-around)
assertEquals(selectedIndex, 0, "Should wrap around to index 0");
});
runTest("SpotlightDialog keydown: ArrowUp navigates cyclically backwards", () => {
const items = [1, 2, 3, 4];
let selectedIndex = 0;
const navigateUp = () => {
selectedIndex = items.length > 0 ? (selectedIndex - 1 + items.length) % items.length : 0;
};
navigateUp(); // 3 (wrap-around backwards)
assertEquals(selectedIndex, 3, "Should wrap backwards to index 3");
navigateUp(); // 2
assertEquals(selectedIndex, 2, "Should move to index 2");
navigateUp(); // 1
assertEquals(selectedIndex, 1, "Should move to index 1");
navigateUp(); // 0
assertEquals(selectedIndex, 0, "Should move to index 0");
});
runTest("SpotlightDialog keydown: Arrow navigation with empty results does not throw or return NaN", () => {
const items: any[] = [];
let selectedIndex = 0;
const navigateDown = () => {
selectedIndex = items.length > 0 ? (selectedIndex + 1) % items.length : 0;
};
const navigateUp = () => {
selectedIndex = items.length > 0 ? (selectedIndex - 1 + items.length) % items.length : 0;
};
navigateDown();
assertEquals(selectedIndex, 0, "Empty list down should be 0");
navigateUp();
assertEquals(selectedIndex, 0, "Empty list up should be 0");
assert(!Number.isNaN(selectedIndex), "Index must not be NaN");
});
runTest("SpotlightDialog keydown: Escape triggers onClose", () => {
let closed = false;
const onClose = () => { closed = true; };
const eventEsc = { key: "Escape", defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
if (eventEsc.key === "Escape") {
eventEsc.preventDefault();
onClose();
}
assertEquals(closed, true, "Escape must close dialog");
assertEquals(eventEsc.defaultPrevented, true, "Escape must prevent default");
});
runTest("SpotlightDialog keydown: Enter invokes action of selected item", () => {
let actionExecuted = false;
const items = [
{ id: "1", action: () => { actionExecuted = false; } },
{ id: "2", action: () => { actionExecuted = true; } },
];
const selectedIndex = 1;
const eventEnter = { key: "Enter", defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
if (eventEnter.key === "Enter") {
eventEnter.preventDefault();
if (items[selectedIndex]) {
items[selectedIndex].action();
}
}
assertEquals(actionExecuted, true, "Enter must execute selected item action");
assertEquals(eventEnter.defaultPrevented, true, "Enter must prevent default");
});
runTest("SpotlightDialog keydown: Enter with empty list is safely ignored", () => {
const items: any[] = [];
const selectedIndex = 0;
let executed = false;
if (items[selectedIndex]) {
items[selectedIndex].action();
executed = true;
}
assertEquals(executed, false, "Enter on empty list should do nothing safely");
});
// 3. Search Filtering Adversarial Cases
runTest("Search Filtering: Empty query returns all 4 actions, all receipts, and all 5 navigation links", () => {
let closed = false;
let pushedRoute = "";
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
routerPush: (p) => { pushedRoute = p; },
});
const { filteredActions, filteredReceipts, filteredNav, filteredItems } = filterCommandItems(items, "");
assertEquals(filteredActions.length, 4, "Should have 4 actions");
assertEquals(filteredReceipts.length, 3, "Should have 3 receipts");
assertEquals(filteredNav.length, 5, "Should have 5 navigation links");
assertEquals(filteredItems.length, 12, "Total items should be 12");
});
runTest("Search Filtering: Case-insensitive & whitespace trimmed matching", () => {
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => {},
routerPush: () => {},
});
const r1 = filterCommandItems(items, " ARAL ");
assertEquals(r1.filteredReceipts.length, 1, "Should find Aral receipt");
assertEquals(r1.filteredReceipts[0].title.includes("Aral"), true, "Title must contain Aral");
const r2 = filterCommandItems(items, "TrAtToRiA");
assertEquals(r2.filteredReceipts.length, 1, "Should find Trattoria receipt");
const r3 = filterCommandItems(items, " rewe ");
assertEquals(r3.filteredReceipts.length, 1, "Should find Rewe receipt");
});
runTest("Search Filtering: Filter by currency amount and receipt number", () => {
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => {},
routerPush: () => {},
});
// Search by amount "68.45"
const rAmount = filterCommandItems(items, "68.45");
assertEquals(rAmount.filteredReceipts.length, 1, "Should match 68.45 €");
assertEquals(rAmount.filteredReceipts[0].id, "receipt-rec-001", "Should be Aral receipt");
// Search by receipt number "TRAT-1002"
const rNum = filterCommandItems(items, "TRAT-1002");
assertEquals(rNum.filteredReceipts.length, 1, "Should match receipt number TRAT-1002");
assertEquals(rNum.filteredReceipts[0].id, "receipt-rec-002", "Should be Trattoria receipt");
});
runTest("Search Filtering: Filter by category tag (e.g. Bewirtung, Tanken)", () => {
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => {},
routerPush: () => {},
});
const rCat = filterCommandItems(items, "Bewirtung");
assertEquals(rCat.filteredReceipts.length, 1, "Should match Bewirtung category");
assertEquals(rCat.filteredReceipts[0].id, "receipt-rec-002", "Should be Trattoria");
});
runTest("Search Filtering: Filter by Action names (e.g. 'Excel', 'CSV', 'Demo', 'Upload')", () => {
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => {},
routerPush: () => {},
});
// Both export actions legitimately mention Excel (the CSV opens in it), so a
// bare "Excel" query must return both — filtering by the format token is what
// has to disambiguate them.
const rExcel = filterCommandItems(items, "Excel");
assertEquals(rExcel.filteredActions.length, 2, "Should find both export actions");
const rXlsx = filterCommandItems(items, ".xlsx");
assertEquals(rXlsx.filteredActions.length, 1, "Should find Excel action");
assertEquals(rXlsx.filteredActions[0].id, "action-excel", "ID must be action-excel");
const rCsv = filterCommandItems(items, "CSV");
assertEquals(rCsv.filteredActions.length, 1, "Should find accounting CSV action");
assertEquals(rCsv.filteredActions[0].id, "action-csv", "ID must be action-csv");
});
runTest("Search Filtering: Filter by navigation path (e.g. '/dashboard/export', 'settings')", () => {
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => {},
routerPush: () => {},
});
const rSettings = filterCommandItems(items, "settings");
assertEquals(rSettings.filteredNav.length, 1, "Should find settings nav item");
assertEquals(rSettings.filteredNav[0].id, "nav-settings", "ID must be nav-settings");
const rActivity = filterCommandItems(items, "/dashboard/activity");
assertEquals(rActivity.filteredNav.length, 1, "Should find activity nav item by route substring");
});
runTest("Search Filtering: Zero-match search returns empty results gracefully", () => {
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => {},
routerPush: () => {},
});
const rNone = filterCommandItems(items, "XYZ999NONEXISTENT");
assertEquals(rNone.filteredItems.length, 0, "No items should match");
assertEquals(rNone.filteredActions.length, 0, "No actions");
assertEquals(rNone.filteredReceipts.length, 0, "No receipts");
assertEquals(rNone.filteredNav.length, 0, "No nav");
});
// 4. Action Invocation & Routing Verification
runTest("Action Invocation: Quick Action 'upload' calls onClose and custom trigger or router fallback", () => {
let closed = false;
let scanTriggered = false;
let pushedRoute = "";
const itemsWithTrigger = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
onTriggerScan: () => { scanTriggered = true; },
routerPush: (p) => { pushedRoute = p; },
});
itemsWithTrigger.actionItems.find((a) => a.id === "action-upload")?.action();
assertEquals(closed, true, "onClose must be called");
assertEquals(scanTriggered, true, "onTriggerScan must be called");
assertEquals(pushedRoute, "", "routerPush should not be called when onTriggerScan is provided");
// Test fallback without onTriggerScan
closed = false;
pushedRoute = "";
const itemsWithoutTrigger = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
routerPush: (p) => { pushedRoute = p; },
});
itemsWithoutTrigger.actionItems.find((a) => a.id === "action-upload")?.action();
assertEquals(closed, true, "onClose must be called");
assertEquals(pushedRoute, "/dashboard", "routerPush should fallback to /dashboard");
});
runTest("Action Invocation: Quick Action 'demo' calls onClose and custom trigger or router fallback", () => {
let closed = false;
let demoTriggered = false;
let pushedRoute = "";
const itemsWithDemo = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
onLoadDemo: () => { demoTriggered = true; },
routerPush: (p) => { pushedRoute = p; },
});
itemsWithDemo.actionItems.find((a) => a.id === "action-demo")?.action();
assertEquals(closed, true, "onClose must be called");
assertEquals(demoTriggered, true, "onLoadDemo must be called");
// Fallback
closed = false;
const itemsWithoutDemo = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
routerPush: (p) => { pushedRoute = p; },
});
itemsWithoutDemo.actionItems.find((a) => a.id === "action-demo")?.action();
assertEquals(closed, true, "onClose must be called");
assertEquals(pushedRoute, "/dashboard", "routerPush should fallback to /dashboard");
});
runTest("Action Invocation: Quick Actions 'excel' and 'csv' route to /dashboard/export", () => {
let closed = false;
let pushedRoute = "";
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
routerPush: (p) => { pushedRoute = p; },
});
items.actionItems.find((a) => a.id === "action-excel")?.action();
assertEquals(closed, true, "onClose must be called");
assertEquals(pushedRoute, "/dashboard/export", "Must route to /dashboard/export");
closed = false;
pushedRoute = "";
items.actionItems.find((a) => a.id === "action-csv")?.action();
assertEquals(closed, true, "onClose must be called");
assertEquals(pushedRoute, "/dashboard/export", "Must route to /dashboard/export");
});
runTest("Action Invocation: Receipt item selection calls onSelectReceipt or routes to /dashboard/activity", () => {
let closed = false;
let selectedReceipt: any = null;
let pushedRoute = "";
const itemsWithSelect = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
onSelectReceipt: (r) => { selectedReceipt = r; },
routerPush: (p) => { pushedRoute = p; },
});
itemsWithSelect.receiptItems[0].action();
assertEquals(closed, true, "onClose must be called");
assertEquals(selectedReceipt?.id, "rec-001", "Selected receipt must match rec-001");
// Fallback
closed = false;
const itemsWithoutSelect = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
routerPush: (p) => { pushedRoute = p; },
});
itemsWithoutSelect.receiptItems[0].action();
assertEquals(closed, true, "onClose must be called");
assertEquals(pushedRoute, "/dashboard/activity", "Must route to /dashboard/activity on fallback");
});
runTest("Action Invocation: Navigation links route to their respective pages", () => {
const routes = [
{ id: "nav-overview", expected: "/dashboard" },
{ id: "nav-activity", expected: "/dashboard/activity" },
{ id: "nav-export", expected: "/dashboard/export" },
{ id: "nav-settings", expected: "/dashboard/settings" },
{ id: "nav-landing", expected: "/" },
];
for (const r of routes) {
let closed = false;
let pushedRoute = "";
const items = buildCommandItems({
isDe: true,
localReceipts: mockReceipts,
onClose: () => { closed = true; },
routerPush: (p) => { pushedRoute = p; },
});
const item = items.navItems.find((n) => n.id === r.id);
assert(item !== undefined, `Nav item ${r.id} should exist`);
item?.action();
assertEquals(closed, true, `onClose must be called for ${r.id}`);
assertEquals(pushedRoute, r.expected, `Target path must be ${r.expected}`);
}
});
// 5. English Localization Verification
runTest("Localization: English strings render appropriately when language === 'en'", () => {
const itemsEn = buildCommandItems({
isDe: false,
localReceipts: mockReceipts,
onClose: () => {},
routerPush: () => {},
});
assertEquals(itemsEn.actionItems[0].title.startsWith("+ Upload / scan"), true, "English action title");
assertEquals(itemsEn.actionItems[1].title, "Load demo receipts", "English demo title");
assertEquals(itemsEn.navItems[0].title, "Dashboard Overview", "English overview nav title");
});
// 6. Source Code Static Token & Lifecycle Invariant Audits
runTest("Source Audit: SpotlightDialog.tsx contains zero rounded-, shadow-, or gradient classes", () => {
const spotlightCode = fs.readFileSync(path.resolve(__dirname, "../src/components/dashboard/SpotlightDialog.tsx"), "utf-8");
const forbiddenPatterns = [
/\brounded(-[a-z0-9]+)?\b/g,
/\bshadow(-[a-z0-9]+)?\b/g,
/\bbg-gradient-[a-z0-9-]+\b/g,
];
for (const pattern of forbiddenPatterns) {
const matches = spotlightCode.match(pattern);
assertEquals(matches, null, `Found forbidden style match ${matches} in SpotlightDialog.tsx`);
}
});
runTest("Source Audit: TopNav.tsx contains zero rounded-, shadow-, or gradient classes", () => {
const topNavCode = fs.readFileSync(path.resolve(__dirname, "../src/components/dashboard/TopNav.tsx"), "utf-8");
const forbiddenPatterns = [
/\brounded(-[a-z0-9]+)?\b/g,
/\bshadow(-[a-z0-9]+)?\b/g,
/\bbg-gradient-[a-z0-9-]+\b/g,
];
for (const pattern of forbiddenPatterns) {
const matches = topNavCode.match(pattern);
assertEquals(matches, null, `Found forbidden style match ${matches} in TopNav.tsx`);
}
});
runTest("Source Audit: Both TopNav.tsx and SpotlightDialog.tsx properly unbind keydown event listeners", () => {
const topNavCode = fs.readFileSync(path.resolve(__dirname, "../src/components/dashboard/TopNav.tsx"), "utf-8");
const spotlightCode = fs.readFileSync(path.resolve(__dirname, "../src/components/dashboard/SpotlightDialog.tsx"), "utf-8");
assert(topNavCode.includes('window.removeEventListener("keydown"'), "TopNav must unbind keydown");
assert(spotlightCode.includes('window.removeEventListener("keydown"'), "SpotlightDialog must unbind keydown");
});
console.log("\n================================================================");
console.log(`RESULTS: ${passed} / ${total} TESTS PASSED (100% SUCCESS)`);
console.log("================================================================\n");