mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
feat(ui): make Fleet, Settings, and the dashboard table usable on mobile (#1331)
* feat(ui): make Fleet, Settings, and the dashboard table usable on mobile Tier 2 of the mobile pass, all gated below the md breakpoint so desktop renders identically: - Fleet: the tab strip scrolls horizontally and the action row (Check Updates / Refresh / Add Node) wraps instead of clipping. - Settings: below md the nav rail is a full-screen list; choosing a section pushes it full-screen with a back affordance (master/detail), matching the stack flow. Desktop keeps the two-pane layout. - Dashboard: the fixed stack-health table scrolls horizontally on a phone. Adds a Playwright desktop visual-regression spec (1280/1440/1920) as a zero-desktop-change gate; its environment-specific snapshots are gitignored. * test(ui): harden the desktop visual-regression gate Mask only the live sidebar ticker and notification count instead of the whole sidebar / top-bar shell, so the gate now proves that shell unchanged too. Lower the pixel budget to 1200 (the only residual churn is in-content live stats); run against seeded / frozen data in CI for a zero-tolerance gate. Adds a data-testid to the activity ticker so it can be masked precisely.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// Zero-desktop-change gate for the mobile work.
|
||||
//
|
||||
// Snapshots every top-level view the mobile pass touches, at desktop widths.
|
||||
// Capture the baseline on the pre-change state, then run again after the mobile
|
||||
// changes: any pixel diff means a desktop base class was edited instead of a
|
||||
// mobile-only `max-md:` override being added.
|
||||
//
|
||||
// # baseline (before the mobile changes):
|
||||
// E2E_PASSWORD=admin123 npx playwright test desktop-visual-regression --update-snapshots
|
||||
// # gate (after the changes):
|
||||
// E2E_PASSWORD=admin123 npx playwright test desktop-visual-regression
|
||||
//
|
||||
// Snapshots are platform-specific and gitignored; regenerate the baseline in
|
||||
// the same environment (or a pinned CI/Docker image) you run the gate in.
|
||||
// ---------------------------------------------------------------------------
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { loginAs, waitForStacksLoaded } from './helpers';
|
||||
|
||||
const WIDTHS = [1280, 1440, 1920] as const;
|
||||
const HEIGHT = 1080;
|
||||
|
||||
interface View {
|
||||
id: string;
|
||||
label: string;
|
||||
open: (page: Page) => Promise<void>;
|
||||
}
|
||||
|
||||
// Navigate via the app's real chrome (no router): top-bar buttons by aria-label,
|
||||
// Settings via the profile menu, stack detail via a sidebar row.
|
||||
const VIEWS: View[] = [
|
||||
{ id: 'home', label: 'home', open: async () => { /* dashboard is the landing view */ } },
|
||||
{ id: 'fleet', label: 'fleet', open: (p) => p.getByRole('button', { name: 'Fleet', exact: true }).click() },
|
||||
// Resources is intentionally omitted: its reclaim hero / table height depends
|
||||
// on async Docker data that shifts run-to-run, so it is not deterministically
|
||||
// snapshot-able in this live environment. Verify its desktop manually, or run
|
||||
// this gate against a seeded/frozen-data instance in CI to include it.
|
||||
{ id: 'app-store', label: 'app-store', open: (p) => p.getByRole('button', { name: 'App Store', exact: true }).click() },
|
||||
{
|
||||
id: 'settings', label: 'settings', open: async (p) => {
|
||||
await p.getByRole('button', { name: /profile/i }).click();
|
||||
await p.getByRole('button', { name: 'Settings', exact: true }).click();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'stack-detail', label: 'stack-detail', open: async (p) => {
|
||||
await p.locator('[data-stacks-loaded="true"] [data-testid="stack-row"]').first().click();
|
||||
await p.getByText('image', { exact: false }).first().waitFor({ timeout: 10_000 }).catch(() => {});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Paint over genuinely non-deterministic content so it does not cause false
|
||||
// diffs. Mask only the elements that actually churn between runs, NOT the
|
||||
// surrounding shell: the rest of the sidebar and the top bar stay unmasked so
|
||||
// the gate proves they are unchanged. Masked elements keep their box, so a
|
||||
// layout shift around them still moves unmasked pixels and is caught.
|
||||
function maskDynamic(page: Page) {
|
||||
return [
|
||||
// The sidebar activity ticker ("x ago" text + live dot) is the only
|
||||
// churning part of the otherwise-static sidebar.
|
||||
page.locator('[data-testid="activity-ticker"]'),
|
||||
// The top-bar notification count changes as alerts arrive.
|
||||
page.locator('[aria-label^="Notifications"]'),
|
||||
page.locator('svg'), // sparklines / gauges / charts
|
||||
page.locator('.xterm'), // live terminal (stack detail)
|
||||
page.locator('[data-testid="deploy-feedback-pill"]'),
|
||||
];
|
||||
}
|
||||
|
||||
for (const width of WIDTHS) {
|
||||
test.describe(`desktop @ ${width}px must not change`, () => {
|
||||
test.use({ viewport: { width, height: HEIGHT } });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await loginAs(page);
|
||||
await waitForStacksLoaded(page);
|
||||
});
|
||||
|
||||
for (const view of VIEWS) {
|
||||
test(view.label, async ({ page }) => {
|
||||
await view.open(page);
|
||||
// Let the fade-up entrance and async data (Docker images, fleet
|
||||
// stats) settle before snapshotting so layout height is stable.
|
||||
await page.waitForTimeout(1200);
|
||||
// Viewport-only (not fullPage): a fixed-height frame removes
|
||||
// scroll-height variance from below-fold live data, while the
|
||||
// above-fold layout is where a base-class regression shows first.
|
||||
await expect(page).toHaveScreenshot(`${view.label}-${width}.png`, {
|
||||
animations: 'disabled',
|
||||
mask: maskDynamic(page),
|
||||
// The only remaining churn is the in-content live stats that
|
||||
// can't be masked without hiding layout (dashboard gauge
|
||||
// values, fleet node CPU/MEM, per-container stats). This small
|
||||
// budget absorbs that text churn yet stays orders of magnitude
|
||||
// below any real desktop layout shift, so a base-class
|
||||
// regression still fails loudly. Run against a seeded /
|
||||
// frozen-data instance in CI to drop this to 0.
|
||||
maxDiffPixels: 1200,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user