The storefront showed no inventory after deploying the categories/tags release. No data was lost: the code queried categories/item_tags/ items.category_id against a database where the migration had not been run, and that failure was invisible at every layer. Three changes, each addressing one layer: Migrations now run at container start, so deployed code cannot be ahead of the schema and the easily-forgotten manual `docker exec migrate.js up` step disappears. migrate.js waits for Postgres to accept connections first, since the NAS brings the DB container up slower than the app, and still exits non-zero so a bad migration stops the container rather than serving a half-migrated schema. Express 4 does not forward a rejected async handler, and no error middleware was mounted, so a failing query never responded at all. Async routes are now wrapped and an error middleware guarantees a 500. A hung request is indistinguishable from an empty result in the UI, which is how a schema mismatch came to read as "the store has no items". The storefront now separates "request failed" from "no items" and offers a retry. fetchItems/fetchFilterOptions throw on a non-OK response rather than returning the parsed error body, which would have been set as the item list and crashed the grid on .map. Also restores the project-context update from 7fb5764, which was left out of PR #24 and ended up dangling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
46 lines
1.7 KiB
TypeScript
46 lines
1.7 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
test.describe('Storefront failure states', () => {
|
|
test('reports a server failure instead of claiming the store is empty', async ({ page }) => {
|
|
await page.route('**/api/items*', (route) =>
|
|
route.fulfill({ status: 500, contentType: 'application/json', body: '{"error":"internal error"}' })
|
|
);
|
|
|
|
await page.goto('/');
|
|
|
|
// Telling a customer "no items yet" when the server is broken is worse than
|
|
// saying nothing — it reads as an empty catalogue and hides the outage.
|
|
await expect(page.getByText('No items yet')).toBeHidden();
|
|
await expect(page.getByText("Couldn't load items")).toBeVisible();
|
|
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
|
|
});
|
|
|
|
test('recovers when the server comes back', async ({ page }) => {
|
|
let failing = true;
|
|
await page.route('**/api/items*', (route) => {
|
|
if (failing) {
|
|
return route.fulfill({ status: 500, contentType: 'application/json', body: '{"error":"internal error"}' });
|
|
}
|
|
return route.continue();
|
|
});
|
|
|
|
await page.goto('/');
|
|
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
|
|
|
|
failing = false;
|
|
await page.getByRole('button', { name: 'Retry' }).click();
|
|
|
|
await expect(page.getByText("Couldn't load items")).toBeHidden();
|
|
});
|
|
|
|
test('a request that never resolves does not render as an empty catalogue', async ({ page }) => {
|
|
// Mirrors the real incident: an un-migrated database left every item query
|
|
// hanging with no response at all.
|
|
await page.route('**/api/items*', () => { /* never fulfilled */ });
|
|
|
|
await page.goto('/');
|
|
|
|
await expect(page.getByText('No items yet')).toBeHidden();
|
|
});
|
|
});
|