Skip to content

feat(budget): add source attribution badges and per-source filter to cost breakdown overview#1355

Merged
steilerDev merged 6 commits intobetafrom
feat/1354-cost-breakdown-source-filter
Apr 25, 2026
Merged

feat(budget): add source attribution badges and per-source filter to cost breakdown overview#1355
steilerDev merged 6 commits intobetafrom
feat/1354-cost-breakdown-source-filter

Conversation

@steilerDev
Copy link
Copy Markdown
Owner

Summary

  • Source attribution badge on every cost-breakdown line item (color-coded, deterministic per source) — collapses to a dot on mobile.
  • Available Funds row reworked as an interactive multi-select filter chip strip showing per-source allocated cost and remaining balance.
  • URL-driven filter state (?sources=<id>,<id>); subtotals and grand totals recompute against the filtered set.
  • Backend response extended with per-line budgetSourceId and a per-response budgetSources aggregate.

Closes #1354

Test plan

  • Unit tests pass (95%+ coverage on new files)
  • Integration tests pass (/api/budget/breakdown carries new fields)
  • E2E tests pass (badges, multi-select filter, URL state, keyboard, responsive, dark mode)
  • Quality Gates green

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) noreply@anthropic.com

Frank Steiler and others added 6 commits April 25, 2026 10:28
…ges and per-source filter (#1354)

- New: client/src/lib/budgetSourceColors.test.ts — 15 tests for getSourceColorIndex (always [1,9], deterministic, never 0) and getSourceBadgeStyleKey (null→sourceUnassigned, consistency with colorIndex)
- New: client/src/components/BudgetSourceChip/BudgetSourceChip.test.tsx — 18 tests for rendering, 24-char truncation, aria-pressed, onToggle callback, disabled state, CSS custom properties (--chip-dot/bg/text), aria-label, keyboard interaction
- Extended: client/src/components/Badge/Badge.test.tsx — 2 tests for new title prop (forwards to span, absent when omitted)
- Extended: client/src/components/CostBreakdownTable/CostBreakdownTable.test.tsx — updated all prop usages (selectedSourceIds/onSourceToggle/onClearSources replacing budgetSources=[]), added budgetSourceId to fixtures, added 20+ tests for source badge rendering, chip strip, filter active state, clear button, empty state
- Extended: client/src/pages/BudgetOverviewPage/BudgetOverviewPage.test.tsx — added budgetSources:[] to breakdown fixtures
- Extended: server/src/services/budgetBreakdownService.test.ts — 3 helpers + 2 describe blocks (~20 tests) for budgetSourceId attribution on WI/HI lines and budgetSources aggregate (id/name/totalAmount, projectedMin/Max by confidence, multi-line accumulation, multi-source, empty cases)
- Extended: server/src/routes/budgetOverview.breakdown.test.ts — 2 helpers + 2 tests for budgetSources array in HTTP response and empty array when no sources assigned

Fixes #1354

Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>
…cost breakdown overview

- Display a deterministic-color source badge next to each line's confidence/quoted/invoiced badge in the cost breakdown table
- Mobile collapses the source badge to a color dot with full source name in aria-label/title
- Available Funds row redesigned as a multi-select chip toolbar showing per-source allocated cost and remaining balance
- Multi-select OR semantics filter the breakdown rows; subtotals and grand totals recompute against the filtered set
- URL state ?sources=<id>,<id> persists the filter across reloads
- Empty state when no lines match the active filter
- Keyboard: Tab through chips, Space/Enter toggles, Escape clears filter and refocuses the Available Funds expand button
- Backend: GET /api/budget/breakdown extended with budgetSourceId per line and a budgetSources aggregate map (id, name, totalAmount, projectedMin, projectedMax)
- Tokens: new --color-source-N-{bg,text,dot} family (10 slots, light + dark)
- New shared component: BudgetSourceChip
- New helper: client/src/lib/budgetSourceColors.ts (deterministic id->slot mapping)

Fixes #1354

Co-Authored-By: Claude backend-developer (Haiku 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude translator (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude ux-designer (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
… tests

Source names now appear in both the chip filter strip and the expanded
sub-rows. Replace getByText assertions with getAllByText length checks
to disambiguate.

Fixes #1354

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>
…lso gated by expand)

The Available Funds chip filter strip is rendered inside the
{availFundsExpanded && ...} block, so on collapse both the chip strip
and the sub-rows unmount. Replace getAllByText.toHaveLength(1) with
queryByText.not.toBeInTheDocument().

Fixes #1354

Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>
- CostBreakdownTable defensively coalesces breakdown.budgetSources to []
  so existing fixtures and forward-compatible payloads do not crash the
  cost breakdown rendering.
- E2E sourceDetailRow page-object now scopes to tr[class*="rowSourceDetail"]
  to avoid matching the chip-toolbar row that also contains the source name.
- Dark-mode badge color test uses toBeAttached() instead of toBeVisible()
  so it works on mobile where the badge label is CSS-hidden.

Fixes #1354

Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <noreply@anthropic.com>
…ions

- Source badge tests on Level 3 rows use toBeAttached() instead of
  toBeVisible() because mobile hides the badge label via CSS while keeping
  the element in the DOM for screen-reader access.
- Selected-source detail row class assertion uses toHaveClass() so it
  auto-retries through the chip-click → URL-state → React-render round-trip.

Fixes #1354

Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <noreply@anthropic.com>
Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[product-architect] Architectural review: APPROVE (posted as comment because the API does not allow self-approval on the orchestrator account).

Verified

  • Wiki API Contract update (GET /api/budget/breakdown) matches the server response: budgetSourceId per BreakdownBudgetLine, budgetSources aggregate array with id/name/totalAmount/projectedMin/projectedMax. Note explaining client-side filtering and per-source semantics is appropriate; no new ADR needed (the decision is small, additive, and documented inline).
  • No DB schema change — the source_id columns on work_item_budgets / household_item_budgets already exist; the service simply selects them.
  • Shared types (BreakdownBudgetLine.budgetSourceId, BudgetSourceSummaryBreakdown, BudgetBreakdown.budgetSources) match the wiki shape and are exported through shared/src/index.ts.
  • Component reuse: Badge is extended via the existing variants prop (no parallel implementation). BudgetSourceChip is built as a new shared component under client/src/components/ — complies with the "every new component must be a reusable shared component" rule.
  • Conventions: import type used consistently, ESM .js extensions preserved, snake_case in SQL, camelCase in TS, /api/ prefix unchanged.
  • Design tokens: 10 source slots (--color-source-0..9) defined for both light and dark themes in tokens.css. Chip/badge consume them via CSS custom properties — no hardcoded colors leak into components.
  • Performance: client filter is O(N) tree walks memoized on (selectedSourceIds, wiAreas, hiAreas, perspective). Backend per-source aggregation is a single pass over already-built wiEntityData/hiEntityData. No O(n²).
  • Test parity: every new/modified production file has a corresponding test file (budgetBreakdownService.test.ts, budgetOverview.breakdown.test.ts, BudgetSourceChip.test.tsx, budgetSourceColors.test.ts, plus expanded CostBreakdownTable.test.tsx).

Medium findings (not blocking — defer to dev-team-lead / refinement)

  1. server/src/services/budgetBreakdownService.ts declares wiBudgetLineSourceMap (lines 570–574) and hiBudgetLineSourceMap (lines 734–738) and populates them but never reads them. The per-source aggregation later iterates wiEntityData / hiEntityData directly via line.budgetSourceId. Remove the dead maps.
  2. client/src/lib/budgetSourceColors.ts: SourceBadgeStyleKey includes the literal source0, but getSourceBadgeStyleKey(null) returns 'sourceUnassigned', so source0 is unreachable through that function. The .source0 class still exists in Badge.module.css as a defensive fallback. Either narrow the type to 'source1' | … | 'source9' | 'sourceUnassigned' or drop .source0 from CSS — pick one.

Palette nit (informational)

  • Light-mode --color-source-7 reuses --color-blue-100 / --color-blue-800 (identical to source 1) — only the dot color differs. With 10 deterministic slots and a small house-build user base this is unlikely to bite, but if two simultaneously-used sources hash to slots 1 and 7 the chips will look near-identical. Worth a future palette pass.

Architecture compliant. No changes required from the architect role.

Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security-engineer]

No blocking findings. Approving this PR from a security perspective.

SQL parameterization — The two new column additions (wib.budget_source_id, hib.budget_source_id) use Drizzle's sql tagged template literals, consistent with the established safe query pattern throughout the service. The new budgetSourceRows aggregate query (SELECT id, name, total_amount FROM budget_sources ORDER BY name ASC) contains no user-controlled input and no interpolated values — clean.

Authorization scope — The GET /api/budget/breakdown endpoint is registered under the global preValidation session hook (auth.ts:84), which applies to all routes. The new budgetSources aggregate query reads from the single-tenant SQLite database with no cross-user data boundary to enforce — consistent with the existing breakdown queries. No elevation or bypass introduced.

Data leakagebudgetSourceId is a project-scoped UUID (same sensitivity as work item IDs already in the response). The BudgetSourceSummaryBreakdown shape exposed is { id, name, totalAmount, projectedMin, projectedMax } — no internal fields, no secrets.

Frontend XSS — Source name and chip label are rendered via React JSX text nodes throughout (no dangerouslySetInnerHTML, no innerHTML, no eval). The title prop added to Badge is forwarded as a standard HTML attribute — React escapes it automatically.

URL filter state?sources=2,5,8 query param is parsed as a numeric Set client-side; it drives selectedSourceIds used for local filtering only, never sent back to the server unvalidated. No SSRF or injection vector.

No findings.

@steilerDev
Copy link
Copy Markdown
Owner Author

[product-owner] Product Owner review: APPROVE (posted as comment because the API does not allow self-approval on the orchestrator account).

All 8 acceptance criteria from #1354 are met. Mapping below.

AC-1: Source badge on every Level 3 row — PASS. BudgetLineRow (CostBreakdownTable.tsx:243-255) renders the source badge alongside the existing confidence/quoted/invoiced badge. Truncation at 20 chars with full name in title and aria-label (l. 225-226, 252-253). Null budgetSourceId falls back to the sourceFilter.unassigned translation (l. 222-223). Mobile collapses to dot-only via CSS (sourceBadgeLabel hidden ≤767px, sourceBadgeDot shown).

AC-2: Stable, token-driven color identity — PASS. getSourceColorIndex is a deterministic id % 10 mapping (budgetSourceColors.ts). 10-slot palette --color-source-N-{bg,text,dot} defined in tokens.css with full dark-mode overrides (60 token entries). No hardcoded hex in components — chip uses scoped --chip-* custom properties; badge uses .sourceN class on Badge.module.css. Architect's nit on light-mode source-7 reusing source-1 bg/text is acknowledged but non-blocking at this user scale.

AC-3: Per-source impact under Available Funds — PASS. CostBreakdownTable.tsx:1303-1338 renders name | totalAmount | -allocatedCost | remaining per source. allocatedCost = resolveProjected(projectedMin, projectedMax, perspective) is perspective-aware. Negative remaining rendered with valueNegative token treatment.

AC-4: Filter chips with multi-select OR semantics — PASS. BudgetSourceChip (role=button, aria-pressed) toggles via onSourceToggle. Filter strip uses role=\"toolbar\" with aria-label=\"Filter by source\". OR semantics confirmed in CostBreakdownTable.tsx:798-820 (visibleLineIds union across all selected source keys). "All sources" / clear button appears when ≥1 chip selected (l. 1288-1296). Subtotals/grand totals recompute against filtered set (l. 1349-1372).

AC-5: URL persistence via ?sources= — PASS. BudgetOverviewPage.tsx:185-223 uses useSearchParams with comma-separated IDs. 'unassigned' literal key handles null-source lines. Cleared when set is empty (no empty ?sources=).

AC-6: Empty state when filter matches nothing — PASS. CostBreakdownTable.tsx:1378-1389 renders EmptyState in <td colSpan={4}> with localized message and "Clear filters" action. Per-source impact strip remains visible above for re-deselection.

AC-7: Accessibility — PASS. Native <button> chips with aria-pressed, focus-visible token, Escape-to-clear with focus return to expand button (l. 932-934). role=\"status\" aria-atomic=\"true\" live region announces filtered count or "all sources" (l. 1393-1401). Source badge exposes aria-label=\"Budget source: {name}\".

AC-8: Responsive + dark mode — PASS. Chip strip wraps with overflow-x: auto; mobile chip min-height 44px; mobile badge collapses to dot. Dark-mode overrides cover all 30 source values.

CI: All gates green (Quality Gates + E2E Gates + 16 E2E shards × 3 viewports + Coverage Report). qa-integration-tester authored unit/integration tests; e2e-test-engineer authored e2e/tests/budget/budget-source-filter.spec.ts (1370 lines). Architect and security reviews posted clean.

No blocking gaps. Ready to merge.

Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ux-designer] Design review for PR #1355 — Source Attribution Badges & Per-Source Filter.


What was verified

  • Token family --color-source-N-{bg,text,dot} (N=0–9), Layer 2 + Layer 3 dark mode overrides
  • Badge extended with source0source9 + sourceUnassigned classes (no parallel implementation)
  • BudgetSourceChip as a new shared component with scoped --chip-bg/text/dot pattern
  • Filter toolbar role="toolbar", chips with aria-pressed, Escape key + focus refocus
  • Mobile dot-only badge (.sourceBadgeLabel / .sourceBadgeDot swap via media query)
  • prefers-reduced-motion guard on chip transitions and row animation
  • Live region (role="status") for filter change announcements

Findings

Medium — Hardcoded hex/rgba in tokens.css (slots 5–9 and some dark-mode overrides)

Slots 5, 6, 8, 9 in Layer 2 use raw hex values instead of Layer 1 palette token references:

/* ACTUAL (Layer 2) */
--color-source-5-bg: #e9d5ff;   /* should be var(--color-purple-100) */
--color-source-5-text: #6b21a8; /* should be var(--color-purple-800) */
--color-source-5-dot: #a855f7;  /* should be var(--color-purple-500) */
--color-source-6-bg: #cffafe;   /* var(--color-cyan-100) */
--color-source-6-text: #0c4a6e; /* var(--color-cyan-900) */
--color-source-6-dot: #06b6d4;  /* var(--color-cyan-500) */
/* … slots 8, 9, and several dark-mode overrides similarly */

If purple and cyan palette tokens are not yet in Layer 1, they should be added there first, then referenced via var() — consistent with how slots 1–4 use var(--color-blue-N), var(--color-green-N), etc. Slot 7 dark-mode dot (#ec4899) and slots 8/9 dark text/dot are also raw hex. Fix: add missing Layer 1 palette tokens and update all Layer 2/3 source-slot values to reference them.

Medium — Slot 7 (Pink) shares the same bg/text tokens as Slot 1 (Blue) in light mode

--color-source-7-bg: var(--color-blue-100);  /* identical to slot 1 */
--color-source-7-text: var(--color-blue-800); /* identical to slot 1 */
--color-source-7-dot: #ec4899;               /* only the dot differs */

Slot 7 is named "Pink" but its background and label look identical to Slot 1 (Blue). The dot color is different, but in the badge the dot is 8px and the background+text is the primary visual. Users with 7+ sources will see two slots that look the same. Fix: use a pink/rose palette for bg and text (e.g., --color-rose-100 / --color-rose-800) so the badge is visually distinct.

Medium — .sourceFilterStrip missing flex-wrap: nowrap on mobile

The spec requires the chip strip to scroll horizontally on mobile (flex-wrap: nowrap; overflow-x: auto). The implemented base rule sets flex-wrap: wrap, with no @media (max-width: 767px) override to switch to nowrap. As a result, chips wrap onto multiple lines on small screens instead of scrolling. Fix:

@media (max-width: 767px) {
  .sourceFilterStrip {
    flex-wrap: nowrap;
  }
}

Low — animation: fadeIn 0.15s ease uses a hardcoded duration

The spec called for var(--transition-normal) here. The chip transitions correctly use the token; the row fade-in does not:

/* ACTUAL */
animation: fadeIn 0.15s ease;
/* SHOULD BE */
animation: fadeIn var(--transition-normal) ease;

Checklist

Item Status
Token family --color-source-N-{bg,text,dot} added to Layer 2 + Layer 3 PASS (but slots 5/6/8/9 hardcoded — medium)
All source badge classes in Badge.module.css use var() references PASS
Badge extended (not duplicated); title prop added PASS
BudgetSourceChip shared component, scoped --chip-* props PASS
role="toolbar", aria-pressed chips PASS
Escape key clears filter + refocuses Available Funds button PASS
Live region role="status" announcements PASS
Mobile dot-only badge (.sourceBadgeLabel hidden) PASS
44px touch targets on mobile PASS
prefers-reduced-motion guard on transitions and animation PASS
Dark mode token overrides in [data-theme="dark"] PASS (but raw hex in several slots — medium)
Mobile chip strip horizontal scroll (nowrap) FAIL (wraps instead — medium)
Row fadeIn uses var(--transition-normal) FAIL (hardcoded 0.15s — low)

Verdict

Non-blocking. The three medium findings (hardcoded palette values in source token slots, slot 7 pink/blue collision, missing mobile flex-wrap: nowrap on the strip) and one low finding (literal 0.15s animation duration) should be addressed before merge or in a follow-up. No accessibility or dark-mode usability blockers.

@steilerDev steilerDev merged commit c684f63 into beta Apr 25, 2026
32 checks passed
@steilerDev steilerDev deleted the feat/1354-cost-breakdown-source-filter branch April 25, 2026 10:56
@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.4.0-beta.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

steilerDev added a commit that referenced this pull request Apr 26, 2026
…with Cost/Payback/Net columns (#1357)

* feat(budget): rework per-source filter to use source rows as toggles with Cost/Payback/Net columns

- Source detail rows under Available Funds become the filter affordance: each <tr role="button" aria-pressed> toggles its source on click, Space, or Enter; Escape selects all
- All sources start selected by default; URL state stores deselections via ?deselectedSources=<id1>,<id2>
- Deselected rows are visually dimmed (text muted, dot opacity 0.4, no left-border accent); aria-pressed conveys state
- Items, areas, and any container with no surviving budget lines render null (cascade beyond just lines)
- Available Funds total recomputes to the sum of currently-selected sources; "(X of Y selected)" caption shown when filter is active
- New per-source columns: Cost (perspective-resolved sum), Payback (entity-level pro-rata, computed client-side), Net = totalAmount + payback - cost
- Remove BudgetSourceChip component, chip filter strip, and obsolete English/German i18n keys
- Live region moved outside table wrapper so announcements survive empty-state toggles

Closes #1356

This supersedes the chip-toolbar UX shipped in #1354/PR #1355 per user feedback.

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude translator (Sonnet 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude ux-designer (Sonnet 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude product-architect (Opus 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude product-owner (Opus 4.6) <noreply@anthropic.com>

* fix(budget): resolve i18n key collision and stale test assertions

- Rename i18n key overview.costBreakdown.availableFunds (object) to
  availableFundsFilter to avoid colliding with the same-named string
  ("Available funds"). JSON last-write-wins meant the label was being
  overwritten by the caption object, breaking 3 unit tests and the
  rendered Available Funds row label.
- Scope getByText('€200,000.00') to the Available Funds row via within()
  to disambiguate from the source detail row's Net column showing the
  same currency value.
- Relax the "1 of 2" caption regex to "<digit> of <digit>" — the fixture
  includes an unassigned line so the total is N+1 named sources.
- Replace the className-comparison dark-mode smoke check with an
  aria-pressed attribute assertion since deselected rows are styled via
  attribute selectors, not class toggles.

Fixes #1356

Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude translator (Sonnet 4.5) <noreply@anthropic.com>

* test(budget): fix two stale CostBreakdownTable scenarios for #1356

- Remove the second fireEvent.click in the work-item cascade test —
  the 'No Area' container is also cascade-hidden, so its expand button
  is never rendered. Asserting 'Sourced Work Item' is absent after the
  WI section expands is sufficient.
- Update the "expand shows sub-rows with name and Net value" test to
  set projectedMin/projectedMax to 0 on the source summaries so the Net
  column equals totalAmount; previously the default 5000/8000 values
  produced a non-zero Cost making Net != totalAmount.

Fixes #1356

Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>

* test(budget): use toHaveText auto-retry for live region + bump mobile row padding

- Replace `textContent()` + `toMatch` with `toHaveText` regex assertion on
  the filter live region. The previous synchronous read could land before
  React re-rendered the announcement after the chip-row click.
- Bump mobile row vertical padding from spacing-3 (12px) to spacing-4
  (16px) so the source detail row's bounding box meets the 44px touch
  target on mobile viewports.

Fixes #1356

Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>

* fix(budget): include Unassigned in selected count for caption + live region

Both the "(X of Y selected)" caption and the role="status" live region
computed selected count without considering the Unassigned pseudo-source.
When the user deselects the Unassigned chip, the previous expression
(budgetSources.length - deselectedSourceIds.size) drifted off-by-one
because deselectedSourceIds may contain the literal 'unassigned' key
that isn't in budgetSources.

Both expressions now compute selected as
  named-selected + (hasUnassignedLines && !deselectedSourceIds.has('unassigned') ? 1 : 0)

matching the existing total formula
  budgetSources.length + (hasUnassignedLines ? 1 : 0).

Fixes #1356

Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>

* test(budget): update caption test to use locale-agnostic regex

The off-by-one fix correctly counts the Unassigned pseudo-source as a
virtual source. The fixture (2 named + 1 unassigned = 3 virtual) now
shows "(2 of 3 selected)" after deselecting one source — the previous
"/1\D+\d+/" regex no longer matches. Use toHaveText with a generic
"<digit> of <digit>" regex so the assertion is locale-agnostic and
robust to count semantics.

Fixes #1356

Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <noreply@anthropic.com>

---------

Co-authored-by: Frank Steiler <frank@steiler.de>
Co-authored-by: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.4.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant