test(proprietary): usage meter and policy overlay coverage, plus every progress bar named - #7324
Draft
reecebrowne wants to merge 38 commits into
Draft
test(proprietary): usage meter and policy overlay coverage, plus every progress bar named#7324reecebrowne wants to merge 38 commits into
reecebrowne wants to merge 38 commits into
Conversation
reecebrowne
force-pushed
the
chore/storybook-proprietary
branch
from
August 7, 2026 12:29
3367d2e to
bdb265e
Compare
…st it exposed The a11y baseline had grandfathered 839 story entries, 71% of them color-contrast. Most of that was the harness lying: Storybook wrapped every story in the Processor's Mantine theme (SuiProvider is mounted only by PortalApp) and never loaded styles/theme.css, so the editor theme's --color-* palette resolved to nothing and Mantine fell back to its stock greys. Stories now get the theme their component runs under, with the CSS vocab that theme needs. That exposed the real failures underneath, all of which were colours chosen as fills being reused as text: - status accents: the -dark shades are the on-light text colours, so they now sit deep enough to clear 4.5:1 on the tinted surfaces they actually appear on (adds green-800, amber-700/800, red-700). - filled accents: green and red fills darken so their white labels pass; amber keeps its hue and takes dark ink instead, because darkening amber far enough for white text turns it brown. - new --c-accent-text for accent copy, which --c-primary and its hover mix were both too light for. - form labels and the required asterisk were gray-400/base red (2.3:1 and 3.4:1). - Mantine's light variant derives its own label colour (~3.7:1); a css-variables resolver points each hue at the accessible shade. - the theme defines red and aliases blue so components naming those colours stop falling through to Mantine's stock palette. Also names Mantine's icon-only dismiss buttons, which had no accessible text at all.
Mantine's c="blue"/"red"/"green"/"orange" resolve to the filled shade — a background colour. As text on the page those land between 2.9:1 and 4.2:1. Point them at the theme's on-light text tokens, which are contrast-checked and follow light/dark.
… name Clears the structural half of the grandfathered baseline. Fixed at the shared component wherever one existed, so the story files themselves needed almost no edits: - progressbars: the shared ProgressBar now REQUIRES a label, so an unnamed bar can no longer be written; StepIndicator names itself and the billing meters take a caller-supplied one. - dialogs: passing aria-label to Mantine's Modal does nothing — rest props land on the outer wrapper and aria-labelledby is overwritten after the spread — so the three affected modals compose Modal.Root/Modal.Content instead. - table headers: action and chevron columns get screen-reader-only text through the existing .sr-only utility. - inputs: named mostly by pointing aria-labelledby at the heading text already on screen, so screen-reader users get the same words sighted users read; SettingsRow and FormField now wire this automatically. - ARIA semantics: popover targets became real controls rather than divs wearing aria-haspopup, role=dialog moved off <aside>, chips stopped nesting a button inside a button, and the file grid's role=row now owns real cells. Also drops an inline colour on the admin banner's link that was overriding the themed accent. 18 new i18n keys, each added to both locales.
…d badges The generated accent ramp mixed shade 600 to 88% of the base hue, which lands at 4.2:1 on the page — and Mantine maps shade 6 to the link colour and the tuple's default text, so every link and light-variant label inherited it. Mixing further down clears it for any accent the user picks; filled surfaces read from --mantine-primary-color-filled and are untouched. Also points the accent-as-text CSS at --c-accent-text rather than --c-accent-fg (a fill/icon colour) in the seven places that used it, and fixes the file-origin badges, which set their label to the raw accent and Mantine's orange over their own 16% tints.
The filled warning kept its light amber and took dark ink, which was the odd one out — every other filled accent is white on colour, and the dark-on-amber pairing reads worse in practice than the contrast figure suggests. Deepening the fill to amber-700 puts the white label at 5.02:1, matching success exactly and danger closely. Also nudges the subtle-text grey: it was tuned to clear 4.5:1 on the page canvas, but banners tint their background darker and it landed at 4.39:1 there.
The same fill-as-text substitution the tokens already fixed, now in the 75 component stylesheet declarations that reached past them: a bare color: var(--c-primary) (or --c-danger/--c-success/--c-warning) reads the fill value, which sits between 2.2:1 and 3.5:1 on the page. Each now takes the matching text token; background, border and outline declarations are untouched. Also deepens the two code-palette greys, which are read on the code header ground rather than on white — the muted one was at 2.6:1.
.storybook/ is where the scan and gate scripts live, and where eslint expects Node globals.
…nd tokens Sweeps the sources the earlier token work didn't reach: Mantine fill variables used as text in stylesheets, inline style objects setting color to an accent or status fill, base --color-* tokens (rather than their -dark text counterparts), the primary light-variant text variable, active tab labels, and two portal rules.
Clears the last 159 grandfathered violations, so the baseline is now empty and any new violation fails the gate outright. Three root causes accounted for nearly all of them: - Mantine's semantic slots were never bound. Its -text, -outline, -light-color, -filled and -dimmed variables default to each hue's solid fill, which is chosen to carry white and is far too light to read as ink. Both theme resolvers now pin them to the -dark tokens. The portal theme had no such bindings at all, and its primaryColor tuple referenced --color-blue-*, which was never defined — so the portal silently fell back to stock Mantine blue. Those tokens now exist alongside the other SUI hues. - The legacy --color-<hue>-50/100/200 rungs all pointed at saturated 400-level primitives, so every "tint" background rendered as a fill. Real tints now back those rungs, which also repairs Mantine's [0..2] light-variant slots. - Text faded with `opacity` rather than a muted colour. Seven sites dropped below the floor that way; each now recedes via ink or surface, which is what conveyed the state anyway. Also names the icon-only assistant button, the ColorInput eye-dropper and labelless loading buttons, and marks the disabled file-picker trigger aria-disabled so axe stops treating it as an active control. Adds --c-*-solid tokens for fills that must carry a white label, and a data-user-content-preview opt-out for nodes that render a facsimile of the user's own document in colours they chose. Verified: 1,417 story renders, 0 violations.
Adds a shared story helper for tool settings panels. Most take a whole parameters hook rather than a plain object, so a story needs the hook's shape with state that actually updates when the panel writes to it — otherwise every field freezes on a static snapshot. The report's per-page list scrolls but holds no control of its own, so it was unreachable without a mouse; it now takes a tab stop, matching the file picker's scroll region.
The pipeline-builder form of both panels: a plain parameters object with a change callback, rather than the tool's own hook.
The report's scroll region gained an accessible name; the key it uses needs to exist in the locale.
Paged output preview: single and multiple results, thumbnails still rendering, and the empty states.
…onButton Tooltip reads the user's logo preference and the sidebar geometry it positions against, so any story rendering one threw. It is used by around a hundred components, which made this a harness gap rather than a per-story one — both providers now sit alongside the others in preview, matching how the real app always mounts them. OperationButton is the run control every tool panel ends with; its disabled states are the interesting ones, because each explains what the user has to fix.
--c-accent-fg is the accent tuned as a foreground — it is used as a text colour in fourteen places. Custom themes overwrite it from --user-accent-fg, which is already forced dark enough on light bases, but the default build left it on the raw fill at 3.67:1. Caught by the new Badge story: none of its consumers had one, so the token had never been scanned.
Wordmark, LogoIcon, SidebarToggleIcon and the app-switch menu items — four small components that share the app's top-left corner, so they are grouped in one file rather than four near-empty ones. AppSwitchMenuItems is a fragment of dropdown items, not a standalone menu, so the story mounts it in the Dropdown its callers use.
CardSelector's options were Cards with an onClick and nothing else — no role, no tab stop, no key handler — so the split method (and every other card choice built on it) could not be reached or chosen without a mouse. They are controls, so they now say so, and a disabled option is marked aria-disabled rather than merely faded. Found by the new story: the component had none, so it had never been scanned.
FileListItem's checkbox had onChange={() => {}} with a comment deferring
to the row's onClick — but that handler is mouse-only, so pressing Space
on the focused checkbox did nothing and file selection was unreachable by
keyboard. The checkbox now carries the keyboard path, and its click stops
at the wrapper so a mouse press doesn't toggle twice.
Two shapes of the same defect in checkout. The plan cards already had a "Select Monthly" / "Select Yearly" button, but it carried no handler — the click lived on the Paper around it, which is mouse-only. Pressing Enter on the focused button did nothing. The button is the natural control, so it now holds the handler and the card stays a mouse convenience. The billing-period cards in StaticCheckoutModal have no inner button at all, so there the card really is the control and takes the semantics to match, via a helper beside the style one it already used.
…al semantics Three more click targets with no keyboard path. DocumentThumbnail repeats its onClick across four render branches, so the semantics are built once and only when a handler was actually given — a decorative thumbnail should not take a tab stop. The OCR setup-guide text was styled as a link and opened one, but was a Text with an onClick: unreachable by keyboard and no target cue on hover. It is now an anchor. IconSelector's tiles are choices, so they say so, and each carries the label its tooltip already showed.
Clicking the page backdrop clears the current selection, but that path is mouse-only — a keyboard user had no way out of a selection at all. The backdrop stays a plain surface rather than becoming a control; it is a canvas, not a button.
Each thumbnail jumps the viewer to that page, but was a Box with an onClick — so page navigation from the sidebar was mouse-only. They are controls now, and aria-current marks the page being viewed, which the highlight alone only conveyed visually.
…abel The sidebar reads the viewer context for page counts and thumbnail rendering. Rather than standing up a ViewerProvider and the document pipeline behind it, the stories supply the small slice of context the sidebar actually touches — enough to exercise page counts, the selected page and the empty state. The story immediately caught the current page's label sitting at 2.91:1: it used the primary fill on the primary tint. It takes the accent text step now, like the other accent copy.
Both files already had stories and I rewrote them wholesale, which deleted ResultsPreview's Default and Loading and swapped HoverActionMenu's typed HoverAction fixtures for ad-hoc icon imports. Story ids are URLs, so removing a name breaks every link to it. Restored both to what main has and layered the extra states on top.
The Advanced toggle carried its name only in a title attribute, which is unreliable for screen readers and invisible to sighted keyboard users; it takes an aria-label now. Caught by the new panel story. HotkeyProvider pulls in the whole tool-workflow chain, but the display reads one function off the context — so the context is exported and the story supplies that slice, using the real formatter so the caps render as they do in the app.
The frame every tour slide sits in: hero panel, copy, step position and the action row — including the not-dismissable and disabled-action states, which decide whether a user can get past a step.
Popover.Target applies aria-haspopup and aria-expanded to whatever it wraps. Wrapping a plain div put aria-expanded somewhere it is not a permitted attribute, so the disclosure state was invalid markup rather than something assistive tech could use. The ActionIcon is the target now. Caught by the new PageSelectByNumberButton story; the viewer's search button had the same shape.
Counting a component covered by an adjacent .stories.tsx is wrong here — MantineForms covers Select, MultiSelect, NumberInput and ColorInput between them, and a sibling check reports those as gaps and invites duplicate stories. Coverage is counted by import instead. The useful part is the classification. A gap needing only props is minutes of work; one needing a context slice or MSW handlers is not, and the two should not sit in the same queue. Contexts the Storybook harness already mounts do not count against a component, and modules that return a config object rather than markup are not surfaces at all.
Storybook resolves @app/* through the proprietary vite tsconfig, but the desktop flavour resolves it desktop-first, and 34 desktop files import @app/* assets that exist only under desktop/. Those imports do not resolve in Storybook, so a desktop story file fails to load outright — worth knowing before someone spends an afternoon on it.
Not just desktop — saas, cloud and prototypes hit the same wall. Storybook resolves @app/* through the proprietary vite tsconfig, so any flavour file importing an @app/* asset that lives only in its own tree fails to resolve and the story file never loads. That is what the 0% areas are, and the fix is a build-config decision with blast radius across every existing story.
Both rail widths, since the mark swaps between icon-only and icon-plus- wordmark as the sidebar collapses.
The classifier answers 'does this need a provider', not 'is this quick'. ButtonAppearanceOverlay wants real PDF bytes and AppConfigModalLazy pulls in the entire settings tree; both classify as props-only.
The viewer's *APIBridge components register an API into the viewer context and render null — wiring, like the providers and gates already excluded. Counting them made the viewer look worse covered than it is and would have had someone writing stories that show a blank frame.
…overage report as a task The shell already had stories and I replaced them wholesale, dropping Default, SteppedWithBack and NotDismissible — and introducing a NotDismissable spelling alongside main's NotDismissible. Story ids are URLs, so each removal broke every link to it. Restored main's version and layered the new states on top. storybook-coverage.mjs was committed but referenced by nothing, so it was undiscoverable; it now has a task alongside the other storybook ones.
reecebrowne
force-pushed
the
chore/storybook-proprietary
branch
from
August 7, 2026 22:38
bdb265e to
7f2fa32
Compare
The context value type was unexported, so the fixture reached for `as never` — which switches off checking entirely, meaning a change to the context shape would not surface here. Exporting the type lets the fixture be checked against the real thing.
…verlay MeterBar's three states are the reason it exists, so each gets a story rather than leaving the bar's fill to carry the meaning alone. The overlay covers each progress case and the accent tones a policy can carry.
A progressbar with values but no name announces a bare percentage — the heading beside it is visual context a screen reader does not get from the node. Eight were unnamed; each takes the wording already shown next to it, reusing the existing key where one exists rather than inventing new copy for the same string. Verified per instance rather than by proximity: an earlier check looked at a fixed window after the first <Progress> in each file, which would mis-judge files holding several.
reecebrowne
force-pushed
the
chore/storybook-proprietary
branch
from
August 7, 2026 23:26
7f2fa32 to
4268c98
Compare
Contributor
🚀 V2 Auto-Deployment Complete!🔗 Direct Test URL (non-SSL) http://54.175.155.236:7324 🧩 Admin portal included - try it at http://54.175.155.236:7324/portal. 📚 Storybook: http://54.175.155.236:27324/ 27 stories changed (+3 config files)Stories
Config
This deployment will be automatically cleaned up when the PR is closed. 🔄 Auto-deployed for approved V2 contributors. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two components covered, and eight unnamed progress bars fixed.
billing/MeterBarcomponents/shared/PolicyEnforcingOverlayEvery progress bar now has a name
A
progressbarwith values but no accessible name announces "45%" and nothing else — the heading beside it is visual context a screen reader never receives.Found in the policy overlay,
WorkbenchBar,ViewerShareButton,AutomationRun,PdfTextEditorView,MobileScannerPage,FormFillandUpdateModal. Each takes the wording already shown next to it, reusing the existing translation key where one existed (policy.enforcingTitle,mobileScanner.uploading,pdfTextEditor.converting) rather than inventing new copy for a string the UI already displays. Three needed new keys, verified against the repo's missing-translation test.Worth stating plainly: #7309 contains a commit titled "give every control, dialog, table header and progressbar a name". It named every one the scan could reach. These weren't reachable — their components had no stories. An a11y sweep is bounded by coverage, which is the argument for treating coverage as a correctness measure rather than documentation.
On method
My first check grepped a fixed window after the first
<Progress>in each file, which would mis-judge any file holding several. The second parses each element's own attribute span. The counts happened to agree — but by luck, and I couldn't have known which candidates were real until it was rewritten.Verification
task frontend:check:all— green; 1,778 tests pass (two known Windows-only path failures).