# no-immediate-assertions Prevent immediate assertions after async operations without proper waiting. ## Rule Details Immediate assertions after asynchronous operations are a common source of flaky tests: - DOM updates happen asynchronously after user interactions - State changes may not be immediately reflected in the UI - Network requests complete at unpredictable times - Component re-renders are asynchronous - Browser events are processed asynchronously This rule helps prevent test flakiness by detecting assertions that happen immediately after async operations without proper waiting mechanisms. ## Options This rule accepts an options object with the following properties: ```json { "test-flakiness/no-immediate-assertions": [ "error", { "allowedAfterOperations": [], "requireWaitFor": true, "ignoreDataTestId": false } ] } ``` ### `allowedAfterOperations` (default: `[]`) Array of operation names that are allowed to be followed by immediate assertions. ```javascript // With allowedAfterOperations: ["render"] render(); expect(screen.getByText("Hello")).toBeInTheDocument(); // Allowed // With allowedAfterOperations: [] render(); expect(screen.getByText("Hello")).toBeInTheDocument(); // Not allowed ``` ### `requireWaitFor` (default: `true`) When set to `true`, requires `waitFor` for assertions after async operations. ```javascript // With requireWaitFor: true (default) await user.click(button); await waitFor(() => expect(element).toBeVisible()); // Required // With requireWaitFor: false await user.click(button); expect(element).toBeVisible(); // Allowed ``` ### `ignoreDataTestId` (default: `false`) When set to `true`, allows immediate assertions on elements with `data-testid`. ```javascript // With ignoreDataTestId: true await user.click(button); expect(screen.getByTestId("result")).toBeVisible(); // Allowed // With ignoreDataTestId: false (default) await user.click(button); expect(screen.getByTestId("result")).toBeVisible(); // Not allowed ``` ## Examples ### Incorrect ```javascript // User interactions followed by immediate assertions await user.click(button); expect(screen.getByText("Success")).toBeInTheDocument(); await user.type(input, "text"); expect(input).toHaveValue("text"); // Form submissions fireEvent.submit(form); expect(onSubmit).toHaveBeenCalled(); // State updates setState(newValue); expect(component.state.value).toBe(newValue); // API calls fetchData(); expect(screen.getByText("Data loaded")).toBeVisible(); // Component interactions await user.hover(element); expect(tooltip).toBeVisible(); await user.focus(input); expect(input).toHaveFocus(); // Playwright immediate assertions await page.click("button"); await expect(page.locator(".result")).toBeVisible(); // Cypress immediate assertions cy.click("button"); cy.get(".message").should("be.visible"); // React Testing Library fireEvent.change(input, { target: { value: "new value" } }); expect(screen.getByDisplayValue("new value")).toBeInTheDocument(); // Mock function calls mockFunction(); expect(mockFunction).toHaveBeenCalledWith(expectedArgs); // DOM manipulations element.classList.add("active"); expect(element).toHaveClass("active"); // Async component updates component.update(); expect(wrapper.find(".updated")).toHaveLength(1); ``` ### Correct ```javascript // Use waitFor for assertions after user interactions await user.click(button); await waitFor(() => { expect(screen.getByText("Success")).toBeInTheDocument(); }); // Wait for form submission effects fireEvent.submit(form); await waitFor(() => { expect(onSubmit).toHaveBeenCalled(); }); // Wait for state changes to propagate setState(newValue); await waitFor(() => { expect(screen.getByText(newValue)).toBeInTheDocument(); }); // Use findBy* queries for async content await user.click(loadButton); expect(await screen.findByText("Data loaded")).toBeInTheDocument(); // Wait for tooltips and overlays await user.hover(element); await waitFor(() => { expect(screen.getByRole("tooltip")).toBeVisible(); }); // Playwright with proper waiting await page.click("button"); await page.waitForSelector(".result"); await expect(page.locator(".result")).toBeVisible(); // Cypress with proper commands cy.click("button"); cy.get(".message").should("be.visible"); // Cypress automatically retries // Use Testing Library's async utilities fireEvent.change(input, { target: { value: "new value" } }); await waitFor(() => { expect(screen.getByDisplayValue("new value")).toBeInTheDocument(); }); // Wait for mock calls in async scenarios triggerAsyncAction(); await waitFor(() => { expect(mockFunction).toHaveBeenCalledWith(expectedArgs); }); // Use immediate assertions only for synchronous operations const result = syncFunction(); expect(result).toBe(expected); // Allowed operations (with configuration) render(); expect(screen.getByText("Hello")).toBeInTheDocument(); // If in allowedAfterOperations ``` ## Best Practices ### 1. Use waitFor for UI Updates Always wrap assertions in `waitFor` when testing UI changes: ```javascript // Instead of immediate assertion await user.click(toggleButton); expect(panel).toBeVisible(); // ❌ Race condition // Use waitFor await user.click(toggleButton); await waitFor(() => { expect(panel).toBeVisible(); // ✅ Safe }); ``` ### 2. Use findBy\* Queries for New Content Use `findBy*` queries when waiting for new content to appear: ```javascript // Instead of immediate getBy* await user.click(loadDataButton); expect(screen.getByText("Data loaded")).toBeInTheDocument(); // ❌ Flaky // Use findBy* await user.click(loadDataButton); expect(await screen.findByText("Data loaded")).toBeInTheDocument(); // ✅ Safe ``` ### 3. Wait for Disappearance Use `waitForElementToBeRemoved` for content that should disappear: ```javascript // Instead of immediate assertion await user.click(closeButton); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); // ❌ Flaky // Wait for removal await user.click(closeButton); await waitForElementToBeRemoved(() => screen.queryByRole("dialog")); // ✅ Safe ``` ### 4. Handle Form Submissions Properly Wait for form submission effects: ```javascript // Instead of immediate check fireEvent.submit(form); expect(onSubmit).toHaveBeenCalled(); // ❌ May not be called yet // Wait for the effect fireEvent.submit(form); await waitFor(() => { expect(onSubmit).toHaveBeenCalled(); // ✅ Safe }); ``` ### 5. Consider Component Update Lifecycle Account for React's update lifecycle: ```javascript // Instead of immediate state check setState(newValue); expect(component.state).toBe(newValue); // ❌ State may not be updated // Wait for component to re-render setState(newValue); await waitFor(() => { expect(screen.getByText(newValue)).toBeInTheDocument(); // ✅ Safe }); ``` ### 6. Mock Timers When Testing Time-Based Behavior Use fake timers for time-based assertions: ```javascript beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); test("should delay action", () => { delayedAction(); // Fast-forward time instead of waiting jest.advanceTimersByTime(1000); expect(screen.getByText("Delayed result")).toBeInTheDocument(); }); ``` ## Framework-Specific Examples ### React Testing Library ```javascript // ❌ Immediate assertions fireEvent.change(input, { target: { value: "test" } }); expect(screen.getByDisplayValue("test")).toBeInTheDocument(); // ✅ Proper waiting fireEvent.change(input, { target: { value: "test" } }); await waitFor(() => { expect(screen.getByDisplayValue("test")).toBeInTheDocument(); }); // ✅ Use user events (they have built-in waiting) await user.type(input, "test"); expect(input).toHaveValue("test"); // User events wait for updates ``` ### Jest ```javascript // ❌ Immediate mock assertions triggerAsyncCallback(); expect(mockFn).toHaveBeenCalled(); // ✅ Wait for async operations triggerAsyncCallback(); await waitFor(() => { expect(mockFn).toHaveBeenCalled(); }); // ✅ Use flush promises for immediate async triggerAsyncCallback(); await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); }); expect(mockFn).toHaveBeenCalled(); ``` ### Cypress ```javascript // ❌ Avoid immediate custom assertions cy.click("button"); cy.get("@apiCall").should("have.been.called"); // Custom assertion // ✅ Use Cypress built-in waiting cy.click("button"); cy.get(".result").should("be.visible"); // Automatically retries // ✅ Wait for specific conditions cy.click("button"); cy.wait("@apiCall"); cy.get(".result").should("contain", "Success"); ``` ### Playwright ```javascript // ❌ Immediate assertions await page.click("button"); await expect(page.locator(".result")).toBeVisible(); // ✅ Use Playwright's auto-waiting await page.click("button"); await page.waitForSelector(".result"); await expect(page.locator(".result")).toBeVisible(); // ✅ Or use waitFor with conditions await page.click("button"); await page.waitForFunction( () => document.querySelector(".result")?.textContent === "Expected", ); ``` ## Common Async Operations That Need Waiting ### User Interactions - `click()`, `type()`, `hover()`, `focus()`, `blur()` - Form submissions - Drag and drop operations ### State Changes - Component state updates - Redux/Context state changes - URL navigation changes ### Network Operations - API calls - Resource loading - WebSocket messages ### DOM Updates - Element creation/removal - Class/attribute changes - Style updates ### Component Lifecycle - Component mounting/unmounting - Effect hook execution - Callback execution ## When Not To Use It This rule may not be suitable if: - You're testing purely synchronous operations - You're using test utilities that handle waiting automatically - You have custom waiting mechanisms that the rule doesn't recognize In these cases, you can: ```javascript // Disable for specific lines // eslint-disable-next-line test-flakiness/no-immediate-assertions expect(syncResult).toBe(expected); // Configure allowed operations { "test-flakiness/no-immediate-assertions": ["error", { "allowedAfterOperations": ["render", "syncOperation"] }] } ``` ## Related Rules - [no-unconditional-wait](./no-unconditional-wait.md) - Encourages conditional waiting over timeouts - [await-async-events](./await-async-events.md) - Ensures proper handling of async events - [no-animation-wait](./no-animation-wait.md) - Prevents waiting for unpredictable animations ## Further Reading - [Testing Library - Async Utilities](https://testing-library.com/docs/dom-testing-library/api-async) - [Kent C. Dodds - Fix the "not wrapped in act(...)" warning](https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning) - [Jest - Testing Asynchronous Code](https://jestjs.io/docs/asynchronous) - [Playwright - Auto-waiting](https://playwright.dev/docs/actionability) - [Cypress - Retry-ability](https://docs.cypress.io/guides/core-concepts/retry-ability)