Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .claude/agent-memory/e2e-test-engineer/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,25 @@
> Detailed notes live in topic files. This index links to them.
> See: `e2e-pom-patterns.md`, `e2e-parallel-isolation.md`, `story-epic08-e2e.md`, `story-933-dav-vendor-contacts.md`, `milestones-e2e.md`, `story-1248-mass-move.md`

## Budget Source Filter E2E (Story #1360, 2026-04-25 — server-side filter)

- **Story #1360** rewrote filter from client-side to server-side. `BudgetSourceSummaryBreakdown` now has `subsidyPaybackMin/Max` NOT `subsidyPayback`.
- URL format: `?deselectedSources=id1,id2` (comma-separated, URL-encoded via `encodeURIComponent(join(','))`).
- `waitForResponse` predicate for filtered: `url.includes('/api/budget/breakdown') && url.includes('deselectedSources=')`.
- `waitForResponse` predicate for unfiltered: `url.includes('/api/budget/breakdown') && !url.includes('deselectedSources=')`.
- **MUST register `waitForResponse` BEFORE the click** that triggers the debounced refetch.
- 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.
- `mountOverviewRoutes` now accepts 4th arg `filteredBreakdownBody?` — returns it when `deselectedSources=` is in URL.
- `makeBreakdownResponse` unassigned source in `budgetSources` now has `id:'unassigned'` — included by default (no opt-in).
- `makeBreakdownSourceAOnly` budgetSources: uses `subsidyPaybackMin: 0, subsidyPaybackMax: 0` (not `subsidyPayback`).
- `breakdownRefetching` CSS class applied to wrapping div during in-flight refetch — testable via `[class*="breakdownRefetching"]`.
- Debounce is 50ms. Debounce debounce coalescence: `filteredRequestCount` listener on `page.on('request')` — works across AbortController cancellations.
- Available Funds expand button `aria-label` = `"Expand available funds sources"` (hardcoded, not i18n).
- Source badge in Level 3 rows: `<span aria-label="Budget source: {name}">`. Unassigned: `aria-label="Budget source: Unassigned"`.
- Source row toggle: `tr[class*="rowSourceDetail"]` with `aria-pressed` attribute. Filter by `getByText(name, {exact:true})`.
- Dark mode color check: create throw-away element to normalize `rgb()` format (see Print E2E Patterns note).
- Prior Story #1354 chips/toolbar pattern is gone — no `role="toolbar"` anymore (tests assert its absence).

## Print E2E Patterns (Issue #1310, 2026-04-19)

- `page.emulateMedia({ media: 'print' })` makes CSS `@media print` rules apply without dispatching window events.
Expand Down
26 changes: 26 additions & 0 deletions .claude/agent-memory/qa-integration-tester/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,32 @@
> Detailed notes live in topic files. This index links to them.
> See: `budget-categories-story-142.md`, `e2e-pom-patterns.md`, `e2e-parallel-isolation.md`, `story-358-document-linking.md`, `story-360-document-a11y.md`, `story-epic08-e2e.md`, `story-509-manage-page.md`, `story-471-dashboard.md`

## Story #1360 — Server-Side Source Filter Tests (2026-04-25)

**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()`.

**Route test `insertWorkItemWithSource` has `budgetSourceId: string` (NOT nullable)**: Use `insertWorkItem({ plannedAmount, confidence })` for null-source WIs in route tests — it always sets `budgetSourceId: null`.

**`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.

**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.

## Story #1358 — CostBreakdownTable Filtered Aggregate Tests (2026-04-25)

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.

## Story #1356 — CostBreakdownTable Per-Source Filter Rework (2026-04-25)

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).

## Story #1354 — CostBreakdownTable Props Refactor Pattern (2026-04-25)

