Skip to content

Commit efe30c8

Browse files
fix(security): consolidate safeHtml/sanitizeHref into react-bridge; fix sanitizeHref hyphen-stripping bug
Moves the canonical DOMPurify and href-sanitization utilities from react-uswds/react-ontario into react-bridge (single source of truth). Component packages now re-export from react-bridge, so a future security patch is applied in one place. Fixes an active security bug in react-uswds/sanitizeHref.ts where the regex /[ -]/g (a character class range from space 0x20 to hyphen 0x2D) silently stripped hyphens from all URLs. Correct regex is /[\x00-\x1f\x7f]/g. Also: - Add coverage thresholds (80%) to react-bridge vitest config - Add tests for 6 previously untested react-uswds components (Form, FormGroup, Grid, Label, Link, ValidationChecklist); Link tests include SEC-tagged regression guards for the hyphen-stripping fix - Add wizard submission onError recovery pattern to REACT_DEVELOPER_GUIDE.md §8 - Update examples/03-multi-step-wizard to pass errorMessage as a prop instead of calling alert(); renders an Alert above the StepIndicator on failure - Expand mobile Playwright projects to cover nav, modal, side-panel, and language-selector specs; remove non-existent a11y.spec.ts reference - Add docs/DESIGN_CRITIQUE.md with 20 open design gaps (v1.0.0 backlog)
1 parent 0a0fa8c commit efe30c8

25 files changed

Lines changed: 1435 additions & 283 deletions

File tree

