release: v0.14.0
Fourteen commits since v0.13.0. See CHANGELOG.md for the full list. Two additions: `npm run doctor`, which runs the config doctor from a terminal — including inside the shipped image — and exits with the worst finding, so it can gate a deployment script; and a Playwright spec that loads all fourteen admin routes against a production build. One change, invisible on screen: the admin panel got a form-field layer (#533). Five fixes: grid.gap on both axes (#513), the dashboard status badge (#539), the journal editor's unsaved-changes guard (#538), essay paragraph markup (#537), and aria-pressed on the panel's switches and pickers.
This commit is contained in:
110
e2e/admin-smoke.spec.ts
Normal file
110
e2e/admin-smoke.spec.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Smoke test for every admin route.
|
||||
*
|
||||
* This exists because of #542: a module-evaluation TDZ error in SettingsEditor
|
||||
* took the entire admin panel down in production while all four gates stayed
|
||||
* green. tsc could not see it (the read went through a hoisted function),
|
||||
* `next build` could not (the module is only evaluated in the browser), the
|
||||
* unit tests could not (nothing imports it), and an HTTP smoke check could not
|
||||
* (the server renders the shell; the throw happens after hydration).
|
||||
*
|
||||
* A real browser is the only thing that catches that class of bug, so this
|
||||
* spec loads each route and asserts three things: no error boundary, the
|
||||
* route's own content actually rendered, and nothing threw during hydration.
|
||||
*/
|
||||
|
||||
/** Matches the fallback playwright.config.ts hands the dev/start server. */
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'e2e-admin-password';
|
||||
|
||||
/**
|
||||
* Each route plus a selector that only exists once that route's own component
|
||||
* has rendered. Asserting the admin chrome would not be enough — a page that
|
||||
* throws is replaced wholesale by app/error.tsx, but a route that renders an
|
||||
* empty shell would still pass.
|
||||
*/
|
||||
const ROUTES: { path: string; content: string; name: string }[] = [
|
||||
{ path: '/admin', content: '.page-builder', name: 'dashboard' },
|
||||
{ path: '/admin/pages', content: '.page-builder', name: 'pages' },
|
||||
{ path: '/admin/journal', content: '.journal-studio', name: 'journal' },
|
||||
// AnalyticsView falls back to .admin-panel when the analytics API is
|
||||
// unreachable, which is the normal case without a live Immich behind it.
|
||||
{ path: '/admin/analytics', content: '.analytics-view, .admin-panel', name: 'analytics' },
|
||||
{ path: '/admin/help', content: '.settings-panel', name: 'help' },
|
||||
{ path: '/admin/settings', content: '.settings-editor', name: 'settings' },
|
||||
...['general', 'theme', 'grid', 'footer', 'legal', 'seo', 'security', 'about'].map((id) => ({
|
||||
path: `/admin/settings/${id}`,
|
||||
content: '.settings-panel',
|
||||
name: `settings/${id}`,
|
||||
})),
|
||||
];
|
||||
|
||||
/**
|
||||
* Console noise that says nothing about the page's own health. Without a live
|
||||
* Immich the status probe and the analytics fetch both fail, and their failures
|
||||
* are reported as console errors. Uncaught exceptions are never filtered —
|
||||
* those are the signal this test was written for.
|
||||
*/
|
||||
const IGNORED_CONSOLE = [
|
||||
/Failed to load resource/i,
|
||||
/net::ERR_/i,
|
||||
/ECONNREFUSED/i,
|
||||
/the server responded with a status of 5\d\d/i,
|
||||
];
|
||||
|
||||
function isRealConsoleError(text: string): boolean {
|
||||
return !IGNORED_CONSOLE.some((pattern) => pattern.test(text));
|
||||
}
|
||||
|
||||
test('every admin route renders without throwing', async ({ page }) => {
|
||||
const pageErrors: string[] = [];
|
||||
const consoleErrors: string[] = [];
|
||||
page.on('pageerror', (error) => pageErrors.push(error.message));
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error' && isRealConsoleError(message.text())) {
|
||||
consoleErrors.push(message.text());
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/admin');
|
||||
|
||||
// A reused local dev server may have been started without ADMIN_PASSWORD.
|
||||
// Skipping beats failing on a configuration difference — CI starts its own
|
||||
// server with the password set, so it never takes this branch.
|
||||
const disabled = await page
|
||||
.locator('.admin-disabled')
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
test.skip(disabled, 'Admin panel disabled — start the server with ADMIN_PASSWORD set.');
|
||||
|
||||
await page.fill('input[type="password"]', ADMIN_PASSWORD);
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page.locator('.page-builder')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// One test walking every route rather than one test each: the routes share a
|
||||
// login, and expect.soft keeps a broken route from hiding the ones after it.
|
||||
// When the panel goes down it usually goes down everywhere, and the useful
|
||||
// report is the full list — a run that stops at the first failure was what
|
||||
// made #542 look like a Settings problem instead of a panel-wide one.
|
||||
for (const route of ROUTES) {
|
||||
await test.step(route.name, async () => {
|
||||
pageErrors.length = 0;
|
||||
consoleErrors.length = 0;
|
||||
|
||||
await page.goto(route.path);
|
||||
|
||||
// app/error.tsx replaces the whole page when a route throws. Assert it
|
||||
// first: its absence is what "the panel still works" actually means.
|
||||
await expect
|
||||
.soft(page.locator('.empty-state__title'), `error boundary on ${route.path}`)
|
||||
.toHaveCount(0);
|
||||
await expect
|
||||
.soft(page.locator(route.content).first(), `no content on ${route.path}`)
|
||||
.toBeVisible({ timeout: 15_000 });
|
||||
|
||||
expect.soft(pageErrors, `uncaught exception on ${route.path}`).toEqual([]);
|
||||
expect.soft(consoleErrors, `console errors on ${route.path}`).toEqual([]);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user