feat(dev): WP-33 — in-app dev switchers for scenario + role
CI / frontend (push) Successful in 1m47s
CI / storybook-a11y (push) Successful in 27m52s
CI / backend (push) Successful in 1m24s
CI / e2e (push) Successful in 27m24s
CI / semgrep (push) Failing after 30s
CI / api-client-drift (push) Successful in 2m7s

Surface the ?scenario= and ?role= dev stand-ins as dropdowns in the existing
debug-state devtool, so a demo can flip them with a click instead of editing the
URL. scenario.ts/role.ts gain set* setters + exported valid-value lists (reused
by the panel, no duplicated source of truth); scenario becomes tab-sticky like
role so it survives navigation. Applied via location.reload() since both are
read per-request in interceptors. Extends the debug-state eslint exemption to the
ui→infrastructure rule (same devtool precedent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-22 16:53:26 +02:00
co-authored by Claude Opus 4.8
parent 610ff7c1cd
commit 1ed4850858
7 changed files with 161 additions and 8 deletions
+7 -2
View File
@@ -17,8 +17,8 @@ import { Role } from '@shared/domain/role';
* reset. Dev-only — the interceptor itself is only wired under `isDevMode()`.
*/
const STORAGE_KEY = 'dev-role';
const isRole = (v: string | null): v is Role =>
v === 'drafter' || v === 'approver' || v === 'admin';
export const ROLES: readonly Role[] = ['drafter', 'approver', 'admin'];
const isRole = (v: string | null): v is Role => !!v && ROLES.includes(v as Role);
export function currentRole(): Role {
const fromUrl = new URLSearchParams(window.location.search).get('role');
@@ -29,3 +29,8 @@ export function currentRole(): Role {
const stored = sessionStorage.getItem(STORAGE_KEY);
return isRole(stored) ? stored : 'drafter';
}
/** Dev switcher entry point: persist the chosen role for the tab (WP-33). */
export function setRole(r: Role): void {
sessionStorage.setItem(STORAGE_KEY, r);
}
@@ -0,0 +1,29 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { currentScenario, setScenario } from './scenario';
const setUrl = (search: string) => history.pushState({}, '', search || '/');
describe('scenario (dev mechanism)', () => {
beforeEach(() => {
sessionStorage.clear();
setUrl('/');
});
it('reads a valid ?scenario= from the URL and persists it for the tab', () => {
setUrl('?scenario=error');
expect(currentScenario()).toBe('error');
setUrl('/'); // navigation drops the query param — value stays sticky
expect(currentScenario()).toBe('error');
});
it('falls back to default when nothing is set or the value is invalid', () => {
expect(currentScenario()).toBe('default');
setUrl('?scenario=nonsense');
expect(currentScenario()).toBe('default');
});
it('setScenario persists the chosen scenario', () => {
setScenario('slow');
expect(currentScenario()).toBe('slow');
});
});
+23 -4
View File
@@ -9,7 +9,7 @@ export type Scenario =
| 'upload-slow'
| 'upload-fail';
const VALID: Scenario[] = [
export const SCENARIOS: readonly Scenario[] = [
'default',
'slow',
'loading',
@@ -19,8 +19,27 @@ const VALID: Scenario[] = [
'upload-fail',
];
/** Reads ?scenario= from the URL so a demo can force each async state. */
const STORAGE_KEY = 'dev-scenario';
const isScenario = (v: string | null): v is Scenario => !!v && SCENARIOS.includes(v as Scenario);
/**
* Reads the active demo scenario so a demo can force each async state.
* Sticky within the tab (sessionStorage), mirroring `role.ts`: a `?scenario=` in the
* URL sets it; later navigation (which drops the query param) keeps the remembered
* value. Set `?scenario=default`, use the dev switcher, or open a fresh tab to reset.
* Dev-only — the interceptor that consumes this is wired only under `isDevMode()`.
*/
export function currentScenario(): Scenario {
const s = new URLSearchParams(window.location.search).get('scenario') as Scenario | null;
return s && VALID.includes(s) ? s : 'default';
const fromUrl = new URLSearchParams(window.location.search).get('scenario');
if (isScenario(fromUrl)) {
sessionStorage.setItem(STORAGE_KEY, fromUrl);
return fromUrl;
}
const stored = sessionStorage.getItem(STORAGE_KEY);
return isScenario(stored) ? stored : 'default';
}
/** Dev switcher entry point: persist the chosen scenario for the tab (WP-33). */
export function setScenario(s: Scenario): void {
sessionStorage.setItem(STORAGE_KEY, s);
}