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
10 changes: 9 additions & 1 deletion frontend/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,11 @@
"emptyMessage": "Scannen Sie einen Barcode, um Ihren Verlauf aufzubauen.",
"startScanning": "Scannen starten →",
"submissionStatus": "Einreichung: {status}",
"submit": "Einreichen →"
"submit": "Einreichen →",
"today": "Heute",
"yesterday": "Gestern",
"thisWeek": "Diese Woche",
"earlier": "Früher"
},
"submit": {
"title": "Produkt einreichen",
Expand Down Expand Up @@ -1988,5 +1992,9 @@
"bronze": "Bronze-Beiträger",
"silver": "Silber-Beiträger",
"gold": "Gold-Beiträger"
},
"categoryPicker": {
"showAll": "Alle Kategorien anzeigen",
"showLess": "Weniger anzeigen"
}
}
10 changes: 9 additions & 1 deletion frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,11 @@
"emptyMessage": "Scan a barcode to start building your history.",
"startScanning": "Start scanning →",
"submissionStatus": "Submission: {status}",
"submit": "Submit →"
"submit": "Submit →",
"today": "Today",
"yesterday": "Yesterday",
"thisWeek": "This Week",
"earlier": "Earlier"
},
"submit": {
"title": "Submit Product",
Expand Down Expand Up @@ -1988,5 +1992,9 @@
"bronze": "Bronze Contributor",
"silver": "Silver Contributor",
"gold": "Gold Contributor"
},
"categoryPicker": {
"showAll": "Show all categories",
"showLess": "Show less"
}
}
10 changes: 9 additions & 1 deletion frontend/messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,11 @@
"emptyMessage": "Zeskanuj kod kreskowy, aby rozpocząć budowanie historii.",
"startScanning": "Rozpocznij skanowanie →",
"submissionStatus": "Zgłoszenie: {status}",
"submit": "Zgłoś →"
"submit": "Zgłoś →",
"today": "Dzisiaj",
"yesterday": "Wczoraj",
"thisWeek": "Ten tydzień",
"earlier": "Wcześniej"
},
"submit": {
"title": "Zgłoś produkt",
Expand Down Expand Up @@ -1988,5 +1992,9 @@
"bronze": "Brązowy Kontrybutor",
"silver": "Srebrny Kontrybutor",
"gold": "Złoty Kontrybutor"
},
"categoryPicker": {
"showAll": "Pokaż wszystkie kategorie",
"showLess": "Pokaż mniej"
}
}
104 changes: 76 additions & 28 deletions frontend/src/app/app/scan/history/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,18 @@ vi.mock("@/lib/supabase/client", () => ({
createClient: () => ({}),
}));

vi.mock("@/lib/i18n", () => ({
useTranslation: () => ({
t: (key: string, params?: Record<string, unknown>) => {
if (params) return `${key}:${JSON.stringify(params)}`;
return key;
},
}),
}));

const mockPush = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: mockPush }),
useRouter: () => ({ push: mockPush, back: vi.fn() }),
}));