`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.

**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.

**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).

## BudgetBar Module-Level Mock Anti-Pattern (2026-04-20)

**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.
Expand Down
18 changes: 18 additions & 0 deletions .claude/agent-memory/translator/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ Action labels in German follow the pattern: `{Noun} {Verb}` with capitalised fir
- `de/areas.json` created 2026-04-16 (Story #1237): `noArea` → "Kein Bereich", `pathLabel` → "Bereichspfad"
- `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
- `de/budget.json` — `sources.lines.noCategory` orphan deleted 2026-04-19 (Issue #1313); `sources.lines.invoiceStatus.*`, `sources.lines.underArea`, `sources.lines.typeColumnHeader`, `sources.lines.statusColumnHeader` added 2026-04-19 (Issue #1313)
- `de/budget.json` — Issue #1356 (2026-04-25): `sourceFilter` rework — removed `label`, `allSources`, `clearAriaLabel`, `chipSelected`, `chipNotSelected`, `activeAnnouncement`; added `statusAnnouncement`; added new blocks `sourceRow.*` and `availableFunds.*`
- **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
- Always check key parity when picking up a new translator spec

## Backup/Restore Terminology (2026-03-22)
Expand Down Expand Up @@ -132,3 +134,19 @@ Note: `claimed` here uses "Beantragt" (applied/requested for subsidy) rather tha
- `summaryClaimedLabel` → "Eingereicht" (bar chart summary; consistent with `barChart.claimed` = "Eingereicht")
- `srOnly` screen reader text: "Eingereicht {{claimed}}, Bezahlt {{paid}}, Projiziert {{projectedMin}} bis {{projectedMax}}, von Gesamt {{total}}"
- Obsolete keys removed in this update: `allocated`, `total`, `available`, `planned`

## Source Filter & Source Badge Patterns — Issue #1354 (2026-04-25)

- `overview.costBreakdown.sourceFilter.*`, `sourceImpact.*`, `sourceBadge.*` added to `de/budget.json`
- "Unassigned" (source filter / source badge context) → "Nicht zugewiesen" (glossary `Unassigned` term, not "Kein X" pattern which is used for area/category absence)
- "Budget source: {{name}}" (aria label) → "Budgetquelle: {{name}}" — always use full glossary term "Budgetquelle" in aria labels, short "Quelle" only in UI labels
- `sourceImpact.allocated` → "Zugeordnet"; `sourceImpact.remaining` → "Verbleibend"
- **Note**: `label`, `allSources`, `clearAriaLabel`, `chipSelected`, `chipNotSelected` and `activeAnnouncement` were added in #1354 but removed again in #1356 rework (chip-based filter replaced)

## Source Row & Status Announcement Patterns — Issue #1356 (2026-04-25)

- `sourceFilter.statusAnnouncement` → "{{selected}} von {{total}} Budgetquellen ausgewählt" (uses plural "Budgetquellen" from glossary)
- `sourceRow.selectedAriaLabel` → "{{name}}, ausgewählt – zum Abwählen klicken" (en-dash, infinitive construction)
- `sourceRow.deselectedAriaLabel` → "{{name}}, abgewählt – zum Auswählen klicken"
- `availableFunds.activeFilterCaption` → "({{selected}} von {{total}} ausgewählt)"
- Aria label click-instruction pattern: "– zum [Verb] klicken" (en-dash, infinitive with "zu")
8 changes: 8 additions & 0 deletions .claude/agent-memory/ux-designer/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,11 @@ Zero new tokens, zero new CSS, zero new components. Text replacement only. Key r
- German currency trailing `€` with space may widen budget table cells — acceptable, no layout fix needed
- SearchPicker default prop strings (`placeholder`, `emptyHint`, etc.) replaced with `t()` — no CSS change
- Language selector UI is Story #917 (ProfilePage) — not this story

## Story #1354 — Source Badges & Filter (Budget Overview)

See [story-1354-source-badges-filter.md](story-1354-source-badges-filter.md). Key: 10-slot `--color-source-N-{bg,text,dot}` token family; `BudgetSourceChip` new shared component; `role="toolbar"` for chip strip; scoped `--chip-*` CSS custom properties pattern for per-slot colors.

## Issue #1356 — Per-Source Filter Rework (CostBreakdownTable)

Spec posted at https://github.com/steilerDev/cornerstone/issues/1356#issuecomment-4319895233. Key: chip toolbar + `BudgetSourceChip` deleted entirely; source detail rows become `<tr role="button" tabIndex={0} aria-pressed>`; filter semantics inverted to `deselectedSourceIds`; URL param renamed `?sources=` → `?deselectedSources=`; deselected state = muted text + `opacity:0.4` dot + transparent border (non-color signals required); Escape on focused row → select-all; no new tokens; cascade requires emptying parent nodes when all children hidden.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
name: Story #1354 — Source Attribution Badges and Per-Source Filter
description: Visual spec for budget source color palette, source badge in BudgetLineRow, and filter chip strip in Available Funds section
type: project
---

## Key Decisions

**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.

**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.

**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).

