You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-**Story #1360** rewrote filter from client-side to server-side. `BudgetSourceSummaryBreakdown` now has `subsidyPaybackMin/Max` NOT `subsidyPayback`.
9
+
- URL format: `?deselectedSources=id1,id2` (comma-separated, URL-encoded via `encodeURIComponent(join(','))`).
10
+
-`waitForResponse` predicate for filtered: `url.includes('/api/budget/breakdown') && url.includes('deselectedSources=')`.
11
+
-`waitForResponse` predicate for unfiltered: `url.includes('/api/budget/breakdown') && !url.includes('deselectedSources=')`.
12
+
-**MUST register `waitForResponse` BEFORE the click** that triggers the debounced refetch.
13
+
- Route mock glob for breakdown: `'**/api/budget/breakdown**'` (leading `**` + trailing `**`) to match full URLs with `http://localhost:PORT/` prefix AND `?deselectedSources=` query strings. Path-only `${API.budgetBreakdown}**` is unreliable — see Playwright route glob memory note.
14
+
-`mountOverviewRoutes` now accepts 4th arg `filteredBreakdownBody?` — returns it when `deselectedSources=` is in URL.
15
+
-`makeBreakdownResponse` unassigned source in `budgetSources` now has `id:'unassigned'` — included by default (no opt-in).
-`breakdownRefetching` CSS class applied to wrapping div during in-flight refetch — testable via `[class*="breakdownRefetching"]`.
18
+
- Debounce is 50ms. Debounce debounce coalescence: `filteredRequestCount` listener on `page.on('request')` — works across AbortController cancellations.
19
+
- Available Funds expand button `aria-label` = `"Expand available funds sources"` (hardcoded, not i18n).
## Story #1360 — Server-Side Source Filter Tests (2026-04-25)
7
+
8
+
**CostBreakdownTable.test.tsx**: Replaced the 12-test `describe('Source filter — aggregate consistency (#1358)')` block with 4-test `describe('Server-driven render path (#1360)')`. The 12 old tests tested deleted client-side helpers (`computePerSourcePayback`, `computeFilteredAggregates`, `visibleLineIds`). Removal strategy: Python `content.replace()` on large block — incremental Edit tool calls left orphaned code. The `buildBreakdownWithTwoSources()` helper was replaced by `buildServerFilteredBreakdown()`.
9
+
10
+
**Route test `insertWorkItemWithSource` has `budgetSourceId: string` (NOT nullable)**: Use `insertWorkItem({ plannedAmount, confidence })` for null-source WIs in route tests — it always sets `budgetSourceId: null`.
11
+
12
+
**`BudgetSourceSummaryBreakdown` type now requires `subsidyPaybackMin/Max`**: Existing tests that use `{ id, name, totalAmount, projectedMin, projectedMax }` without these fields will have TypeScript errors. New tests must include both fields.
13
+
14
+
**Debounce + AbortController test patterns**: For Scenario 29 (error path), use real timers + `waitFor({ timeout: 5000 })` instead of fake timers. The `DEBOUNCE_MS=50` effect fires after `isLoading` transitions to false (double-fetch on mount is intentional — debounce effect re-runs when `isLoading` changes). For scenarios with fake timers: use `await act(async () => { jest.advanceTimersByTime(100); await Promise.resolve(); })` to advance timers and flush microtasks together.
15
+
16
+
## Story #1358 — CostBreakdownTable Filtered Aggregate Tests (2026-04-25)
17
+
18
+
Added `describe('Source filter — aggregate consistency (#1358)')` block (12 tests, lines ~4003–4782) to `CostBreakdownTable.test.tsx`. Key patterns: (1) Use `within(row).getByText(...)` to avoid multi-match collisions. (2) Get Level 0 header row via `screen.getByRole('button', { name: 'Expand work item budget by area' }).closest('tr')`. (3) Get Level 1 area row via `screen.getByRole('button', { name: 'Expand WI Area' }).closest('tr')`. (4) Get Level 2 item row via `screen.getByRole('link', { name: 'Item Title' }).closest('tr')`. (5) `td.colBudget` selector on rows for cost cell text assertions. (6) Math: `resolveLineCost(line, avg)` for `own_estimate` with `plannedAmount=N` = N (avg of 0.8N and 1.2N). (7) Pro-rata payback share = weight × entityPayback where weight = max-cost / sum-of-max-costs.
19
+
20
+
## Story #1356 — CostBreakdownTable Per-Source Filter Rework (2026-04-25)
21
+
22
+
Props changed again: `selectedSourceIds` → `deselectedSourceIds`, `onClearSources` → `onSelectAllSources`. Semantics inverted — a source is HIDDEN when its ID is in `deselectedSourceIds`. Source rows changed from chip toolbar (`role="toolbar"`, `Filter: Name` buttons) to `<tr role="button" aria-pressed="true|false" tabIndex={0}>` toggle rows directly in the Available Funds expansion. Tests checking `role="toolbar"` or `Filter: Name` buttons must be removed and replaced with `container.querySelector('tr[role="button"]')` assertions. Replace all old chip count assertions (e.g. `toHaveLength(2)` for "chip + sub-row") with `toBeInTheDocument()` for the single source detail row. The `onSelectAllSources` prop is called on Escape keydown on the source row (not on a toolbar).
23
+
24
+
## Story #1354 — CostBreakdownTable Props Refactor Pattern (2026-04-25)
25
+
26
+
`CostBreakdownTable` had `budgetSources={[]}` prop replaced with `selectedSourceIds={new Set()} onSourceToggle={() => {}} onClearSources={() => {}}`. When a component's prop API changes, use `replace_all: true` on Edit tool to update all test usages in one pass (28 occurrences updated at once). Also add new required fixture fields (`budgetSources: []` on BudgetBreakdown, `budgetSourceId: null` on BreakdownBudgetLine) via Python `sed`-style script when the pattern is uniform across many objects.
27
+
28
+
**Fix Loop Round 1 (2026-04-25)**: Tests at lines ~1844/1859/1884 still passed `budgetSources` as a JSX prop AND were missing required props. Fix: move source data into `breakdown={{ ...buildBreakdownWithWI(), budgetSources: [buildSourceSummary(...)] }}` and add `selectedSourceIds onSourceToggle onClearSources`. Also removed obsolete `buildBudgetSource()` helper (used `BudgetSource` full type — now use `buildSourceSummary()` with `BudgetSourceSummaryBreakdown`). In `BudgetOverviewPage.test.tsx`, Scenario 30 was testing the old `budgetSources` prop flow; updated to populate `breakdown.budgetSources` instead. Added Escape key tests for new `handleToolbarKeyDown` behavior.
29
+
30
+
**Stale dist warning**: `node_modules/@cornerstone/shared/dist/` must be rebuilt (`tsc -p shared/tsconfig.json --outDir node_modules/@cornerstone/shared/dist`) when shared types change. Without rebuild, `tsc --noEmit` on client shows false positives for `budgetSourceId`, `budgetSources`, `BudgetSourceSummaryBreakdown`. Jest is unaffected (maps to source).
**Critical**: Mocking `BudgetBar` at module level (`jest.unstable_mockModule('../../components/BudgetBar/BudgetBar.js', ...)`) breaks ALL existing tests that rely on BudgetBar rendering content (labels, role="img", segment text). BudgetBar renders segment labels (e.g. "Paid (unclaimed)", "Claimed") that existing tests assert on. The fix: test segment keys via observable behavior (aria-label, summaryLabel text) rather than mock capture. For segment structure verification, use `container.querySelectorAll('[class*="summaryRow"]')` to check rows and their label text order.
-`de/budget.json` — `overview.costBreakdown.area.unassigned` and `sources.lines.unassignedArea` both updated to "Kein Bereich" 2026-04-19 (Issue #1295), aligned with `de/areas.json` and the `noCategory` → "Keine Kategorie" parallel pattern
-**Pre-existing gap** (as of 2026-04-25, outside #1356 scope): `sources.lines.typeColumnHeader` and `sources.lines.statusColumnHeader` exist in `en` but not `de` — needs a dedicated spec to fix
49
51
- Always check key parity when picking up a new translator spec
50
52
51
53
## Backup/Restore Terminology (2026-03-22)
@@ -132,3 +134,19 @@ Note: `claimed` here uses "Beantragt" (applied/requested for subsidy) rather tha
-`overview.costBreakdown.sourceFilter.*`, `sourceImpact.*`, `sourceBadge.*` added to `de/budget.json`
141
+
- "Unassigned" (source filter / source badge context) → "Nicht zugewiesen" (glossary `Unassigned` term, not "Kein X" pattern which is used for area/category absence)
142
+
- "Budget source: {{name}}" (aria label) → "Budgetquelle: {{name}}" — always use full glossary term "Budgetquelle" in aria labels, short "Quelle" only in UI labels
-**Note**: `label`, `allSources`, `clearAriaLabel`, `chipSelected`, `chipNotSelected` and `activeAnnouncement` were added in #1354 but removed again in #1356 rework (chip-based filter replaced)
name: Story #1354 — Source Attribution Badges and Per-Source Filter
3
+
description: Visual spec for budget source color palette, source badge in BudgetLineRow, and filter chip strip in Available Funds section
4
+
type: project
5
+
---
6
+
7
+
## Key Decisions
8
+
9
+
**Source color palette**: 10-slot deterministic palette (`colorIndex = sourceId % 10`). Slot 0 = Unassigned (gray). Slots 1–9 match calendar-item hue families plus cyan. No schema change required.
10
+
11
+
**New token family**: `--color-source-N-bg`, `--color-source-N-text`, `--color-source-N-dot` for N=0–9, with Layer 3 dark-mode overrides. Added to tokens.css Layer 2 / Layer 3.
12
+
13
+
**Token naming**: `-dot` suffix provides a higher-saturation swatch color for the circle dot indicator (distinct from `-bg` which is low-saturation for badge background).
14
+
15
+
**Color-blind affordance**: source name label always present alongside dot; name truncated at 20 chars (badge) / 24 chars (chip) with full name in `title` + `aria-label`.
16
+
17
+
**Badge extension**: Add `source0`–`source9` and `sourceUnassigned` CSS classes to `Badge.module.css`. Add optional `title` prop to `Badge.tsx`. No new component needed for the badge.
18
+
19
+
**BudgetSourceChip**: New shared component at `client/src/components/BudgetSourceChip/`. Renders as `<button>`. Uses scoped CSS custom properties (`--chip-bg`, `--chip-text`, `--chip-dot`) set as inline `style` on the element to allow static CSS classes to consume per-slot token values cleanly (avoids 10x `.chipSlotN.chipSelected` rules).
20
+
21
+
**ARIA pattern for chip strip**: `role="toolbar"` (NOT `role="radiogroup"` — multi-select semantics). Each chip: `role="button"` (native), `aria-pressed="true|false"`. Tab moves through all chips. Escape clears filter and focuses "Available Funds" expand button.
**Available Funds columns**: existing name+total columns gain `allocatedCost` and `remaining` columns. Selected source detail rows get left-border accent using `--chip-dot` scoped property on `<tr>`.
26
+
27
+
**Empty filter state**: use existing `EmptyState` component in a `<td colSpan={4}>` row. No icon variant (filtered, not "add first item").
28
+
29
+
**Mobile badge**: dot only (label hidden via CSS). Touch-and-hold shows `title` attribute. Dot uses `-dot` token (higher contrast than `-bg`).
30
+
31
+
**Mobile chip strip**: `flex-wrap: nowrap; overflow-x: auto`. Chips 44px min touch height on mobile.
32
+
33
+
**Animations**: row fade-in via `@keyframes fadeIn` + `var(--transition-normal)`. `prefers-reduced-motion: reduce` disables all animations. Chip transitions via `var(--transition-normal)`.
34
+
35
+
**Why**: Spec posted as comment on issue #1354. Architecture decision (deterministic vs. persisted color) flagged to product-architect.
0 commit comments