test(mobile): declare per-view mobile treatment and harden the visual gate (#1332)

* test(mobile): single-source map for per-view mobile treatment

Declare how every top-level view behaves on a phone in one place:
MOBILE_TREATMENTS is a Record<ActiveView, ...>, so adding a new view without
classifying it (bespoke / responsive / desktop-only / detail) fails the type
check. BESPOKE_MOBILE_VIEWS is derived from it instead of hand-maintained, and
a unit test keeps the two in lockstep and pins the current bespoke set so a
change is deliberate. EditorLayout consumes the derived set; behavior is
unchanged.

* ci(visual): run the desktop-unchanged gate in its own job

Split the visual-regression spec into a dedicated Playwright "visual" project,
excluded from the default chromium project the functional E2E job runs, so a
missing or platform-mismatched baseline can no longer fail every PR.

Add a Visual Regression workflow: a compare job gates PRs into main against
committed baselines (and skips with a warning until they are seeded), and a
manual seed job regenerates the baselines on the Linux runner and commits them
to a feature branch (refusing main). Baselines are platform-specific, so they
must be produced on the runner rather than locally.

Drop the stack-detail view from the gate: a fresh CI app has no stack to open
and its live log stream is not deterministic; the shell plus the four content
views still catch a desktop base-class regression.
This commit is contained in:
Anso
2026-06-07 14:56:49 -04:00
committed by GitHub
parent 928a3a8343
commit 8302048bc4
6 changed files with 212 additions and 25 deletions
@@ -0,0 +1,32 @@
import { describe, it, expect } from 'vitest';
import { MOBILE_TREATMENTS, BESPOKE_MOBILE_VIEWS, type MobileTreatment } from './mobile-treatments';
const VALID: MobileTreatment[] = ['bespoke', 'responsive', 'desktop-only', 'detail'];
describe('mobile treatments', () => {
it('classifies every view with a known treatment', () => {
// The Record<ActiveView, MobileTreatment> type already forces every view to
// be present at compile time; this guards the values at runtime so a typo or
// a bad merge cannot leave an unknown treatment in the map.
for (const [view, treatment] of Object.entries(MOBILE_TREATMENTS)) {
expect(VALID, `${view} has an unknown treatment "${treatment}"`).toContain(treatment);
}
});
it('keeps BESPOKE_MOBILE_VIEWS in lockstep with the bespoke treatments', () => {
const declaredBespoke = Object.entries(MOBILE_TREATMENTS)
.filter(([, treatment]) => treatment === 'bespoke')
.map(([view]) => view)
.sort();
expect([...BESPOKE_MOBILE_VIEWS].sort()).toEqual(declaredBespoke);
});
it('pins the set of bespoke phone screens (update deliberately when adding one)', () => {
// A change here means a top-level view gained or lost a bespoke phone screen.
// Updating this list should go hand in hand with adding the screen under
// components/mobile/ and wiring its case in EditorLayout's renderMobileBespoke.
expect([...BESPOKE_MOBILE_VIEWS].sort()).toEqual(
['dashboard', 'fleet', 'scheduled-ops', 'settings'],
);
});
});