Identifying State Leak Signatures #
Look for DOM elements retaining stale data or unexpected network requests firing on test start. Use browser memory snapshots and performance traces to track object retention across test boundaries. Compare heap allocations before and after each spec execution to isolate retention spikes. In Playwright, the --trace flag captures network and DOM activity per test; in Cypress, the command log shows requests that fire outside of intercept scopes.
Isolating the Leak Source #
Run tests sequentially with --retries=0 and isolate failing specs. Inject beforeEach/afterEach hooks that explicitly clear React state stores and reset mocked timers. Correlate leak timing with specific useEffect dependency arrays. Understanding the broader Root Causes of JavaScript Test Flakiness helps prioritize which isolation strategy to deploy first.
Implementing Deterministic Cleanup #
Enforce strict return functions in all useEffect hooks to abort fetches and clear intervals. Configure E2E runners to spawn fresh browser contexts per spec. Inject teardown scripts that force React unmounting before the next test begins.
Validation & Regression Prevention #
Add automated leak detection to CI by monitoring heap size deltas and tracking unhandled promise rejections. Implement snapshot assertions on global state stores post-test to catch silent pollution. Enforce strict lint rules for missing cleanup returns in async hooks (e.g., ESLint react-hooks/exhaustive-deps).
Framework Configuration & Cleanup Snippets #
// Playwright: Force fresh context per test (default behavior with test isolation)
// Set storageState to undefined to ensure no auth cookies or localStorage carry over.
import { test } from '@playwright/test';
test.use({ storageState: undefined });
// React Cleanup: Strict useEffect teardown
// The return function runs when the component unmounts or deps change.
// Without it, the interval continues firing after unmount, polluting the next test.
useEffect(() => {
const id = setInterval(fetchData, 5000);
return () => clearInterval(id);
}, [fetchData]);
// Cypress Config: Enforce test isolation
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
// testIsolation: true (the default since Cypress 12) clears cookies,
// localStorage, and sessionStorage between each test automatically.
testIsolation: true,
retries: { runMode: 2, openMode: 0 }
}
});
// Cypress: Attempt to trigger GC after each test for heap tracking.
// window.gc() is only available when Chrome is launched with --expose-gc.
// Use this for debugging only, not in standard CI pipelines.
afterEach(() => {
cy.window().then(win => {
if (typeof win.gc === 'function') win.gc();
});
});
Common Pitfalls #
- Relying on
setTimeoutorcy.wait()instead of explicit state assertions - Missing cleanup returns in
useEffectfor async data fetches - Sharing browser contexts or localStorage across E2E test files
- Ignoring React StrictMode double-invoke behavior during test initialization — StrictMode intentionally runs effects twice in development to surface missing cleanup; ensure all effects have proper teardown
- Failing to mock or intercept pending XHR/fetch calls before unmount
FAQ #
Q: How do I distinguish a state leak from a network race condition? A: State leaks persist across test boundaries and affect DOM/memory baselines, while network races cause transient timeouts within a single test run. Isolate by disabling network intercepts and observing if the failure persists — if it does, the root cause is leaked state rather than network timing.
Q: Does React StrictMode cause false-positive flakiness in E2E tests?
A: StrictMode intentionally double-invokes effects in development to surface missing cleanup. Ensure all async operations have proper teardown returns. In test environments, StrictMode behavior depends on whether you bundle with NODE_ENV=development or production; most E2E setups target the production build and are unaffected.
Q: What is the most reliable way to force React unmounting between tests?
A: Navigate to a blank route (/reset or a dedicated empty page) before each test, or use fresh browser contexts. Verify the React root container is empty before mounting the next component by asserting that document.querySelector('#root') has no children.
Reliability Metrics #
| Metric | Target |
|---|---|
| Target Pass Rate | 99.5%+ |
| Max Flake Rate | <0.5% |
| Avg Cleanup Latency | <150 ms |
| Heap Growth Threshold | 5 MB per test |
| Cross-Test Pollution Incidents | 0 |