vi.mock("next/link", () => ({
Expand All @@ -35,6 +44,53 @@ vi.mock("@/components/common/skeletons", () => ({
ScanHistorySkeleton: () => <div data-testid="skeleton" role="status" aria-label="Loading scan history" />,
}));

vi.mock("@/components/common/PullToRefresh", () => ({
PullToRefresh: ({ children }: { children: React.ReactNode }) => <div data-testid="pull-to-refresh">{children}</div>,
}));

vi.mock("@/components/common/EmptyState", () => ({
EmptyState: ({ titleKey, action }: { titleKey: string; action?: { labelKey: string } }) => (
<div>
<p>{titleKey}</p>
{action && <button>{action.labelKey}</button>}
</div>
),
}));

vi.mock("@/components/common/EmptyStateIllustration", () => ({
EmptyStateIllustration: ({ titleKey, action }: { titleKey: string; action?: { labelKey: string; href?: string } }) => (
<div>
<p>{titleKey}</p>
{action && <a href={action.href}>{action.labelKey}</a>}
</div>
),
}));

vi.mock("@/components/layout/Breadcrumbs", () => ({
Breadcrumbs: () => <nav data-testid="breadcrumbs" />,
}));

vi.mock("@/lib/format-time", () => ({
formatRelativeTime: () => "just now",
}));

vi.mock("@/lib/score-utils", () => ({
getScoreColor: () => "bg-green-500",
getScoreBand: (score: number) => {
if (score == null || score < 1 || score > 100) return null;
return { band: "red", labelKey: "scoreBand.poor", color: "var(--color-score-red)", bgColor: "bg-score-red/10", textColor: "text-score-red-text" };
},
toTryVitScore: (score: number) => 100 - score,
}));

vi.mock("@/lib/constants", () => ({
NUTRI_COLORS: { A: "#038141", B: "#85BB2F", C: "#FECB02", D: "#EE8100", E: "#E63E11" },
}));

vi.mock("@/lib/events", () => ({
trackEvent: vi.fn(),
}));

// ─── Helpers ────────────────────────────────────────────────────────────────

function Wrapper({ children }: Readonly<{ children: React.ReactNode }>) {
Expand Down Expand Up @@ -112,17 +168,9 @@ describe("ScanHistoryPage", () => {
it("renders page title and subtitle", async () => {
render(<ScanHistoryPage />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByRole("heading", { name: /Scan History/i })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: /scanHistory\.title/i })).toBeInTheDocument();
});
expect(screen.getByText("Your barcode scan activity")).toBeInTheDocument();
});

it("links back to scanner", async () => {
render(<ScanHistoryPage />, { wrapper: createWrapper() });
expect(screen.getByText("← Back to Scanner").closest("a")).toHaveAttribute(
"href",
"/app/scan",
);
expect(screen.getByText("scanHistory.subtitle")).toBeInTheDocument();
});

it("shows loading skeleton", () => {
Expand All @@ -139,10 +187,10 @@ describe("ScanHistoryPage", () => {
render(<ScanHistoryPage />, { wrapper: createWrapper() });
await waitFor(() => {
expect(
screen.getByText("Failed to load scan history."),
screen.getByText("scanHistory.loadFailed"),
).toBeInTheDocument();
});
expect(screen.getByText("Retry")).toBeInTheDocument();
expect(screen.getByText("common.retry")).toBeInTheDocument();
});

it("shows empty state when no scans", async () => {
Expand All @@ -152,19 +200,19 @@ describe("ScanHistoryPage", () => {
});
render(<ScanHistoryPage />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByText("No scans yet")).toBeInTheDocument();
expect(screen.getByText("scanHistory.emptyTitle")).toBeInTheDocument();
});
expect(screen.getByText("Start scanning →").closest("a")).toHaveAttribute(
expect(screen.getByText("scanHistory.startScanning").closest("a")).toHaveAttribute(
"href",
"/app/scan",
);
});

it("renders filter buttons", async () => {
render(<ScanHistoryPage />, { wrapper: createWrapper() });
expect(screen.getByText("All")).toBeInTheDocument();
expect(screen.getByText("Found")).toBeInTheDocument();
expect(screen.getByText("Not Found")).toBeInTheDocument();
expect(screen.getByText("scanHistory.all")).toBeInTheDocument();
expect(screen.getByText("scanHistory.found")).toBeInTheDocument();
expect(screen.getByText("scanHistory.notFound")).toBeInTheDocument();
});

it("renders found scan rows with product info", async () => {
Expand Down Expand Up @@ -195,7 +243,7 @@ describe("ScanHistoryPage", () => {
await waitFor(() => {
expect(screen.getByText("Lay's Classic")).toBeInTheDocument();
});
const submitLink = screen.getByText("Submit →").closest("a");
const submitLink = screen.getByText("scanHistory.submit").closest("a");
expect(submitLink).toHaveAttribute(
"href",
"/app/scan/submit?ean=9999999999999",
Expand All @@ -205,7 +253,7 @@ describe("ScanHistoryPage", () => {
it("shows submission status when already submitted", async () => {
render(<ScanHistoryPage />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByText(/Submission: pending/)).toBeInTheDocument();
expect(screen.getByText(/scanHistory\.submissionStatus/)).toBeInTheDocument();
});
});

Expand All @@ -216,7 +264,7 @@ describe("ScanHistoryPage", () => {
});
// scan-3 has submission_status "pending" so it should NOT have a Submit → link
// scan-2 has no submission_status so it SHOULD have a Submit → link
const submitLinks = screen.getAllByText("Submit →");
const submitLinks = screen.getAllByText("scanHistory.submit");
expect(submitLinks).toHaveLength(1);
});

Expand All @@ -235,7 +283,7 @@ describe("ScanHistoryPage", () => {
await waitFor(() => {
expect(screen.getByText("Lay's Classic")).toBeInTheDocument();
});
expect(screen.queryByText("← Prev")).not.toBeInTheDocument();
expect(screen.queryByText("common.prev")).not.toBeInTheDocument();
});

it("shows pagination for multiple pages", async () => {
Expand All @@ -245,10 +293,10 @@ describe("ScanHistoryPage", () => {
});
render(<ScanHistoryPage />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByText("← Prev")).toBeInTheDocument();
expect(screen.getByText("common.prev")).toBeInTheDocument();
});
expect(screen.getByText("Next →")).toBeInTheDocument();
expect(screen.getByText("Page 1 of 3")).toBeInTheDocument();
expect(screen.getByText("common.next")).toBeInTheDocument();
expect(screen.getByText('common.pageOf:{"page":1,"pages":3}')).toBeInTheDocument();
});

it("disables prev button on first page", async () => {
Expand All @@ -258,7 +306,7 @@ describe("ScanHistoryPage", () => {
});
render(<ScanHistoryPage />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByText("← Prev")).toBeDisabled();
expect(screen.getByText("common.prev")).toBeDisabled();
});
});

Expand All @@ -270,15 +318,15 @@ describe("ScanHistoryPage", () => {
expect(screen.getByText("Lay's Classic")).toBeInTheDocument();
});

await user.click(screen.getByText("Found"));
await user.click(screen.getByText("scanHistory.found"));
// Filter should have been changed; a new query would fire
expect(mockGetScanHistory).toHaveBeenCalled();
});

it("shows not-found indicator for failed lookups", async () => {
render(<ScanHistoryPage />, { wrapper: createWrapper() });
await waitFor(() => {
const notFoundTexts = screen.getAllByText("Not Found");
const notFoundTexts = screen.getAllByText("scanHistory.notFound");
expect(notFoundTexts.length).toBeGreaterThanOrEqual(1);
});
});
Expand Down
Loading
Loading