# no-global-state-mutation Prevent global state mutations that can cause test interdependencies. ## Rule Details Global state mutations in tests can cause tests to depend on each other, leading to flaky behavior: - Tests may pass when run individually but fail when run together - Test execution order can affect results - Parallel test execution can cause race conditions - State changes in one test can leak into subsequent tests - Environment variables and global objects can be modified unexpectedly This rule helps prevent test flakiness by detecting mutations to global state that could affect other tests. ## Options This rule accepts an options object with the following properties: ```json { "test-flakiness/no-global-state-mutation": [ "error", { "allowInHooks": true } ] } ``` ### `allowInHooks` (default: `true`) When set to `true`, allows global state mutations in setup and teardown hooks (`beforeEach`, `afterEach`, `beforeAll`, `afterAll`). ```javascript // With allowInHooks: true (default) beforeEach(() => { window.location.href = "http://localhost"; // Allowed }); // With allowInHooks: false beforeEach(() => { window.location.href = "http://localhost"; // Not allowed }); ``` ## Examples ### Incorrect ```javascript // Direct global object mutations window.location.href = "http://example.com"; document.title = "Test Page"; global.fetch = mockFetch; process.env.NODE_ENV = "test"; // Local/Session storage mutations localStorage.setItem("token", "fake-token"); sessionStorage.clear(); // Console pollution console.log("Debug info"); console.error("Test error"); // Navigator modifications navigator.userAgent = "fake-agent"; // Global variable creation globalTestVar = "some value"; // Creates global without declaration // Event listener modifications window.addEventListener("resize", handler); document.addEventListener("click", handler); // Test execution state changes test.only("should do something", () => { // This affects global test execution }); // Process environment mutations process.env.API_URL = "http://localhost:3000"; delete process.env.NODE_ENV; // Document structure changes document.write("
Test content
"); document.body.innerHTML = "

Test