docs/DESIGN_CRITIQUE.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# Design Critique — gps-design-systems-react
2+
3+
**Date:** 2026-06-25
4+
**Artifact critiqued:** The full monorepo as a product — architecture, component system, developer experience, testing posture, and documentation.
5+
**Purpose:** Identify strengths and open gaps. Gaps are marked `[ ]` — they are not failures, just decisions that are currently open or deferred. Some may be intentional; treat each as a question worth answering before v1.0.0 ship.
6+
7+
---
8+
9+
## What was evaluated
10+
11+
- Package inventory and coupling model (`react-uswds`, `react-bridge`, `react-ontario`, `gov-components`)
12+
- Component API consistency across tiers
13+
- Wizard / multi-step form pattern
14+
- Testing posture per package
15+
- Developer documentation
16+
- Showcase and reference apps
17+
- Design system coverage and extensibility model
18+
19+
---
20+
21+
## Strengths (what is working well)
22+
23+
### 1. Coupling model is intentional and enforced
24+
`react-bridge` has zero design-system imports. ESLint rules enforce the separation at the import boundary. This is the right call — it means the bridge can serve USWDS, Ontario, or any future design system without modification. The constraint is documented, named (PB-1, SB-1), and has a rationale.
25+
26+
### 2. Wizard layer solves the right problem at the right abstraction
27+
`createWizardStore` + `useWizardStep` decomposes OmniScript into three named layers with clear ownership: Zustand for global memory, RHF+Zod for per-step validation, gateway for the one exit point. The uncontrolled-input decision (no re-render on keystroke) is correct for performance and is documented. The module-scope rule for `createWizardStore` is clearly stated and has an explicit anti-pattern in the docs.
28+
29+
### 3. Security posture is explicit
30+
DOMPurify is lazy-initialized, frozen, and configured with `FORCE_BODY` (mXSS hardening). `sanitizeHref` handles mixed-case bypass attempts. The gateway route dispatch uses `hasOwnProperty` to guard prototype pollution. These are not incidental — they are documented in `SECURITY.md` with a rationale table. This is a high-trust pattern for a product that will run inside a Salesforce org context.
31+
32+
### 4. i18n is first-class across all packages
33+
Each package owns an isolated `i18next` instance. The prop-wins-over-translation contract (`prop ?? t('key')`) is consistent. The `.i18n.test.tsx` sibling pattern means locale coverage is tested, not assumed.
34+
35+
### 5. Focus management is precise
36+
`queueMicrotask` (not `setTimeout`) for post-render focus moves. Explicit `triggerRef` capture for modal restoration. Boolean `isSelectingRef` for ComboBox blur guard. These indicate someone thought carefully about the LWS proxy membrane and where timing assumptions break.
37+
38+
### 6. Documentation is genuinely useful
39+
`ARCHITECTURE.md` documents the platform constraint register explicitly (modal `aria-hidden`, SVG cross-origin, Skipnav target, frontdoor expiry). `SECURITY.md` has a sanitizer decision table. `AGENT_GUIDE.md` and `REACT_DEVELOPER_GUIDE.md` target different audiences correctly. The Glossary bridges Salesforce and web vocabulary for teams coming from one side or the other.
40+
41+
---
42+
43+
## Gaps
44+
45+
Each gap is a question, not a verdict. The `[ ]` marker means: _this decision is open — address it, defer it consciously, or close it as won't-fix._
46+
47+
---
48+
49+
### Architecture and package model
50+
51+
**GAP-A1 — `gov-components` has no publishable build**
52+
`@gov-ido/gov-components` is `private: true` with no tsup build, no `main`/`module`/`types` fields, and can only be consumed from within this monorepo. The 84 components it contains are not accessible to PS teams working in separate repos.
53+
`[ ]` Decision: Is `gov-components` intended to ship as a published package, or is in-repo consumption the permanent model?
54+
_Principle: Consistency — packages that look like libraries but can't be used as libraries violate the mental model of a package registry._
55+
56+
**GAP-A2 — `react-ontario` and `react-uswds` duplicate utilities**
57+
`safeHtml` and `sanitizeHref` are implemented independently in both `react-uswds` and `react-ontario`. If a security fix is needed (e.g., a new `javascript:` bypass pattern), it must be applied in two places.
58+
`[ ]` Decision: Should shared security utilities live in `react-bridge` or a new `@sfgps-ds/utils` package?
59+
_Principle: Consistency — same thing should behave the same way everywhere._
60+
61+
**GAP-A3 — GraphQL gateway migration is partially deferred**
62+
`createGraphQLGateway` exists and is documented, but the env.d.ts block and one-line import rename needed at GA graduation are noted as deferred. The current state creates a latent inconsistency between dev and production gateway behavior.
63+
`[ ]` Decision: Is the GA migration blocked on a Salesforce SDK release date, or is there an owner and timeline?
64+
65+
**GAP-A4 — No monorepo build orchestration**
66+
Build ordering relies on npm workspace scripts with no Turborepo/Nx dependency graph. `gov-components` has no build step at all. As the number of publishable packages grows, build correctness becomes harder to guarantee.
67+
`[ ]` Decision: Is the current npm-workspace approach sufficient for the target number of packages, or is a task runner needed before v1.0.0?
68+
69+
---
70+
71+
### Component API consistency
72+
73+
**GAP-C1 — Six `react-uswds` components have no tests**
74+
`Form`, `FormGroup`, `Grid`, `Label`, `Link`, and `ValidationChecklist` have no `__tests__` directory. All six are low-complexity presentational components, but they are also the components most likely to be composed together — a regression in `Form` or `Label` may not surface until an integration test fails.
75+
`[ ]` Decision: Are these intentionally deferred (low risk, high cost), or is this a coverage gap to close before v1.0.0?
76+
_Principle: Trust — predictable behavior requires predictable coverage._
77+
78+
**GAP-C2 — `react-bridge` has no coverage thresholds**
79+
The three component packages enforce 80% coverage (branches / functions / lines / statements). `react-bridge` has no thresholds configured. This is the most critical package — a regression in `useGateway`, `createWizardStore`, or `usePlatformEvent` affects every consuming app.
80+
`[ ]` Decision: Should `react-bridge` enforce the same 80% threshold (or higher) as the component packages?
81+
82+
**GAP-C3 — `OntarioSummaryList` is excluded from the public API**
83+
The component exists in source, has a test file, and two import tests are skipped pending upstream ODS export. This creates a gap in the Ontario DS coverage that is invisible from the public API surface.
84+
`[ ]` Decision: Is there a tracking issue upstream with ODS for exporting `OntarioSummaryList`? Is a workaround (re-export with a warning) acceptable before the upstream fix lands?
85+
86+
**GAP-C4 — `gov-components` has no SPEC.md files**
87+
`react-uswds` has 49 of 59 SPEC.md files; `react-ontario` has 33 of 33. `gov-components` has 0 of 83. SPEC.md files are how the Universal Spec Matrix contracts (LWS-1 through I18N-2) are applied per component. Without them, the 84 gov components have no declared baseline for what "correct" looks like.
88+
`[ ]` Decision: Is SPEC.md coverage for `gov-components` in scope before v1.0.0, or is the private/in-repo status the reason it's deferred?
89+
90+
**GAP-C5 — `TEMPLATE.test.tsx` in `gov-components` is aspirational infrastructure not yet used**
91+
The shared test setup (`setup/test-setup`) with `assertUSWDSClass`, `LWS_RULES`, `assertNoScriptInjection`, etc., exists but is not imported by any actual component test. Component tests are self-contained. The template represents an intent that hasn't been socialized into the test authoring pattern.
92+
`[ ]` Decision: Should the shared setup be adopted as the standard for all new `gov-components` tests, or retired as over-engineering for a private package?
93+
_Principle: Clarity — a template that doesn't reflect actual practice misleads contributors._
94+
95+
---
96+
97+
### Wizard and multi-step forms
98+
99+
**GAP-W1 — No orchestrator-level error recovery pattern is documented**
100+
`useWizardStep` handles per-step validation errors via `ErrorSummary`. But the docs and examples don't show what happens when the final gateway `post` call fails — whether the wizard stays on the last step, shows a full-screen error state, or redirects. For a government service form (e.g., a permit application), submission failure recovery is a critical UX concern.
101+
`[ ]` Decision: Should the developer guide and `examples/03-multi-step-wizard/` demonstrate a submission error state as a required pattern, not just a happy path?
102+
_Principle: Trust — honest states include error states, not just loading and success._
103+
104+
**GAP-W2 — Save-in-progress / resume is absent from the wizard model**
105+
The Zustand store survives back-navigation and step remounts within a session. It does not persist across page refreshes or sessions. For government forms that may take multiple sessions to complete (disability applications, permit renewals), this is a significant gap.
106+
`[ ]` Decision: Is session persistence (e.g., localStorage hydration or a draft API endpoint) in scope for v1.0.0, or explicitly out of scope for this layer?
107+
_Cognitive principle: Zeigarnik Effect — users return to incomplete tasks; the system should meet them there._
108+
109+
**GAP-W3 — No branching/conditional step model**
110+
The wizard pattern assumes a linear step sequence. OmniScript supported conditional navigation (skip step B if answer to step A is X). The current model requires consumers to implement branching logic themselves in the orchestrator.
111+
`[ ]` Decision: Is conditional step navigation a documented pattern for consumers to own, or a gap in the bridge abstraction?
112+
113+
---
114+
115+
### Testing posture
116+
117+
**GAP-T1 — E2E coverage for `gov-components` is not verified as comprehensive**
118+
`component-lab` has 90 gov-component pages and Playwright runs against them, but there are no SPEC.md files defining what each E2E test must cover. E2E tests may exist for some components and not others, and there is no traceability between test intent and component contract.
119+
`[ ]` Decision: Is E2E coverage for `gov-components` sufficient without SPEC.md contracts, or does this create a compliance risk (AODA-1, AODA-2, AODA-3)?
120+
121+
**GAP-T2 — Mobile E2E coverage is limited to two specs**
122+
Playwright mobile projects (Pixel 5, iPhone 12) are limited to `a11y.spec.ts` and `banner.spec.ts`. The remaining ~138 E2E specs run desktop-only. Components with touch targets, overflow menus, or responsive layout (Nav, SidePanel, LanguageSelector, Modal) are not validated on mobile.
123+
`[ ]` Decision: Is mobile E2E coverage a v1.0.0 requirement or deferred to a post-ship iteration?
124+
_Interaction principle: Touch targets must be validated at runtime, not assumed from code._
125+
126+
**GAP-T3 — `react-bridge` has no E2E coverage**
127+
Unit tests cover gateway, wizard, config, and pub/sub in isolation. There is no E2E fixture that exercises a full wizard-submit-gateway cycle against a mock server. The `03-multi-step-wizard` reference app may serve this purpose, but it is not wired to the Playwright suite.
128+
`[ ]` Decision: Should the reference apps be part of the automated test suite, or are they documentation artifacts only?
129+
130+
---
131+
132+
### Developer experience
133+
134+
**GAP-D1 — No component storybook or interactive component browser**
135+
`component-lab` and `ontario-lab` are Vite fixtures with manually authored pages — one page per component. There is no Storybook, Histoire, or equivalent that provides interactive prop controls, variant documentation, or a searchable component index. PS teams evaluating which component to use must read source or docs.
136+
`[ ]` Decision: Is an interactive component browser in scope, or is the Experience Cloud showcase the intended browsing surface?
137+
_Cognitive principle: Recognition over recall — making variants visible reduces the need for contributors to remember what a component can do._
138+
139+
**GAP-D2 — No automated changelog / release notes generation**
140+
Changesets CLI is present for versioning, but there is no documented process for what a changelog entry must contain, who writes it, or whether it is generated from commit messages or authored manually. For an OSS package, changelog quality directly affects consumer trust.
141+
`[ ]` Decision: Is changelog authoring a manual step owned by the release engineer, or should it be automated from conventional commit messages?
142+
143+
**GAP-D3 — `ADDING_A_DESIGN_SYSTEM.md` has no reference implementation**
144+
The 9-phase checklist for onboarding a new design system is comprehensive. But the first two real integrations (USWDS and Ontario) both exist in the same repo, so there is no external reference for a team adding a third design system from a blank slate.
145+
`[ ]` Decision: Should `react-ontario` serve as the canonical worked example, with the checklist cross-referencing it explicitly?
146+
147+
---
148+
149+
### Documentation
150+
151+
**GAP-DOC1 — No user-facing error message catalog**
152+
`SECURITY.md` documents what the sanitizer blocks. `ARCHITECTURE.md` documents the platform constraint register. Neither document covers what error messages a user sees in the browser when things go wrong (Zod validation messages, gateway errors, config validation failures). These are usually authored ad-hoc in each component or wizard step.
153+
`[ ]` Decision: Should there be a standard error message vocabulary for this system, or is error copy owned entirely by the consuming app?
154+
_Principle: Clarity — explicit labels beat clever ones; error messages are labels for failure states._
155+
156+
**GAP-DOC2 — Agentforce integration is documented but not showcased end-to-end**
157+
`AgentforceWidget` has a separate entry point (`./agentforce`), a mock adapter, and a token server (`agentforce-token-server/`). There is no complete worked example showing how a PS team would wire the widget to a real Agentforce agent in a scratch org. The token server is a `server.mjs` with no accompanying instructions in the quickstart.
158+
`[ ]` Decision: Is an end-to-end Agentforce integration example in scope for v1.0.0?
159+
160+
---
161+
162+
## Summary of open gaps by area
163+
164+
| Area | Gap IDs | Count |
165+
|---|---|---|
166+
| Architecture / packaging | A1, A2, A3, A4 | 4 |
167+
| Component API consistency | C1, C2, C3, C4, C5 | 5 |
168+
| Wizard / multi-step forms | W1, W2, W3 | 3 |
169+
| Testing posture | T1, T2, T3 | 3 |
170+
| Developer experience | D1, D2, D3 | 3 |
171+
| Documentation | DOC1, DOC2 | 2 |
172+
| **Total** | | **20** |
173+
174+
---
175+
176+
## Recommended prioritization for v1.0.0
177+
178+
The following gaps most directly affect **trust** (the principle that a system behaves predictably and honestly) and **consumer correctness** (PS teams building on this will reproduce whatever the reference establishes):
179+
180+
1. **GAP-C2** — Add coverage thresholds to `react-bridge`. Highest-value, lowest-effort fix.
181+
2. **GAP-W1** — Document and implement submission error recovery in the wizard example. Government forms will fail in production; the pattern needs to exist before it's needed.
182+
3. **GAP-A2** — Consolidate `safeHtml`/`sanitizeHref` to a single source. Security utilities should not drift.
183+
4. **GAP-C1** — Add baseline tests for the six untested `react-uswds` components. Low complexity, low effort, closes a visible gap before OSS publish.
184+
5. **GAP-T2** — Expand mobile E2E to at least the overlay/navigation components (Nav, Modal, SidePanel). AODA compliance requires mobile validation.
185+
186+
The remaining gaps (A1, A3, A4, C3, C4, C5, W2, W3, T1, T3, D1, D2, D3, DOC1, DOC2) are legitimate deferred decisions — not bugs. Each should have a named owner and a milestone before the OSS repo goes public.

