refactor(catalog): derive desktop sort options from SORT_FIELD_LABELS
All checks were successful
deploy / test-backend (pull_request) Successful in 27m32s
deploy / test-frontend (pull_request) Successful in 10m21s
deploy / build-and-push (pull_request) Has been skipped
deploy / deploy (pull_request) Has been skipped

The desktop sort <select> hard-coded its four <option>s while the mobile
sort sheet rendered them from SORT_FIELD_LABELS. The two happened to match,
but a label rename in mangaSort.ts would have silently diverged the desktop
control. Render the desktop options from the same map so they can't.

No behaviour change (the rendered options are identical), so no version
bump. Adds an e2e guard asserting the desktop select's option values and
labels equal SORT_FIELD_LABELS in order — verified red against the old
hard-coded markup (with a label renamed) and green after.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-25 19:22:45 +02:00
parent dee53fa212
commit a08c49b708
2 changed files with 35 additions and 4 deletions

View File

@@ -1,4 +1,5 @@
import { test, expect, type Page } from '@playwright/test';
import { SORT_FIELD_LABELS } from '../src/lib/mangaSort';
// These E2E tests run against the SvelteKit dev server, which proxies /api
// to the backend. Playwright starts vite via `webServer` (see
@@ -44,6 +45,35 @@ test('home page renders the Mangalord heading and search input', async ({ page }
await expect(page.getByTestId('empty')).toContainText('No mangas yet');
});
// Anti-drift guard: the desktop sort <select> and the mobile sort sheet must
// both enumerate exactly SORT_FIELD_LABELS, in the same order. The desktop
// select used to hard-code its <option>s, so a label rename in mangaSort.ts
// would silently diverge the two surfaces. Comparing the rendered options to
// the single source of truth makes that divergence fail loudly.
test('desktop sort options stay in sync with SORT_FIELD_LABELS', async ({ page }) => {
await mockAnonymous(page);
await page.route('**/api/v1/mangas*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(emptyPage)
});
});
await page.goto('/');
const select = page.getByTestId('sort-select');
await expect(select).toBeVisible();
const options = select.locator('option');
const labels = (await options.allTextContents()).map((s) => s.trim());
const values = await options.evaluateAll((opts) =>
opts.map((o) => (o as HTMLOptionElement).value)
);
expect(values).toEqual(Object.keys(SORT_FIELD_LABELS));
expect(labels).toEqual(Object.values(SORT_FIELD_LABELS));
});
test('search updates the manga list', async ({ page }) => {
await mockAnonymous(page);
let lastSearch: string | null = null;

View File

@@ -538,10 +538,11 @@
<label class="sort">
<span>Sort</span>
<select bind:value={sort} onchange={onSortChange} data-testid="sort-select">
<option value="updated">Last updated</option>
<option value="created">Date added</option>
<option value="title">Title</option>
<option value="author">Author</option>
<!-- Options derive from SORT_FIELD_LABELS — the same source the
mobile sort sheet uses — so the two surfaces can't drift. -->
{#each Object.entries(SORT_FIELD_LABELS) as [value, label] (value)}
<option {value}>{label}</option>
{/each}
</select>
</label>
<div class="sort">