**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`.

**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.

**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).

**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.

**Filter state**: URL query param `?sources=2,5,8` (comma-separated IDs). `BudgetOverviewPage` owns URL state, passes `selectedSourceIds: Set<number>` + `onSourceToggle` + `onClearFilters` props to `CostBreakdownTable`.

**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>`.

**Empty filter state**: use existing `EmptyState` component in a `<td colSpan={4}>` row. No icon variant (filtered, not "add first item").

**Mobile badge**: dot only (label hidden via CSS). Touch-and-hold shows `title` attribute. Dot uses `-dot` token (higher contrast than `-bg`).

**Mobile chip strip**: `flex-wrap: nowrap; overflow-x: auto`. Chips 44px min touch height on mobile.

**Animations**: row fade-in via `@keyframes fadeIn` + `var(--transition-normal)`. `prefers-reduced-motion: reduce` disables all animations. Chip transitions via `var(--transition-normal)`.

**Why**: Spec posted as comment on issue #1354. Architecture decision (deterministic vs. persisted color) flagged to product-architect.
57 changes: 57 additions & 0 deletions client/src/components/Badge/Badge.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,63 @@
color: var(--color-user-inactive-text);
}

/* Budget source badge variants (source0–source9, sourceUnassigned) */
.source0 {
background-color: var(--color-source-0-bg);
color: var(--color-source-0-text);
}

.source1 {
background-color: var(--color-source-1-bg);
color: var(--color-source-1-text);
}

.source2 {
background-color: var(--color-source-2-bg);
color: var(--color-source-2-text);
}

.source3 {
background-color: var(--color-source-3-bg);
color: var(--color-source-3-text);
}

.source4 {
background-color: var(--color-source-4-bg);
color: var(--color-source-4-text);
}

.source5 {
background-color: var(--color-source-5-bg);
color: var(--color-source-5-text);
}

.source6 {
background-color: var(--color-source-6-bg);
color: var(--color-source-6-text);
}

.source7 {
background-color: var(--color-source-7-bg);
color: var(--color-source-7-text);
}

.source8 {
background-color: var(--color-source-8-bg);
color: var(--color-source-8-text);
}

.source9 {
background-color: var(--color-source-9-bg);
color: var(--color-source-9-text);
}

.sourceUnassigned {
background-color: var(--color-source-0-bg);
color: var(--color-source-0-text);
font-style: italic;
}

/* Responsive */
@media (max-width: 767px) {
.badge {
Expand Down
16 changes: 16 additions & 0 deletions client/src/components/Badge/Badge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,20 @@ describe('Badge', () => {
const span = container.querySelector('span');
expect(span?.textContent).toBe('raw_value');
});

// ─── title prop ─────────────────────────────────────────────────────────────

it('forwards title prop to the rendered span', () => {
const { container } = render(
<Badge variants={SIMPLE_VARIANTS} value="foo" title="Full Source Name" />,
);
const span = container.querySelector('span');
expect(span).toHaveAttribute('title', 'Full Source Name');
});

it('does not render title attribute when title prop is not passed', () => {
const { container } = render(<Badge variants={SIMPLE_VARIANTS} value="foo" />);
const span = container.querySelector('span');
expect(span).not.toHaveAttribute('title');
});
});
5 changes: 3 additions & 2 deletions client/src/components/Badge/Badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ interface BadgeProps {
variants: BadgeVariantMap;
value: string;
ariaLabel?: string;
title?: string;
testId?: string;
className?: string;
}

export function Badge({ variants, value, ariaLabel, testId, className }: BadgeProps) {
export function Badge({ variants, value, ariaLabel, title, testId, className }: BadgeProps) {
const variant = variants[value];
const combinedClass = [styles.badge, variant?.className, className].filter(Boolean).join(' ');

return (
<span className={combinedClass} aria-label={ariaLabel} data-testid={testId}>
<span className={combinedClass} aria-label={ariaLabel} title={title} data-testid={testId}>
{variant?.label ?? value}
</span>
);
Expand Down
Loading
Loading