"; ``` ### Correct ```javascript // Use proper setup/teardown hooks beforeEach(() => { // Setup is allowed in hooks window.location.href = "http://localhost"; localStorage.setItem("token", "test-token"); }); afterEach(() => { // Cleanup in teardown localStorage.clear(); delete window.customProperty; }); // Use local variables instead test("should handle data", () => { const mockData = { id: 1, name: "test" }; const result = processData(mockData); expect(result).toBe(expected); }); // Mock globals properly with cleanup beforeEach(() => { originalFetch = global.fetch; global.fetch = jest.fn(); }); afterEach(() => { global.fetch = originalFetch; }); // Use environment variable mocking with restore beforeEach(() => { originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = "test"; }); afterEach(() => { process.env.NODE_ENV = originalEnv; }); // Use proper test utilities test("should render correctly", () => { render(); expect(screen.getByText("Hello")).toBeInTheDocument(); }); // Use local scope for test data test("should calculate total", () => { const items = [{ price: 10 }, { price: 20 }]; expect(calculateTotal(items)).toBe(30); }); ``` ## Best Practices ### 1. Use Setup and Teardown Hooks Always use proper setup and teardown hooks for global state changes: ```javascript describe("Component with global state", () => { let originalLocation; beforeEach(() => { // Store original state originalLocation = window.location.href; // Set test state window.location.href = "http://localhost:3000/test"; }); afterEach(() => { // Restore original state window.location.href = originalLocation; // Clear any other changes localStorage.clear(); }); test("should work with mocked location", () => { // Test implementation }); }); ``` ### 2. Mock Globals Properly Use proper mocking techniques that include cleanup: ```javascript describe("API tests", () => { const originalFetch = global.fetch; beforeEach(() => { global.fetch = jest.fn(); }); afterEach(() => { global.fetch = originalFetch; jest.clearAllMocks(); }); }); ``` ### 3. Use Test Utilities for Environment Variables Use testing utilities for environment variable management: ```javascript // Using jest-environment-node const originalEnv = process.env; beforeEach(() => { process.env = { ...originalEnv }; process.env.NODE_ENV = "test"; process.env.API_URL = "http://localhost:3000"; }); afterEach(() => { process.env = originalEnv; }); ``` ### 4. Prefer Local Test Data Keep test data local to avoid global state pollution: ```javascript // Instead of global test data globalTestData = { users: [...] }; // Use local data in each test test('should handle users', () => { const testData = { users: [{ id: 1, name: 'John' }] }; const result = processUsers(testData.users); expect(result).toEqual(expected); }); ``` ### 5. Use Proper DOM Testing Use testing utilities that handle DOM state properly: ```javascript // Instead of manipulating document directly document.body.innerHTML = "
test
"; // Use testing library utilities test("should render component", () => { render(); expect(screen.getByRole("button")).toBeInTheDocument(); }); ``` ### 6. Handle Storage APIs Correctly Mock storage APIs with proper cleanup: ```javascript beforeEach(() => { // Mock localStorage const mockStorage = { getItem: jest.fn(), setItem: jest.fn(), removeItem: jest.fn(), clear: jest.fn(), }; Object.defineProperty(window, "localStorage", { value: mockStorage, }); }); ``` ## Framework-Specific Examples ### Jest ```javascript // ❌ Avoid global mutations global.API_URL = "http://localhost"; // ✅ Use proper Jest patterns beforeAll(() => { process.env.API_URL = "http://localhost"; }); afterAll(() => { delete process.env.API_URL; }); // ✅ Use Jest's environment variables // In jest.config.js module.exports = { setupFilesAfterEnv: ["/test-setup.js"], testEnvironment: "jsdom", }; ``` ### Vitest ```javascript // ✅ Use Vitest's environment handling import { beforeEach, afterEach, vi } from "vitest"; beforeEach(() => { vi.stubGlobal("fetch", vi.fn()); }); afterEach(() => { vi.unstubAllGlobals(); }); ``` ### Cypress ```javascript // ❌ Avoid global mutations in test files window.localStorage.setItem("token", "fake"); // ✅ Use Cypress commands and hooks beforeEach(() => { cy.window().then((win) => { win.localStorage.setItem("token", "fake"); }); }); // ✅ Use Cypress utilities cy.clearLocalStorage(); cy.clearCookies(); ``` ### Playwright ```javascript // ✅ Use Playwright's context isolation test("should work with storage", async ({ page }) => { await page.addInitScript(() => { localStorage.setItem("token", "fake"); }); // Test implementation }); ``` #### Browser-context callbacks are exempt Callbacks passed to `page.evaluate`, `page.addInitScript`, and `page.evaluateHandle` run in the **browser context**, not in the Node test process. Global-state mutations inside them (e.g. `localStorage` cleanup) are isolated browser-side operations, not Node-side global-state leaks, so the rule does **not** flag them — no `eslint-disable` comment is needed: ```javascript // ✅ Not flagged — runs in the browser, not Node test.beforeEach(async ({ page }) => { await page.addInitScript(() => { localStorage.removeItem("my-persist-key"); }); }); // ✅ Also not flagged — page.evaluate / page.evaluateHandle callbacks await page.evaluate(() => { localStorage.clear(); delete window.__APP_STATE__; }); ``` > **Note (Cypress):** the `cy.window().then((win) => win.localStorage...)` > form is out of scope — it mutates `win.localStorage` (a member-object > reference), not the bare `localStorage` global, so the rule already does not > flag it. **Limitations of the exemption:** - Only **inline** callbacks are exempt. A reference passed by name — `page.addInitScript(myNamedFn)` where `myNamedFn` mutates storage — is still flagged, because the rule cannot follow the reference to its body. - The exemption keys off the callback method **name** (`evaluate`/`addInitScript`/`evaluateHandle`), not the receiver object, so any unrelated `.evaluate(fn)` (e.g. mathjs `math.evaluate(...)`) is also exempted. This is a deliberate trade-off that keeps all Playwright receivers (`page`/`frame`/`locator`/`elementHandle`) working without hard-coding `page`. ## Common Global Objects to Avoid Mutating ### Process Environment ```javascript // ❌ Avoid process.env.NODE_ENV = "production"; // ✅ Better beforeEach(() => { originalNodeEnv = process.env.NODE_ENV; process.env.NODE_ENV = "production"; }); afterEach(() => { process.env.NODE_ENV = originalNodeEnv; }); ``` ### Window Object ```javascript // ❌ Avoid window.customProperty = "value"; // ✅ Better beforeEach(() => { window.customProperty = "value"; }); afterEach(() => { delete window.customProperty; }); ``` ### Document ```javascript // ❌ Avoid document.title = "Test Page"; // ✅ Better beforeEach(() => { originalTitle = document.title; document.title = "Test Page"; }); afterEach(() => { document.title = originalTitle; }); ``` ## When Not To Use It This rule may not be suitable if: - You're testing global state management specifically - Your application architecture requires global state modifications - You're testing browser APIs that require global mutations - You're using test utilities that handle cleanup automatically In these cases, you can: ```javascript // Disable for specific lines // eslint-disable-next-line test-flakiness/no-global-state-mutation window.customAPI = mockAPI; // Disable for test files that specifically test global behavior /* eslint-disable test-flakiness/no-global-state-mutation */ ``` ## Related Rules - [no-test-isolation](./no-test-isolation.md) - Prevents test interdependencies - [no-random-data](./no-random-data.md) - Prevents non-deterministic test data - [no-unconditional-wait](./no-unconditional-wait.md) - Encourages proper test timing ## Further Reading - [Jest - Setup and Teardown](https://jestjs.io/docs/setup-teardown) - [Testing Library - Best Practices](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library) - [Vitest - Mocking](https://vitest.dev/guide/mocking.html) - [Playwright - Test Isolation](https://playwright.dev/docs/browser-contexts) - [Clean Code - Test Isolation Principles](https://blog.cleancoder.com/uncle-bob/2017/05/05/TestDefinitions.html)