docs/REACT_DEVELOPER_GUIDE.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -846,27 +846,41 @@ The orchestrator tracks the current step and makes the single gateway call on fi
846846
```tsx
847847
function CaseWizard() {
848848
const [step, setStep] = useState(1);
849+
const [submitError, setSubmitError] = useState<string | null>(null);
849850
const { payload, reset } = useCaseWizard();
850851
const { mutate: submit, isPending } = useGatewayMutation<CasePayload, { caseNumber: string }>('cases');
851852
852853
function handleFinalSubmit() {
854+
// Clear any prior submission error before retrying.
855+
setSubmitError(null);
853856
// One call. Apex receives the complete typed payload.
854857
// Validation Rules, Flows, and Triggers handle everything else.
855858
submit(payload, {
856-
onSuccess: (result) => { reset(); setStep(1); },
859+
onSuccess: () => { reset(); setStep(1); },
860+
onError: (err) => {
861+
// The wizard store is still populated — the user can fix and resubmit
862+
// without re-entering data. Always handle onError: government forms will
863+
// fail in production and the user needs a recovery path.
864+
setSubmitError((err as Error)?.message ?? 'Submission failed. Please try again.');
865+
},
857866
});
858867
}
859868
860869
return (
861870
<>
871+
{submitError && (
872+
<Alert heading="Submission error" type="error">{submitError}</Alert>
873+
)}
862874
{step === 1 && <StepAgency onNext={() => setStep(2)} />}
863875
{step === 2 && <StepDraft onNext={() => setStep(3)} onBack={() => setStep(1)} />}
864-
{step === 3 && <StepReview onBack={() => setStep(2)} onSubmit={handleFinalSubmit} />}
876+
{step === 3 && <StepReview onBack={() => setStep(2)} onSubmit={handleFinalSubmit} isPending={isPending} />}
865877
</>
866878
);
867879
}
868880
```
869881

882+
> **Always handle `onError` on the final submit.** The wizard store is still populated when a gateway call fails — the user stays on the review step and can resubmit without re-entering data. Displaying an `<Alert type="error">` above the step gives the user a clear recovery path. Never leave `onError` undefined in a production wizard.
883+
870884
### Testing a wizard step
871885

872886
Provide the store's payload as `storePayload` and assert that `updatePayload` receives the correct value after a valid submit.

0 commit comments

Comments
 (0)