@micha.bigler/ui-core-micha 2.29.1 → 2.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/WORK_ORDERS.md
CHANGED
|
@@ -23,7 +23,7 @@ prefixed ID; never reuse a bare flat number across workstreams.
|
|
|
23
23
|
|
|
24
24
|
| ID | Titel | Beschreibung | Datum | Status | Commit(s) | Notiz |
|
|
25
25
|
|---|---|---|---|---|---|---|
|
|
26
|
-
| CHART-5 | Dual y-axis + always-visible x-axis ticks on `TimeSeriesChart` | Opt-in second y-axis via a new per-series `axis: 'secondary'` field (backward-compatible, off by default) with auto-detected integer-only tick labels per axis group; deterministic x-axis `tickLabelInterval` so bucket labels never all vanish under MUI's default collision-avoidance (the live symptom: zero x-axis labels with 24 hourly buckets). Reverses CHART-2's deliberate "no second y-axis" choice on fresh operator instruction; `DESIGN.md` #8 updated to record the scoped exception. Full spec: [work-orders/CHART-5.md](work-orders/CHART-5.md). | 2026-08-07 | done | 839e6c5, 84c37ac, 0dd3bac, f4e5586, 4a12a71 | Operator-requested after seeing the shipped chart live: y-axis showed decimal ticks (0.2, 0.4, ...) for an integer user-count series sharing an axis with fractional hours; x-axis showed no time labels at all. **Codex out of credits, known up front** — implemented directly (author-flip, independent review mandatory). Independent `reviewer` + `ui_reviewer` ran concurrently: **`reviewer` found R1 (P1, real bug)** — used `yAxisKey` (removed in MUI X-Charts v7, per the installed v8.29.2's own CHANGELOG and `useBarPlotData.mjs` source, which reads `series[i].yAxisId`) instead of `yAxisId`; the WO explicitly asked to verify this passthrough against real MUI behaviour rather than assume, and the first pass didn't — the dual-axis feature would have silently plotted every series on the primary axis in production despite all mocked tests passing (a mocked-prop test structurally cannot catch a wrong-field-name bug, since it only checks whatever the code itself sets). Fixed (`yAxisKey` → `yAxisId`, verified against `node_modules` source directly, not just the mock) and the pre-existing `tests/BarChart.test.jsx` "dual axis" case corrected too (R2 — it used the same wrong field name and was cited by the WO as precedent, giving false confidence). **`ui_reviewer` found U1 (P1, real bug)** — `tickLabelInterval`'s `index % step === 0` systematically dropped the LAST x-axis bucket unless it landed exactly on a step boundary (e.g. 24 labels/step 3 → indices 0..21, never 23), exactly the failure mode the WO's own SCOPE C explicitly warned to check for; fixed by force-including `index === lastIndex`, with a new `it.each([24,12,7,1])` regression test, confirmed non-vacuous by reverting the fix. **U2 (P2)**: `DESIGN.md` #8's "Never dual-axis" now contradicted the shipped code — updated to record dual-axis as a scoped, opt-in exception (CHART-5) rather than a general rule change. U3 (P3, fixed `MAX_X_TICKS` under-displays low-density ranges e.g. 12 monthly buckets showing only 6) and U4 (P3, no UI indication of which axis a toggle's series plots against) both accepted, not fixed — informational, no code change. Re-verified after fixes: 31/31 scoped vitest tests (`TimeSeriesChart.test.jsx` + `BarChart.test.jsx`) green, `tsc` build clean. Published: ucm 2.29.0 (minor — new optional per-series field + prop, additive, no breaking change). **Third bug found post-publish (2.29.1 patch)**: after jg-ferien adopted it (ACT-7) the operator reported live that the secondary axis was "obviously there but invisible" and x-axis labels were still missing. Investigated by adding a new DX-1 harness entry (`dev/entries.jsx` `TimeSeriesChartEntry`, previously missing — this component had NEVER been visually verified in a real browser through CHART-2/3/4/5) and reading the real rendered DOM directly. X-axis confirmed working (9 evenly-spread labels for 24 buckets) — the live report was very likely a stale PWA service-worker cache on jg-ferien's side, not a code defect. Y-axis confirmed broken: only 1 of 2 `yAxis` entries rendered in the DOM. Root cause traced to the installed MUI X-Charts source (`defaultizeAxis.js`): `defaultPosition = index === 0 ? 'left' : 'none'` — every yAxis array entry past the first silently renders nothing unless given an explicit `position`, a class of bug no mocked-`BarChart` test can catch (the mock never runs MUI's own defaulting logic). Fixed by setting `position: 'left'`/`'right'` explicitly on the primary/secondary entries; independent `reviewer` traced the fix against the same MUI source and confirmed no second gating layer exists. Live-reverified in the dev harness after the fix: both axes render with independent scales (primary `0,1,2`; secondary `0.0,0.5,1.0,1.5,2.0`). 32/32 tests green, build clean. Published: ucm 2.29.1 (patch). |
|
|
26
|
+
| CHART-5 | Dual y-axis + always-visible x-axis ticks on `TimeSeriesChart` | Opt-in second y-axis via a new per-series `axis: 'secondary'` field (backward-compatible, off by default) with auto-detected integer-only tick labels per axis group; deterministic x-axis `tickLabelInterval` so bucket labels never all vanish under MUI's default collision-avoidance (the live symptom: zero x-axis labels with 24 hourly buckets). Reverses CHART-2's deliberate "no second y-axis" choice on fresh operator instruction; `DESIGN.md` #8 updated to record the scoped exception. Full spec: [work-orders/CHART-5.md](work-orders/CHART-5.md). | 2026-08-07 | done | 839e6c5, 84c37ac, 0dd3bac, f4e5586, 4a12a71 | Operator-requested after seeing the shipped chart live: y-axis showed decimal ticks (0.2, 0.4, ...) for an integer user-count series sharing an axis with fractional hours; x-axis showed no time labels at all. **Codex out of credits, known up front** — implemented directly (author-flip, independent review mandatory). Independent `reviewer` + `ui_reviewer` ran concurrently: **`reviewer` found R1 (P1, real bug)** — used `yAxisKey` (removed in MUI X-Charts v7, per the installed v8.29.2's own CHANGELOG and `useBarPlotData.mjs` source, which reads `series[i].yAxisId`) instead of `yAxisId`; the WO explicitly asked to verify this passthrough against real MUI behaviour rather than assume, and the first pass didn't — the dual-axis feature would have silently plotted every series on the primary axis in production despite all mocked tests passing (a mocked-prop test structurally cannot catch a wrong-field-name bug, since it only checks whatever the code itself sets). Fixed (`yAxisKey` → `yAxisId`, verified against `node_modules` source directly, not just the mock) and the pre-existing `tests/BarChart.test.jsx` "dual axis" case corrected too (R2 — it used the same wrong field name and was cited by the WO as precedent, giving false confidence). **`ui_reviewer` found U1 (P1, real bug)** — `tickLabelInterval`'s `index % step === 0` systematically dropped the LAST x-axis bucket unless it landed exactly on a step boundary (e.g. 24 labels/step 3 → indices 0..21, never 23), exactly the failure mode the WO's own SCOPE C explicitly warned to check for; fixed by force-including `index === lastIndex`, with a new `it.each([24,12,7,1])` regression test, confirmed non-vacuous by reverting the fix. **U2 (P2)**: `DESIGN.md` #8's "Never dual-axis" now contradicted the shipped code — updated to record dual-axis as a scoped, opt-in exception (CHART-5) rather than a general rule change. U3 (P3, fixed `MAX_X_TICKS` under-displays low-density ranges e.g. 12 monthly buckets showing only 6) and U4 (P3, no UI indication of which axis a toggle's series plots against) both accepted, not fixed — informational, no code change. Re-verified after fixes: 31/31 scoped vitest tests (`TimeSeriesChart.test.jsx` + `BarChart.test.jsx`) green, `tsc` build clean. Published: ucm 2.29.0 (minor — new optional per-series field + prop, additive, no breaking change). **Third bug found post-publish (2.29.1 patch)**: after jg-ferien adopted it (ACT-7) the operator reported live that the secondary axis was "obviously there but invisible" and x-axis labels were still missing. Investigated by adding a new DX-1 harness entry (`dev/entries.jsx` `TimeSeriesChartEntry`, previously missing — this component had NEVER been visually verified in a real browser through CHART-2/3/4/5) and reading the real rendered DOM directly. X-axis confirmed working (9 evenly-spread labels for 24 buckets) — the live report was very likely a stale PWA service-worker cache on jg-ferien's side, not a code defect. Y-axis confirmed broken: only 1 of 2 `yAxis` entries rendered in the DOM. Root cause traced to the installed MUI X-Charts source (`defaultizeAxis.js`): `defaultPosition = index === 0 ? 'left' : 'none'` — every yAxis array entry past the first silently renders nothing unless given an explicit `position`, a class of bug no mocked-`BarChart` test can catch (the mock never runs MUI's own defaulting logic). Fixed by setting `position: 'left'`/`'right'` explicitly on the primary/secondary entries; independent `reviewer` traced the fix against the same MUI source and confirmed no second gating layer exists. Live-reverified in the dev harness after the fix: both axes render with independent scales (primary `0,1,2`; secondary `0.0,0.5,1.0,1.5,2.0`). 32/32 tests green, build clean. Published: ucm 2.29.1 (patch). **Fourth report, root cause NOT in ucm**: operator reported x-axis labels still empty even after 2.29.1. Extensive live investigation (scoped DOM queries, computed-style checks, a real-browser-DOM comparison against this repo's own dev harness in both dev and production-build mode, a DM-Sans-not-loaded font test) could not reproduce it anywhere in ucm with the exact real bucket data — ruled out: stale cache, MUI axis config, container width, dev-vs-prod build, font-loading timing. Root cause found via web search: a known, unfixed upstream MUI X-Charts bug (mui/mui-x#18768, duplicate of #18399) where insufficient measured space makes its `ellipsize` logic collapse an ENTIRE tick label to `''` instead of shortening it, confirmed live via a `<tspan>` with correct styling but zero text content — triggered by jg-ferien's verbose label format ("08/05, 10:00 AM", 16 chars) combined with its actual DM Sans font metrics (not reproducible with this repo's default Roboto-based harness). Fixed in jg-ferien (ACT-9: short, granularity-aware labels), not in ucm — `TimeSeriesChart` itself has no defect here, it's a host-side label-length choice interacting with an upstream MUI bug. This repo's DX-1 harness entry (`TimeSeriesChartEntry`) updated to model the correct short-label pattern so it doesn't teach the risky verbose format. |
|
|
27
27
|
| CHART-4 | Fix `TimeSeriesChart` chart canvas rendering at zero height | Neither `ChartFrame` nor `BarChart` ever receive a height from `TimeSeriesChart` — MUI X-Charts' responsive container measures a zero-height parent and draws no bars/axes, while the legend (sized by its own content) renders regardless, which is why the live symptom was "legend shows, chart canvas doesn't". Fix supplies a preset default height (`CHART_HEIGHT = 320`, `minHeight` on `ChartFrame` + `height` forwarded to the real `MuiBarChart`), matching the working `dev/entries.jsx` reference pattern. Full spec: [work-orders/CHART-4.md](work-orders/CHART-4.md). | 2026-08-07 | done | f0953a1, fcb289b | Second, independent bug found live in jg-ferien immediately after CHART-3 + the ucm pin bump landed — CHART-3's own fix is what let this one become visible for the first time (it was previously masked by the permanently-empty-chart bug). Root cause of it shipping twice undetected: every test in `tests/TimeSeriesChart.test.jsx`/`tests/BarChart.test.jsx` mocks the real `@mui/x-charts/BarChart`, so no test has ever mounted the real chart and observed its size. **Codex out of credits, known up front** — implemented directly (author-flip, independent review mandatory). **Test-writing pitfall found and self-corrected during implementation**: first wrote an *unmocked* render test asserting the real chart SVG's `viewBox` height was non-zero — verified vacuous (stays green even fully reverting the fix, since jsdom's `ResizeObserver`/layout handling doesn't reproduce the real zero-height-parent browser behavior this bug depends on). Deleted; replaced with a `chartSpy`-mocked prop assertion (`props.height` non-zero) matching the file's existing convention, confirmed non-vacuous (fails pre-fix with `props.height` undefined). Independent `reviewer` + `ui_reviewer` ran concurrently: `reviewer` verified the fix mechanism end-to-end (no prop-name collision between `BarChart`'s own `minHeight`/`aspect` and the forwarded `height`) and the test's non-vacuity claim, no findings. `ui_reviewer`: two P3 informational notes, neither blocking — (U1) the fixed 320px height is a deliberate, documented exception to `DESIGN.md` #14 ("responsive = container-sized, not fixed px"), needed specifically because MUI X-Charts' own `ResizeObserver`-based height measurement is what caused CHART-4 in the first place; a code comment was added warning future readers not to "fix" this back to a responsive height without re-verifying the underlying MUI bug is resolved. (U2) `CHART_HEIGHT` uses 320 for both `ChartFrame.minHeight` and `BarChart.height`, vs. `dev/entries.jsx`'s 360/320 split — functionally harmless (WO explicitly permits one fixed value), no change made. 13/13 scoped vitest tests green, `tsc` build clean. Published: ucm 2.28.2 (patch, no interface change). No app pin bumps in this WO — jg-ferien picks it up on its next ucm bump. |
|
|
28
28
|
| CHART-3 | Fix `TimeSeriesChart` rendering nothing on async data load | `visibleKeys` was a lazy `useState` initialized once from `data` at first mount; any host that fetches asynchronously after mount (the normal case, e.g. jg-ferien's `ActivitySection`) ends up with a permanently-empty `visibleKeys`, so `isDataEmpty` stays `true` forever and the chart body never renders even once real data arrives. Fix syncs newly-seen series keys to visible-by-default while preserving a user's manual toggle-off across data updates. Full spec: [work-orders/CHART-3.md](work-orders/CHART-3.md). | 2026-08-06 | done | 9a1bd9d, 48cefa8 | Found live in jg-ferien's Aktivität screen (ACT-3 consumer) — toolbar rendered, chart body stayed on `ChartFrame`'s empty state permanently. **Codex was out of workspace credits** (genuine quota error on invocation, no partial work) — orchestrator implemented directly per the Codex→Claude fallback rule; independence flip applies (author = orchestrator, so independent review was mandatory, not self-check). Independent `reviewer` + `ui_reviewer` both ran (background, concurrent): `ui_reviewer` found no findings; `reviewer` found R1 (P2, real bug — the seen-keys tracker was a `useRef` mutated during the render body alongside a paired `setVisibleKeys` call; under React concurrent rendering an interrupted/discarded render leaves the ref mutation applied while the paired state update is discarded, permanently hiding a key without it ever reaching `visibleKeys` — a different-shaped recurrence of the exact bug this WO exists to fix) and R2 (P3, test-file duplication — a second near-duplicate render helper). Both fixed: seen-keys tracking moved from a ref to a second `useState` (both states updated together in the render body, so a discarded render rolls both back together — React's documented pattern for this), and the duplicate test helper collapsed into the existing `renderChart`'s `rerenderWith`. Re-verified: 12/12 scoped vitest tests green (3 new CHART-3 regression tests, 2 of which confirmed non-vacuous by failing against the pre-fix code), `tsc` build clean. Published: ucm 2.28.1 (patch, no interface change). No app pin bumps in this WO — jg-ferien picks it up on its next ucm bump. |
|
|
29
29
|
| ONB-1 | Per-app configurable notifications onboarding step | New `browserPush` prop `{nagUntil, showOnce}` on `OnboardingProvider`; parameterizes the `browser_push` descriptor's condition (default changes from implicit "all-channels" to "any-channel", stopping the over-nag); `showOnce` via a persisted `onboarding_seen` set (frozen-at-mount ref, no mid-session flicker) | 2026-07-17 | done | 1ae7c20 | Default behavior change affects cockpit and all consumers on their next ucm bump. jg-ferien companion WO pins the new version with `browserPush={{nagUntil: 'any-channel'}}` explicit (matches new default, but pinned explicitly per the WO). |
|
|
@@ -50,5 +50,6 @@ prefixed ID; never reuse a bare flat number across workstreams.
|
|
|
50
50
|
| SHELL-1 | A shared user menu: avatar, profile, logout | Measured 2026-08-04: **seven render sites across two apps** for two adjacent actions — spesix has an account link (`:184`) plus a separate logout (`:224`), and the same pair again at `:269`/`:307` for mobile; survey_app has account nav (`:67`), logout desktop (`:152`) and logout mobile (`:90`); jg-ferien has an avatar linking to `/account` and **no header logout at all** (it sits at the bottom of `/more`). The duplication is worse *within* each app than between them. ucm exports nothing header-shaped today and has no avatar component, though it has owned `logout` in `auth/AuthContext.jsx:132` all along. | 2026-08-04 | done | 8ef8cbf (WO), bf4cdd1 (impl) | Envelope: [work-orders/SHELL-1.md](work-orders/SHELL-1.md). New `SHELL-*` prefix for shared application-shell surfaces, distinct from `MSG-*`/`NOTIF-*`. **Design settled with the operator against an interactive prototype — the boundary is not to be renegotiated:** ucm owns the avatar, the menu, Profil (`/account`, verified identical in all four apps) and Abmelden; the host passes only account-related entries, already permission-filtered and already translated; **the context/structure picker never enters the menu.** That last point is measured, not assumed — spesix's `activeSchoolId` is consumed by **13 files** (Approvals/Budgets/Claims/AppLayout) and jg's `selectedEventId` by **18**; it re-scopes the entire application's data, so it stays a permanently visible control of its own. Operator 2026-08-04: org/structure pickers are app-specific and already solved where they live; a second route to them here would be the very duplication this WO removes. **Expected integration for spesix and survey_app is `<UserMenu resolveLink={navigate} />` and nothing else** — only jg passes an entry. Two traps carried over from elsewhere in this estate: an earlier prototype had Benachrichtigungen as a ucm built-in, but **only jg** embeds `NotificationSettings` and `?tab=notifications` exists only there, so three apps would have got a menu entry pointing at a route they lack — verify every built-in against every app; and **labels arrive as finished strings, never i18n keys**, which is exactly why `django-core-micha` MSG-13 is currently broken. Stated cost: **Profil goes from one tap to two in every app** (paid consciously — the price of the consolidation). hram is **unverified** (no header file under `components/`) — do not claim four apps benefit until someone looks. Scope E (dcm MSG-13 preview `Switch`) had already landed separately as NOTIF-14 (`72e6697`) before this WO started, so A-D ship alongside it in the same release. Host header rewires (spesix/survey_app/jg/hram) are four separate small WOs, one per app, including removing jg's now-redundant local `UserAvatar` — not done here. **Codex → Claude fallback**: Codex exited immediately on "workspace out of credits" (same failure as NOTIF-14) before writing any code, so this session implemented directly — author-flip rule applied, independent `reviewer` **and** `ui_reviewer` both ran before commit (WO's TIER line makes both mandatory). **Decisions stated per WO's explicit requirement** (both reviewers flagged these as initially undocumented, now recorded here): scope D icons — **no entry carries a leading or trailing icon** (uniform with none, avoids an unstated icon contract for host items); Profil/Abmelden i18n — **new `UserMenu.PROFILE`/`UserMenu.LOGOUT` keys** in a fresh `src/i18n/userMenuTranslations.ts`, not a reuse of `Account.TAB_PROFILE` (different UI surface, keeps the component independently consumable). New component `src/auth/UserMenu.jsx` mirrors `NotificationBell({ resolveLink })`'s exact anchor/Menu/onClose shape — no custom keyboard/focus-trap code, relies entirely on MUI `Menu`'s native Escape/click-outside/focus-return. Gate: `tests/UserMenu.test.jsx` (new, 6/6 passed, all WO-required cases incl. non-vacuous ordering + logout-stub assertions) + affected-areas set `tests/AuthContext.test.jsx` + `tests/NotificationSettings.test.jsx` (untouched by this diff, included per WO's test-scope note since scope E is in the same release) → 18/18 passed, 0 regressions; `tsc -p tsconfig.build.json --noEmit` clean. Both reviewers' only findings were the now-resolved documentation gaps above — no code-correctness findings from either. **HOTFIX 2026-08-04 (`b5b6abd`, published 2.26.1)**: operator screenshot feedback — two-letter avatar initials looked cramped in the 32px circle (MUI's default `Avatar` font-size doesn't auto-scale for a smaller width/height override). Added `fontSize: '0.8rem'`. Independent `reviewer`: no findings. **HOTFIX 2026-08-04 (`769f55d`, published 2.26.2)**: operator screenshot from hram — identity block showed the same email twice (display name falls back to `username` when no first/last name; on allauth email-login hosts `username === email`). The email line now only renders when it differs from the shown display name. Independent `reviewer`: no findings. |
|
|
51
51
|
| CHART-2 | Time-series chart preset with range picker and series toggles | Extends CHART-1, whose `ChartFrame` already ships a **toolbar slot that no consumer uses yet**. Operator request 2026-08-04 from jg's activity chart, generalised: every app charting a time series wants a duration picker and per-series toggles, and `django-core-micha` ACT-1 will make activity data available to all of them. jg's `ActivitySection.jsx:9` currently imports `@mui/x-charts` directly and hand-rolls its own Paper/loading/error/empty. | 2026-08-04 | done | a277c82, abf11b0 | Envelope: [work-orders/CHART-2.md](work-orders/CHART-2.md). **Presentational only — the host fetches**; the component takes data plus callbacks and calls no endpoint. **The half most likely to be dropped: the range picker must emit a GRANULARITY alongside the window** — new `TimeSeriesChart` component emits `onRangeChange(rangeKey, granularity)` per the fixed table (1d→hour, 1w→4hour, 1m→day, 1y→month), test 1 asserts the exact emitted payload, non-vacuity confirmed by manually removing the granularity arg and observing the test fail. **Series toggles implemented as `Checkbox`+`FormControlLabel`** (this repo's established multi-choice pattern, matching `PollCard.jsx`); range picker as `RadioGroup`/`Radio`/`FormControlLabel` (same repo's deliberate single-choice convention, chosen over `ToggleButtonGroup` for which this repo has zero precedent). **Second y-axis: explicitly NOT added** — series toggles are the chosen fix (stated in the component's own JSDoc), not silently absent. **Color/identity stability**: each series' color is keyed to its original (unfiltered) index, not its position after toggling others off — verified independently by `reviewer`, prevents a toggle from shifting a *different* series' color. **Codex → Claude fallback**: Codex exited immediately on "workspace out of credits" before writing any code, so this session implemented directly — author-flip rule applied, independent `reviewer` **and** `ui_reviewer` both ran before commit (mandatory per WO tier), **both clean, no findings**. `ui_reviewer` confirmed accessible labels (series name text, not color alone), native MUI keyboard nav untouched, all 4 languages present for new i18n keys, host-supplied series labels never passed through `t()`. Gate: 9/9 new `TimeSeriesChart` tests + 25/25 across the full chart-component test set (`ChartFrame`/`BarChart`/`LineChart`/`chartsPalette`/`TimeSeriesChart`), `tsc --noEmit` clean. `ChartFrame`/`BarChart`/`LineChart`/`palette.js` confirmed untouched. |
|
|
52
52
|
| MSG-6j | Poll option label collides with its vote-count/percent text at zero gap | Operator screenshots from jg-ferien production: `PollCard.jsx` renders each option's label and result text in a `Stack justifyContent="space-between"` with no `spacing`, inside a `MessageBubble` that is `width: fit-content` — since the bubble's own width is derived from this exact content, there is never leftover space for space-between to distribute, so label and vote-count/percent text end up flush against each other. Percentage math itself confirmed correct (operator's second screenshot showed the right numbers once both votes had landed) -- layout only. | 2026-08-04 | done | e47e243, c480bce | Envelope: [work-orders/MSG-6j.md](work-orders/MSG-6j.md). Two additive fixes: `spacing={1}` on the label/result row so space-between can never collapse to zero gap; `minWidth: 260` on the poll's `Paper`, reconciled against `MessageBubble`'s `maxWidth: min(75%, 680px)` ceiling at a 375px viewport (75% = 281px there). **Codex → Claude fallback**: Codex exited immediately on "workspace out of credits" (twice) before writing any code, so this session implemented directly — author-flip rule applied, independent `reviewer` ran before commit. **One finding addressed before commit**: a speculative `noWrap` added to the result-text `Typography` (not one of the WO's two prescribed fixes) risked silently ellipsizing the vote-count/percent text on a narrow squeeze instead of wrapping — worse than the reported bug for a sighted user, since the count would become invisible rather than merely tight. Reverted. **Disclosed, not fixed (P3, WO only required 375px verification)**: `minWidth: 260` can still exceed `MessageBubble`'s ceiling below ~347px viewport width (e.g. a 320px device) — a residual squeeze risk on narrower-than-tested devices. Live browser verification at 375px was not performed this round (port conflict with a concurrent session's dev server, unresolvable from this session) — relied on `getComputedStyle`-based regression tests instead, non-vacuity proven by temporarily removing the `spacing` fix and confirming the test fails. Gate: `tests/messagingPollRendering.test.jsx` 7/7 (2 new); full suite 278/279 (the one failure, `NotificationsProvider.test.jsx`, is an unrelated WebSocket-mock timing flake under full-suite load, confirmed passing 9/9 in isolation); `tsc -p tsconfig.build.json --noEmit` clean. Published as npm 2.26.3 (confirmed via `gh run list` on the publish workflow). **jg-ferien's ucm pin bump to 2.26.3 is a separate, not-yet-done step.** |
|
|
53
|
-
| AUTH-1 | Password forms: state the rules (helperText + inline validator) | Spesix field report 2026-08-06: the invitation set-password screen rejects first-time users without ever stating the rules. Add `helperText` (default `Auth.PASSWORD_RULES_HINT`, optional `passwordRulesHint` prop override) on the new-password field of **PasswordSetForm + PasswordChangeForm** (NOT `PasswordResetRequestForm` — email-only, report listed it by mistake) + an inline validator for the two cheap/faithful rules (min-8, not-all-numeric) via the existing `localErrorKey` path (signal before the backend rejects). Baseline = dcm's 4 default validators. ucm-only, **zero per-app**. Full spec: [work-orders/AUTH-1.md](work-orders/AUTH-1.md). | 2026-08-06 | done | 84387cf, 103a252 | Tier 2 (shared-core). **Codex → Claude fallback**: re-confirmed out of workspace credits three times on 2026-08-06 alone (see `project_codex_quota_status` memory) before this WO started, so implemented directly — author-flip rule applied, independent `reviewer` **and** `ui_reviewer` both ran before commit. `PasswordChangeForm.jsx` had no `localErrorKey` mechanism at all before this WO (only `PasswordSetForm.jsx` did) — added it from scratch, matching the existing pattern exactly (same `<Box sx={{color:'error.main'}}>`, same check ordering). **`ui_reviewer`: no findings** (helperText/error-box conventions match, translation coverage 316/316 keys × 4 locales, hint text checked against dcm's actual `AUTH_PASSWORD_VALIDATORS`, no missing DX-1 harness entry since none existed before either). **`reviewer`: one P2 fixed before commit** — the inline min-8/not-all-numeric rule was hard-coded with no escape hatch; since `AUTH_PASSWORD_VALIDATORS` is a per-app Django setting this shared-core repo doesn't control, an app running a looser backend policy would have valid passwords rejected client-side on its next ucm pin-bump. Fixed by adding optional `minPasswordLength` (default 8) / `allowNumericPassword` (default false) props to both forms, with regression tests; all other consumers keep today's behaviour unchanged (identical defaults). Two P3s from `reviewer` disclosed, not fixed (both pre-existing/out-of-scope): the WO's own rationale text overstated that `PasswordChangeForm` already had the local-error pattern (it didn't; corrected here); neither form clears its local error on the next keystroke after a rejection (pre-existing `PasswordSetForm` behaviour, now also present in `PasswordChangeForm`). Gate: 32/32 new tests across 4 files; full suite 322/322, `tsc` clean. Published
|
|
54
|
-
| AUTH-2 | Translate backend auth error codes (stop leaking raw codes) | ucm surfaces raw backend/allauth error **codes**, not text: `apiClient.jsx:107` extracts `err.code`, components do `t(err.code)`/`setErrorKey(err.code)`, but the catalogue has no entries → users see literal `email_password_mismatch` / `password_too_short`. Affects **all** rendering auth screens (login/change-pw/MFA/profile). Fix = add de/fr/en i18n entries **keyed by the enumerated codes** → the existing `t(err.code)` resolves, **zero component/app change**. Does NOT reach the delegating invitation `PasswordSetForm` (that's AUTH-1). Full spec: [work-orders/AUTH-2.md](work-orders/AUTH-2.md). | 2026-08-06 | done | 84387cf, 103a252 | Tier 2 (shared-core). Same Codex→Claude fallback and reviewer pairing as AUTH-1 (one combined diff, both WOs landed together). **Keying decided: raw-code (drop-in)**, per the WO's own stated preference — no operator request for the namespaced `i18nKey` alternative. **Codes enumerated by reading the installed allauth/Django source directly** (not guessed): `email_password_mismatch`/`username_password_mismatch`/`phone_password_mismatch` (`allauth/headless/account/inputs.py` `LoginInput.clean`), `too_many_login_attempts`/`invalid_login`/`account_inactive`/`enter_current_password` (`allauth/account/adapter.py` error_messages + `allauth/headless/account/inputs.py` `ChangePasswordInput.clean_current_password` — confirmed this genuinely means "wrong current password", not merely "field left blank"), `incorrect_code` (`allauth/mfa/**`), `password_too_short`/`password_too_common`/`password_entirely_numeric`/`password_too_similar` (Django `contrib/auth/password_validation.py`, matching dcm's `AUTH_PASSWORD_VALIDATORS` baseline). Added de/fr/en/**sw** (file's established 4-locale convention, honoured over the WO text's literal "de/fr/en"). **`ui_reviewer` + `reviewer`: no AUTH-2-specific findings** (both spot-checked the code spellings independently against the same installed packages; no key collisions with existing `Auth.*` keys; `authErrorCodeRendering.test.jsx` proves a mapped code renders human text via a real i18next instance and a genuinely-uncoded error still falls back to the existing generic `Auth.PASSWORD_CHANGE_FAILED`). Gate: shared with AUTH-1 — full suite 322/322, `tsc` clean. Published
|
|
53
|
+
| AUTH-1 | Password forms: state the rules (helperText + inline validator) | Spesix field report 2026-08-06: the invitation set-password screen rejects first-time users without ever stating the rules. Add `helperText` (default `Auth.PASSWORD_RULES_HINT`, optional `passwordRulesHint` prop override) on the new-password field of **PasswordSetForm + PasswordChangeForm** (NOT `PasswordResetRequestForm` — email-only, report listed it by mistake) + an inline validator for the two cheap/faithful rules (min-8, not-all-numeric) via the existing `localErrorKey` path (signal before the backend rejects). Baseline = dcm's 4 default validators. ucm-only, **zero per-app**. Full spec: [work-orders/AUTH-1.md](work-orders/AUTH-1.md). | 2026-08-06 | done | 84387cf, 103a252 | Tier 2 (shared-core). **Codex → Claude fallback**: re-confirmed out of workspace credits three times on 2026-08-06 alone (see `project_codex_quota_status` memory) before this WO started, so implemented directly — author-flip rule applied, independent `reviewer` **and** `ui_reviewer` both ran before commit. `PasswordChangeForm.jsx` had no `localErrorKey` mechanism at all before this WO (only `PasswordSetForm.jsx` did) — added it from scratch, matching the existing pattern exactly (same `<Box sx={{color:'error.main'}}>`, same check ordering). **`ui_reviewer`: no findings** (helperText/error-box conventions match, translation coverage 316/316 keys × 4 locales, hint text checked against dcm's actual `AUTH_PASSWORD_VALIDATORS`, no missing DX-1 harness entry since none existed before either). **`reviewer`: one P2 fixed before commit** — the inline min-8/not-all-numeric rule was hard-coded with no escape hatch; since `AUTH_PASSWORD_VALIDATORS` is a per-app Django setting this shared-core repo doesn't control, an app running a looser backend policy would have valid passwords rejected client-side on its next ucm pin-bump. Fixed by adding optional `minPasswordLength` (default 8) / `allowNumericPassword` (default false) props to both forms, with regression tests; all other consumers keep today's behaviour unchanged (identical defaults). Two P3s from `reviewer` disclosed, not fixed (both pre-existing/out-of-scope): the WO's own rationale text overstated that `PasswordChangeForm` already had the local-error pattern (it didn't; corrected here); neither form clears its local error on the next keystroke after a rejection (pre-existing `PasswordSetForm` behaviour, now also present in `PasswordChangeForm`). Gate: 32/32 new tests across 4 files; full suite 322/322, `tsc` clean. **Published version correction**: the shared org CI runner had a multi-hour backlog on 2026-08-06 (queued jobs "not acquired by Runner"; see `reference_netcup_ci_runner` memory) — the 2.28.0 tag itself was never actually published on npm (confirmed via `npm view … @2.28.0` → 404), because by the time a runner slot freed up, three more sessions had pushed CHART-3/4/5 on top and the workflow published from that later HEAD instead. AUTH-1/AUTH-2 code is confirmed present (verified by downloading and grepping the actual npm tarball) starting at the first version that did publish, **npm 2.28.1**, and in every version since (currently 2.29.1). Consumers adopt on their own pin-bump (Spesix pinned at 2.5.0 = separate later bump, unaffected today). Companion: AUTH-2. |
|
|
54
|
+
| AUTH-2 | Translate backend auth error codes (stop leaking raw codes) | ucm surfaces raw backend/allauth error **codes**, not text: `apiClient.jsx:107` extracts `err.code`, components do `t(err.code)`/`setErrorKey(err.code)`, but the catalogue has no entries → users see literal `email_password_mismatch` / `password_too_short`. Affects **all** rendering auth screens (login/change-pw/MFA/profile). Fix = add de/fr/en i18n entries **keyed by the enumerated codes** → the existing `t(err.code)` resolves, **zero component/app change**. Does NOT reach the delegating invitation `PasswordSetForm` (that's AUTH-1). Full spec: [work-orders/AUTH-2.md](work-orders/AUTH-2.md). | 2026-08-06 | done | 84387cf, 103a252 | Tier 2 (shared-core). Same Codex→Claude fallback and reviewer pairing as AUTH-1 (one combined diff, both WOs landed together). **Keying decided: raw-code (drop-in)**, per the WO's own stated preference — no operator request for the namespaced `i18nKey` alternative. **Codes enumerated by reading the installed allauth/Django source directly** (not guessed): `email_password_mismatch`/`username_password_mismatch`/`phone_password_mismatch` (`allauth/headless/account/inputs.py` `LoginInput.clean`), `too_many_login_attempts`/`invalid_login`/`account_inactive`/`enter_current_password` (`allauth/account/adapter.py` error_messages + `allauth/headless/account/inputs.py` `ChangePasswordInput.clean_current_password` — confirmed this genuinely means "wrong current password", not merely "field left blank"), `incorrect_code` (`allauth/mfa/**`), `password_too_short`/`password_too_common`/`password_entirely_numeric`/`password_too_similar` (Django `contrib/auth/password_validation.py`, matching dcm's `AUTH_PASSWORD_VALIDATORS` baseline). Added de/fr/en/**sw** (file's established 4-locale convention, honoured over the WO text's literal "de/fr/en"). **`ui_reviewer` + `reviewer`: no AUTH-2-specific findings** (both spot-checked the code spellings independently against the same installed packages; no key collisions with existing `Auth.*` keys; `authErrorCodeRendering.test.jsx` proves a mapped code renders human text via a real i18next instance and a genuinely-uncoded error still falls back to the existing generic `Auth.PASSWORD_CHANGE_FAILED`). Gate: shared with AUTH-1 — full suite 322/322, `tsc` clean. **Published version correction**: see AUTH-1's row — the 2.28.0 tag was skipped by a CI runner backlog and never published; verified present in the npm tarball from **2.28.1** onward (currently 2.29.1). Companion: AUTH-1. |
|
|
55
|
+
| AUTH-3 | SignUpPage showed the access-code-specific success message for every signup mode | Operator-observed live bug 2026-08-07: open self-registration (no access code) showed "Falls der Zugangscode für dieses Projekt gültig ist, wurde ein Einladungslink an X gesendet" — nonsensical when there is no code at all. Root cause: `SignUpPage.jsx`'s `handleSubmit` hardcoded `Auth.INVITE_REQUEST_SUCCESS` for every mode (open/access-code/email-domain/QR) regardless of the backend's actual response. | 2026-08-07 | done | 599e73d, b4e1fe7 | No WO file — operator-reported bug, fixed directly, same convention as MSG-6c/MSG-6d/MSG-6j. Tier 2 (shared-core). **Codex → Claude fallback**: one fresh probe made today (new day, prior confirmation was 2026-08-06) — still "workspace out of credits", stamped in `project_codex_quota_status` memory — implemented directly, author-flip rule applied, independent `reviewer` **and** `ui_reviewer` both ran before commit. **Root cause confirmed against the actual backend** (`django-core-micha` `register_request` view + `validate_access_code_or_error`): every signup mode's success response already carries a mode-agnostic, accurate `code: "Auth.INVITE_SENT"`; an invalid access code is never silently accepted, it raises a distinct 400 (`Auth.ACCESS_CODE_INVALID_OR_INACTIVE`) — so the old key's "if valid…" ambiguity never matched backend reality for any mode, not just the ones reported. Fix: `setSuccessKey(res?.code || 'Auth.INVITE_SENT')`, using the backend's own code; no component/app change beyond this. **Both reviewers independently flagged the same P3**: switching away from the old key silently dropped the `{{email}}` confirmation (`Auth.INVITE_SENT` had none). Fixed before commit — extended `Auth.INVITE_SENT`'s text to include `{{email}}` in all 4 locales (safe: it had exactly one consumer, this call site, confirmed by grep). `Auth.INVITE_REQUEST_SUCCESS` is now dead/orphaned (both reviewers flagged as P3, tracked here, not removed — out of this fix's scope). Gate: `tests/SignUpPage.test.jsx` 13/13 (3 new, proving the backend code wins over the old hardcoded key, and the no-code fallback); full suite 342/342, `tsc` clean. |
|
package/dev/entries.jsx
CHANGED
|
@@ -42,16 +42,35 @@ function BarChartEntry() {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function TimeSeriesChartEntry() {
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
const buckets =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
// Real API response from the live jg-ferien Aktivität bug (1-week range,
|
|
46
|
+
// 4h granularity), pasted verbatim by the operator -- reproducing the
|
|
47
|
+
// exact bucket_start/distinct_users/presence_hours combination.
|
|
48
|
+
const buckets = [
|
|
49
|
+
{ bucket_start: '2026-08-05T08:00:00+00:00', distinct_users: 1, presence_hours: 0.0 },
|
|
50
|
+
{ bucket_start: '2026-08-05T12:00:00+00:00', distinct_users: 1, presence_hours: 0.0 },
|
|
51
|
+
{ bucket_start: '2026-08-05T16:00:00+00:00', distinct_users: 2, presence_hours: 0.17 },
|
|
52
|
+
{ bucket_start: '2026-08-05T20:00:00+00:00', distinct_users: 2, presence_hours: 0.09 },
|
|
53
|
+
{ bucket_start: '2026-08-06T04:00:00+00:00', distinct_users: 1, presence_hours: 0.05 },
|
|
54
|
+
{ bucket_start: '2026-08-06T08:00:00+00:00', distinct_users: 1, presence_hours: 0.05 },
|
|
55
|
+
{ bucket_start: '2026-08-06T12:00:00+00:00', distinct_users: 2, presence_hours: 0.01 },
|
|
56
|
+
{ bucket_start: '2026-08-06T20:00:00+00:00', distinct_users: 1, presence_hours: 0.01 },
|
|
57
|
+
{ bucket_start: '2026-08-07T00:00:00+00:00', distinct_users: 1, presence_hours: 0.01 },
|
|
58
|
+
{ bucket_start: '2026-08-07T04:00:00+00:00', distinct_users: 1, presence_hours: 0.03 },
|
|
59
|
+
{ bucket_start: '2026-08-07T08:00:00+00:00', distinct_users: 1, presence_hours: 0.03 },
|
|
60
|
+
];
|
|
61
|
+
// Short, time-only labels: a verbose format (e.g. "08/05, 10:00 AM") can
|
|
62
|
+
// trip a known, unfixed MUI X-Charts bug where insufficient measured
|
|
63
|
+
// space collapses the ENTIRE tick label to an empty string instead of a
|
|
64
|
+
// shortened one (mui/mui-x#18768, duplicate of #18399) -- confirmed live
|
|
65
|
+
// in jg-ferien with its real DM Sans font metrics. Keep consumer labels
|
|
66
|
+
// short regardless of range/granularity.
|
|
67
|
+
function formatBucketLabel(value) {
|
|
68
|
+
const date = new Date(value);
|
|
69
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
70
|
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
71
|
+
}
|
|
53
72
|
const data = {
|
|
54
|
-
xLabels: buckets.map((b) =>
|
|
73
|
+
xLabels: buckets.map((b) => formatBucketLabel(b.bucket_start)),
|
|
55
74
|
series: [
|
|
56
75
|
{ key: 'distinct_users', label: 'Eindeutige Nutzer', data: buckets.map((b) => b.distinct_users) },
|
|
57
76
|
{ key: 'presence_hours', label: 'Anwesenheitszeit (Std.)', data: buckets.map((b) => b.presence_hours), axis: 'secondary' },
|
|
@@ -64,7 +83,7 @@ function TimeSeriesChartEntry() {
|
|
|
64
83
|
yAxisLabel="Anzahl"
|
|
65
84
|
secondaryYAxisLabel="Stunden"
|
|
66
85
|
data={data}
|
|
67
|
-
defaultRange="
|
|
86
|
+
defaultRange="1w"
|
|
68
87
|
/>
|
|
69
88
|
);
|
|
70
89
|
}
|
|
@@ -91,10 +91,10 @@ export const authTranslations = {
|
|
|
91
91
|
"sw": "Msimbo wa ufikiaji umesasishwa."
|
|
92
92
|
},
|
|
93
93
|
"Auth.INVITE_SENT": {
|
|
94
|
-
"de": "Die Einladung wurde
|
|
95
|
-
"fr": "L'invitation a été envoyée.",
|
|
96
|
-
"en": "The invitation has been sent.",
|
|
97
|
-
"sw": "Mwaliko umetumwa."
|
|
94
|
+
"de": "Die Einladung wurde an {{email}} gesendet.",
|
|
95
|
+
"fr": "L'invitation a été envoyée à {{email}}.",
|
|
96
|
+
"en": "The invitation has been sent to {{email}}.",
|
|
97
|
+
"sw": "Mwaliko umetumwa kwa {{email}}."
|
|
98
98
|
},
|
|
99
99
|
"Auth.RESET_REQUEST_ACCEPTED": {
|
|
100
100
|
"de": "Wenn für diese Adresse ein Konto existiert, wurde eine E-Mail zum Zurücksetzen gesendet.",
|
package/dist/pages/SignUpPage.js
CHANGED
|
@@ -89,14 +89,18 @@ export function SignUpPage() {
|
|
|
89
89
|
return;
|
|
90
90
|
setSubmitting(true);
|
|
91
91
|
try {
|
|
92
|
-
await submitRegistrationRequest({
|
|
92
|
+
const res = await submitRegistrationRequest({
|
|
93
93
|
email,
|
|
94
94
|
mode,
|
|
95
95
|
accessCode,
|
|
96
96
|
registrationContextToken: mode === 'self_signup_qr' ? tokenFromUrl : null,
|
|
97
97
|
turnstileToken: turnstileRequired ? turnstileToken : undefined,
|
|
98
98
|
});
|
|
99
|
-
|
|
99
|
+
// The backend's own response code is already mode-agnostic and accurate
|
|
100
|
+
// (Auth.INVITE_SENT). Falling back to the access-code-specific "if valid,
|
|
101
|
+
// an invite was sent" wording here was wrong for open/email-domain/QR
|
|
102
|
+
// signup, which have no code whose validity needs hiding.
|
|
103
|
+
setSuccessKey((res === null || res === void 0 ? void 0 : res.code) || 'Auth.INVITE_SENT');
|
|
100
104
|
}
|
|
101
105
|
catch (err) {
|
|
102
106
|
setErrorKey(err.code || 'Auth.INVITE_FAILED');
|
package/package.json
CHANGED
|
@@ -100,10 +100,10 @@ export const authTranslations = {
|
|
|
100
100
|
},
|
|
101
101
|
|
|
102
102
|
"Auth.INVITE_SENT": {
|
|
103
|
-
"de": "Die Einladung wurde
|
|
104
|
-
"fr": "L'invitation a été envoyée.",
|
|
105
|
-
"en": "The invitation has been sent.",
|
|
106
|
-
"sw": "Mwaliko umetumwa."
|
|
103
|
+
"de": "Die Einladung wurde an {{email}} gesendet.",
|
|
104
|
+
"fr": "L'invitation a été envoyée à {{email}}.",
|
|
105
|
+
"en": "The invitation has been sent to {{email}}.",
|
|
106
|
+
"sw": "Mwaliko umetumwa kwa {{email}}."
|
|
107
107
|
},
|
|
108
108
|
|
|
109
109
|
"Auth.RESET_REQUEST_ACCEPTED": {
|
package/src/pages/SignUpPage.jsx
CHANGED
|
@@ -123,14 +123,18 @@ export function SignUpPage() {
|
|
|
123
123
|
|
|
124
124
|
setSubmitting(true);
|
|
125
125
|
try {
|
|
126
|
-
await submitRegistrationRequest({
|
|
126
|
+
const res = await submitRegistrationRequest({
|
|
127
127
|
email,
|
|
128
128
|
mode,
|
|
129
129
|
accessCode,
|
|
130
130
|
registrationContextToken: mode === 'self_signup_qr' ? tokenFromUrl : null,
|
|
131
131
|
turnstileToken: turnstileRequired ? turnstileToken : undefined,
|
|
132
132
|
});
|
|
133
|
-
|
|
133
|
+
// The backend's own response code is already mode-agnostic and accurate
|
|
134
|
+
// (Auth.INVITE_SENT). Falling back to the access-code-specific "if valid,
|
|
135
|
+
// an invite was sent" wording here was wrong for open/email-domain/QR
|
|
136
|
+
// signup, which have no code whose validity needs hiding.
|
|
137
|
+
setSuccessKey(res?.code || 'Auth.INVITE_SENT');
|
|
134
138
|
} catch (err) {
|
|
135
139
|
setErrorKey(err.code || 'Auth.INVITE_FAILED');
|
|
136
140
|
if (turnstileRequired) {
|
|
@@ -136,3 +136,49 @@ describe('SignUpPage Turnstile integration', () => {
|
|
|
136
136
|
expect(window.turnstile.render).not.toHaveBeenCalled();
|
|
137
137
|
});
|
|
138
138
|
});
|
|
139
|
+
|
|
140
|
+
describe('SignUpPage — success message reflects the backend response, not a hardcoded mode', () => {
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
vi.clearAllMocks();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
afterEach(() => {
|
|
146
|
+
cleanup();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
async function submit(mode) {
|
|
150
|
+
renderSignUp({ signupModes: [mode] });
|
|
151
|
+
fireEvent.change(screen.getByRole('textbox', { name: /Auth\.EMAIL_LABEL/ }), {
|
|
152
|
+
target: { value: 'ada@example.com' },
|
|
153
|
+
});
|
|
154
|
+
if (mode === 'self_signup_access_code') {
|
|
155
|
+
fireEvent.change(screen.getByRole('textbox', { name: /Auth\.ACCESS_CODE_LABEL/ }), {
|
|
156
|
+
target: { value: 'some-code' },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
fireEvent.click(screen.getByRole('button', { name: 'Auth.SIGNUP_SUBMIT' }));
|
|
160
|
+
await waitFor(() => expect(authApi.submitRegistrationRequest).toHaveBeenCalled());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
it('shows the backend-provided code for open self-signup, not the access-code-specific message', async () => {
|
|
164
|
+
authApi.submitRegistrationRequest.mockResolvedValue({ code: 'Auth.INVITE_SENT', email: 'ada@example.com', mode: 'self_signup_open' });
|
|
165
|
+
await submit('self_signup_open');
|
|
166
|
+
|
|
167
|
+
await waitFor(() => expect(screen.getByText('Auth.INVITE_SENT')).toBeTruthy());
|
|
168
|
+
expect(screen.queryByText('Auth.INVITE_REQUEST_SUCCESS')).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('shows the backend-provided code for access-code self-signup too', async () => {
|
|
172
|
+
authApi.submitRegistrationRequest.mockResolvedValue({ code: 'Auth.INVITE_SENT', email: 'ada@example.com', mode: 'self_signup_access_code' });
|
|
173
|
+
await submit('self_signup_access_code');
|
|
174
|
+
|
|
175
|
+
await waitFor(() => expect(screen.getByText('Auth.INVITE_SENT')).toBeTruthy());
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('falls back to Auth.INVITE_SENT if the backend response carries no code', async () => {
|
|
179
|
+
authApi.submitRegistrationRequest.mockResolvedValue({});
|
|
180
|
+
await submit('self_signup_open');
|
|
181
|
+
|
|
182
|
+
await waitFor(() => expect(screen.getByText('Auth.INVITE_SENT')).toBeTruthy());
|
|
183
|
+
});
|
|
184
|
+
});
|