Skip to content

usePrintExpansion hook loses pre-print state snapshot due to effect re-run on expandedKeys change #1450

@steilerDev

Description

@steilerDev

BUG-{number}: usePrintExpansion hook does not restore pre-print expansion state after afterprint

Severity: Major
Component: Frontend UI — Budget Overview print (CostBreakdownTable / usePrintExpansion)
Found in: e2e/tests/budget/budget-overview-print.spec.ts — "On-screen expansion state restored after afterprint"

Steps to Reproduce

  1. Navigate to Budget Overview (/budget/overview)
  2. Expand Work Items section and the Rohbau area (so Keller area row is visible)
  3. Leave the Keller area collapsed (so Kellerbau work item is NOT visible)
  4. Open the browser print dialog (or dispatch window.dispatchEvent(new Event('beforeprint')))
  5. Observe: all rows become fully expanded (including Kellerbau) — this is correct
  6. Close/cancel the print dialog (or dispatch window.dispatchEvent(new Event('afterprint')))
  7. Observe: Kellerbau work item remains VISIBLE — it should have been hidden again (pre-print state: Keller collapsed)

Expected Behavior

After afterprint fires, the CostBreakdownTable should restore to the exact expansion state that existed just before beforeprint fired. In this case: Work Items expanded, Rohbau expanded, Keller collapsed — so Kellerbau should NOT be visible.

Actual Behavior

Kellerbau (and all rows that were expanded during print) remain visible after afterprint. The table never collapses back to the pre-print state.

Root Cause (diagnosed)

usePrintExpansion captures the pre-print snapshot in a local variable inside its useEffect:

useEffect(() => {
  let snapshot: Set<string> | null = null;

  function handleBeforePrint() {
    snapshot = new Set(expandedKeys); // ← captured here
    forceExpand();                    // ← setExpandedKeys(allKeys) → triggers React re-render
  }

  function handleAfterPrint() {
    if (snapshot !== null) {
      setExpandedKeys(snapshot);      // ← this handler is REPLACED before afterprint fires
      snapshot = null;
    }
  }

  window.addEventListener('beforeprint', handleBeforePrint);
  window.addEventListener('afterprint', handleAfterPrint);

  return () => { /* cleanup */ };
}, [expandedKeys, forceExpand, setExpandedKeys]); // ← 'expandedKeys' in deps

The bug: forceExpand() inside handleBeforePrint calls setExpandedKeys(new Set(allKeys)), which changes expandedKeys. Because expandedKeys is in the useEffect dependency array, React runs the effect cleanup (removing the handlers that have snapshot) and then installs new handlers (with snapshot = null). When afterprint fires later, the NEW handleAfterPrint runs — snapshot is null → no restoration.

Fix Suggestion

Use a useRef to store the snapshot so the value persists across effect re-runs:

const snapshotRef = useRef<Set<string> | null>(null);

useEffect(() => {
  function handleBeforePrint() {
    snapshotRef.current = new Set(expandedKeys);
    forceExpand();
  }
  function handleAfterPrint() {
    if (snapshotRef.current !== null) {
      setExpandedKeys(snapshotRef.current);
      snapshotRef.current = null;
    }
  }
  window.addEventListener('beforeprint', handleBeforePrint);
  window.addEventListener('afterprint', handleAfterPrint);
  return () => {
    window.removeEventListener('beforeprint', handleBeforePrint);
    window.removeEventListener('afterprint', handleAfterPrint);
  };
}, [expandedKeys, forceExpand, setExpandedKeys]);

Environment

  • Browser: Chromium (Playwright E2E)
  • Viewport: Desktop 1920×1080
  • Docker: yes (testcontainers)

Evidence

E2E test failure (PR #1447, shard 1, run 25992894130):

TimeoutError: locator.waitFor: Timeout 5000ms exceeded.
Call log:
  - waiting for locator('...').getByRole('row').filter({ hasText: 'Kellerbau' }) to be hidden
    15 × locator resolved to visible <tr class="rowLevel2_EefiO">…</tr>

The Kellerbau row remains visible even after afterprint is dispatched. The page accessibility tree confirms all rows stay expanded indefinitely.

Notes

The E2E test "On-screen expansion state restored after afterprint" in e2e/tests/budget/budget-overview-print.spec.ts:412 correctly describes the required behavior per the AC spec. The test is not at fault — the production hook has the closure bug described above.

The companion test "Print forces full expansion of collapsed breakdown rows via beforeprint" had a separate test-side selector bug (fixed in the same PR) and is not related to this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions