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
3 changes: 3 additions & 0 deletions frontend/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@
"listView": "Liste",
"recentSearches": "Letzte Suchen",
"emptyState": "Suchen Sie nach Name, Marke oder stöbern Sie mit Filtern",
"emptyStateDescription": "Finden Sie gesündere Alternativen, vergleichen Sie Produkte und treffen Sie fundierte Entscheidungen.",
"trySearching": "Versuchen Sie zu suchen",
"savedSearches": "Gespeicherte Suchen",
"searchFailed": "Suche fehlgeschlagen. Bitte versuchen Sie es erneut.",
"resultsFor": "für '{query}'",
"sortedBy": "{field} {direction}",
Expand Down
3 changes: 3 additions & 0 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@
"listView": "List",
"recentSearches": "Recent searches",
"emptyState": "Search by name, brand, or browse with filters",
"emptyStateDescription": "Find healthier alternatives, compare products, and make informed choices.",
"trySearching": "Try searching for",
"savedSearches": "Saved searches",
"searchFailed": "Search failed. Please try again.",
"resultsFor": "for \"{query}\"",
"sortedBy": "{field} {direction}",
Expand Down
3 changes: 3 additions & 0 deletions frontend/messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@
"listView": "Lista",
"recentSearches": "Ostatnie wyszukiwania",
"emptyState": "Szukaj po nazwie, marce lub przeglądaj z filtrami",
"emptyStateDescription": "Znajdź zdrowsze alternatywy, porównuj produkty i podejmuj świadome wybory.",
"trySearching": "Spróbuj wyszukać",
"savedSearches": "Zapisane wyszukiwania",
"searchFailed": "Wyszukiwanie nie powiodło się. Spróbuj ponownie.",
"resultsFor": "dla \"{query}\"",
"sortedBy": "{field} {direction}",
Expand Down
46 changes: 46 additions & 0 deletions frontend/src/app/app/search/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,52 @@ describe("SearchPage", () => {
).toBeInTheDocument();
});

it("renders empty state with description and popular search chips", () => {
render(<SearchPage />, { wrapper: createWrapper() });
// Description text from search.emptyStateDescription
expect(
screen.getByText(
"Find healthier alternatives, compare products, and make informed choices.",
),
).toBeInTheDocument();
// "Try searching for" label
expect(screen.getByText("Try searching for")).toBeInTheDocument();
// First 6 popular terms rendered as chips
expect(screen.getByText("milk")).toBeInTheDocument();
expect(screen.getByText("cheese")).toBeInTheDocument();
expect(screen.getByText("yogurt")).toBeInTheDocument();
expect(screen.getByText("bread")).toBeInTheDocument();
expect(screen.getByText("butter")).toBeInTheDocument();
expect(screen.getByText("sausage")).toBeInTheDocument();
// 7th term should NOT be visible (sliced to 6)
expect(screen.queryByText("ham")).not.toBeInTheDocument();
});

it("clicking a popular search chip triggers search", async () => {
mockSearchProducts.mockResolvedValue(makeSearchResponse());
const user = userEvent.setup();

render(<SearchPage />, { wrapper: createWrapper() });

const chip = screen.getByText("milk");
await user.click(chip);

// Input should be populated with the term
const input = screen.getByPlaceholderText("Search products…");
expect(input).toHaveValue("milk");

// Search should be triggered
await waitFor(() => {
expect(mockSearchProducts).toHaveBeenCalled();
});
});

it("saved searches link has title attribute for accessibility", () => {
render(<SearchPage />, { wrapper: createWrapper() });
const link = screen.getByTitle("Saved searches");
Comment on lines +271 to +273
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test asserts accessibility via getByTitle, but title is not a reliable accessibility mechanism and isn’t exposed consistently through the accessibility tree. After adding an aria-label/accessible name on the Saved Searches link, prefer asserting with getByRole('link', { name: ... }) instead.

Suggested change
it("saved searches link has title attribute for accessibility", () => {
render(<SearchPage />, { wrapper: createWrapper() });
const link = screen.getByTitle("Saved searches");
it("saved searches link is accessible via role and name", () => {
render(<SearchPage />, { wrapper: createWrapper() });
const link = screen.getByRole("link", { name: "Saved searches" });

Copilot uses AI. Check for mistakes.
expect(link).toBeInTheDocument();
});

it("submits search on form submit and shows results", async () => {
mockSearchProducts.mockResolvedValue(makeSearchResponse());
const user = userEvent.setup();
Expand Down
36 changes: 32 additions & 4 deletions frontend/src/app/app/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ export default function SearchPage() {
}}
className="touch-target flex items-center gap-1.5 text-xs text-foreground-secondary hover:text-foreground"
aria-label={t("search.toggleViewMode")}
title={t("search.toggleViewMode")}
>
{viewMode === "list" ? (
<LayoutGrid size={14} aria-hidden="true" className="inline" />
Expand All @@ -442,6 +443,7 @@ export default function SearchPage() {
type="button"
onClick={() => setShowSaveDialog(true)}
className="touch-target text-xs text-foreground-muted hover:text-brand"
title={t("search.saveSearch")}
>
<Save size={14} aria-hidden="true" className="inline" />{" "}
<span className="hidden xs:inline">
Expand All @@ -454,6 +456,7 @@ export default function SearchPage() {
<Link
href="/app/search/saved"
className="touch-target text-xs text-foreground-muted hover:text-brand"
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Saved Searches link text is hidden on small screens (hidden xs:inline), so the link can lose its accessible name. Using only title doesn’t consistently fix this for assistive tech. Add an aria-label (and optionally keep title for mouse hover) so the link is always announced correctly.

Suggested change
className="touch-target text-xs text-foreground-muted hover:text-brand"
className="touch-target text-xs text-foreground-muted hover:text-brand"
aria-label={t("search.savedSearches")}

Copilot uses AI. Check for mistakes.
title={t("search.savedSearches")}
>
<ClipboardList
size={14}
Expand Down Expand Up @@ -492,10 +495,35 @@ export default function SearchPage() {

{/* Empty state — no search or filters active */}
{!isSearchActive && recentSearches.length === 0 && (
<EmptyStateIllustration
type="no-results"
titleKey="search.emptyState"
/>
<div className="space-y-6">
<EmptyStateIllustration
type="no-results"
titleKey="search.emptyState"
descriptionKey="search.emptyStateDescription"
/>

{/* Popular search suggestions */}
<div className="text-center">
<p className="mb-2 text-xs font-medium uppercase tracking-wider text-foreground-muted">
{t("search.trySearching")}
</p>
<div className="flex flex-wrap justify-center gap-2">
{t("search.popularTerms")
.split(",")
.slice(0, 6)
.map((term) => (
<button
key={term}
type="button"
onClick={() => selectRecent(term.trim())}
className="touch-target rounded-full border border-border px-3 py-1.5 text-sm text-foreground-secondary transition-colors hover:border-brand hover:text-brand"
>
{term.trim()}
</button>
))}
</div>
</div>
</div>
)}

{/* Loading */}
Expand Down
Loading