mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 03:36:59 +00:00
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:
@@ -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'],
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ActiveView } from './hooks/useViewNavigationState';
|
||||
|
||||
// How a top-level view behaves below the md breakpoint.
|
||||
// bespoke a dedicated phone screen (masthead-led, in components/mobile/)
|
||||
// responsive the desktop view reflowed via max-md: utilities, no bespoke layout
|
||||
// desktop-only a heavy authoring/terminal surface that stays desktop-first
|
||||
// detail the full-screen stack-detail surface (the editor view)
|
||||
export type MobileTreatment = 'bespoke' | 'responsive' | 'desktop-only' | 'detail';
|
||||
|
||||
// Single source of truth for the mobile treatment of every top-level view.
|
||||
//
|
||||
// The `Record<ActiveView, ...>` shape is the guard: adding a new `ActiveView`
|
||||
// without classifying it here fails `tsc`, so no view can ship without a
|
||||
// deliberate decision about its phone behavior. `mobile-treatments.test.ts`
|
||||
// ties the 'bespoke' entries to `BESPOKE_MOBILE_VIEWS` and to the bespoke
|
||||
// screens actually wired in EditorLayout, so the declaration cannot drift from
|
||||
// the implementation.
|
||||
export const MOBILE_TREATMENTS: Record<ActiveView, MobileTreatment> = {
|
||||
dashboard: 'bespoke',
|
||||
fleet: 'bespoke',
|
||||
'scheduled-ops': 'bespoke',
|
||||
settings: 'bespoke',
|
||||
editor: 'detail',
|
||||
resources: 'responsive',
|
||||
templates: 'responsive',
|
||||
'global-observability': 'responsive',
|
||||
'auto-updates': 'responsive',
|
||||
'audit-log': 'responsive',
|
||||
'host-console': 'desktop-only',
|
||||
};
|
||||
|
||||
// The content surfaces that render a bespoke phone screen instead of the
|
||||
// reflowed desktop workspace. Derived from MOBILE_TREATMENTS so it cannot drift
|
||||
// from the declared treatments. EditorLayout drops the global TopBar for these
|
||||
// and renders their masthead-led screen.
|
||||
export const BESPOKE_MOBILE_VIEWS: ReadonlySet<ActiveView> = new Set(
|
||||
(Object.keys(MOBILE_TREATMENTS) as ActiveView[]).filter(
|
||||
view => MOBILE_TREATMENTS[view] === 'bespoke',
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user