# no-index-queries Prevent using index-based queries that depend on DOM order and can break easily. ## Rule Details Index-based queries are fragile and can lead to flaky tests: - DOM order can change due to conditional rendering - New elements can be added, shifting existing indexes - Different data sets can affect element positioning - Dynamic content loading can alter DOM structure - CSS order changes can affect visual but not DOM order This rule helps prevent test flakiness by detecting queries that rely on element position rather than semantic meaning. ## Options This rule accepts an options object with the following properties: ```json { "test-flakiness/no-index-queries": [ "error", { "allowNthChild": false, "allowSpecificIndices": [0, -1], "ignoreDataTestId": true } ] } ``` ### `allowNthChild` (default: `false`) When set to `true`, allows CSS selectors with positional queries like `:nth-child()`, `:first-child`, and `:last-child`. ```javascript // With allowNthChild: true const thirdItem = document.querySelector("li:nth-child(3)"); // Allowed const firstButton = document.querySelector("button:first-child"); // Allowed // With allowNthChild: false (default) const thirdItem = document.querySelector("li:nth-child(3)"); // Not allowed const firstButton = document.querySelector("button:first-child"); // Not allowed ``` ### `allowSpecificIndices` (default: `[0, -1]`) An array of specific indices that are allowed when accessing query results. By default, allows first (`0`) and last (`-1`) element access. ```javascript // With allowSpecificIndices: [0, -1] (default) const items = screen.getAllByRole("listitem"); expect(items[0]).toBeInTheDocument(); // Allowed (first element) expect(items[items.length - 1]).toBeInTheDocument(); // Allowed (last element) expect(items[2]).toBeInTheDocument(); // Not allowed // With allowSpecificIndices: [0, 1, 2] const items = screen.getAllByRole("listitem"); expect(items[2]).toBeInTheDocument(); // Now allowed // With allowSpecificIndices: [] const items = screen.getAllByRole("listitem"); expect(items[0]).toBeInTheDocument(); // Not allowed ``` ### `ignoreDataTestId` (default: `true`) When set to `true`, ignores index usage when queries use `data-testid` attributes, since test IDs are typically stable identifiers. ```javascript // With ignoreDataTestId: true (default) const items = screen.getAllByTestId("list-item"); expect(items[0]).toBeInTheDocument(); // Allowed // With ignoreDataTestId: false const items = screen.getAllByTestId("list-item"); expect(items[0]).toBeInTheDocument(); // Not allowed ``` ## Examples ### Incorrect ```javascript // Array index access on DOM queries const buttons = screen.getAllByRole("button"); expect(buttons[0]).toHaveTextContent("Submit"); expect(buttons[1]).toBeDisabled(); // First/last element access const items = screen.getAllByTestId("item"); expect(items[0]).toHaveClass("first"); expect(items[items.length - 1]).toHaveClass("last"); // CSS nth-child selectors expect(screen.getByTestId("list")).toHaveSelector( "li:nth-child(1)", "First Item", ); expect(screen.getByTestId("list")).toHaveSelector( "li:nth-child(2)", "Second Item", ); // Playwright index-based locators await page.locator("button").nth(0).click(); await page.locator("li").nth(2).hover(); // Cypress index-based selections cy.get("button").eq(0).click(); cy.get(".item").first().should("be.visible"); cy.get(".item").last().should("contain", "Last"); // jQuery-style index selection $("button").eq(1).click(); $("li:first").text(); $("li:last").addClass("active"); // Direct array destructuring const [firstButton, secondButton] = screen.getAllByRole("button"); expect(firstButton).toHaveTextContent("Cancel"); // Index-based assertions in loops const items = screen.getAllByRole("listitem"); for (let i = 0; i < items.length; i++) { expect(items[i]).toHaveTextContent(`Item ${i + 1}`); // Fragile } // Table cell access by position const rows = screen.getAllByRole("row"); expect(rows[1].cells[0]).toHaveTextContent("John"); expect(rows[1].cells[1]).toHaveTextContent("Doe"); ``` ### Correct ```javascript // Use semantic queries instead of index expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Cancel" })).toBeDisabled(); // Query by content or attributes expect(screen.getByText("First Item")).toHaveClass("first"); expect(screen.getByText("Last Item")).toHaveClass("last"); // Use more specific selectors expect(screen.getByTestId("submit-button")).toHaveTextContent("Submit"); expect(screen.getByTestId("cancel-button")).toBeDisabled(); // Playwright with semantic locators await page.getByRole("button", { name: "Submit" }).click(); await page.getByText("Menu Item").hover(); // Cypress with content-based queries cy.contains("button", "Submit").click(); cy.get('[data-testid="first-item"]').should("be.visible"); cy.contains("Last Item").should("be.visible"); // Query by ARIA labels and roles expect(screen.getByLabelText("Username")).toBeInTheDocument(); expect(screen.getByRole("navigation")).toContainElement( screen.getByRole("link", { name: "Home" }), ); // Use within() to scope queries const navigation = screen.getByRole("navigation"); expect( within(navigation).getByRole("link", { name: "Home" }), ).toBeInTheDocument(); // Table queries by content const table = screen.getByRole("table"); const johnRow = within(table).getByRole("row", { name: /john/i }); expect(within(johnRow).getByRole("cell", { name: "John" })).toBeInTheDocument(); // Use find queries for dynamic content const firstItem = await screen.findByText("First Item"); expect(firstItem).toBeInTheDocument(); // Content-based assertions const items = screen.getAllByRole("listitem"); expect(items).toHaveLength(3); items.forEach((item, index) => { expect(item).toHaveTextContent(expectedContent[index]); }); ``` ## Best Practices ### 1. Use Semantic Queries Always prefer queries that describe what the element is, not where it is: ```javascript // Instead of position-based const buttons = screen.getAllByRole("button"); await user.click(buttons[0]); // Use semantic meaning await user.click(screen.getByRole("button", { name: "Submit" })); ``` ### 2. Query by User-Visible Content Test what users see, not DOM structure: ```javascript // Instead of DOM position const items = screen.getAllByTestId("item"); expect(items[2]).toBeVisible(); // Use user-visible content expect(screen.getByText("Third Item")).toBeVisible(); ``` ### 3. Use within() for Scoped Queries Use `within()` to query inside specific containers: ```javascript // Instead of assuming order const rows = screen.getAllByRole("row"); expect(rows[1]).toHaveTextContent("John"); // Use scoped queries const table = screen.getByRole("table"); const johnRow = within(table).getByRole("row", { name: /john/i }); expect(johnRow).toBeInTheDocument(); ``` ### 4. Test Collections Semantically When testing collections, focus on content rather than position: ```javascript // Instead of index-based testing const items = screen.getAllByRole("listitem"); expect(items[0]).toHaveTextContent("Apple"); expect(items[1]).toHaveTextContent("Banana"); // Test the collection as a whole expect(screen.getByText("Apple")).toBeInTheDocument(); expect(screen.getByText("Banana")).toBeInTheDocument(); expect(screen.getAllByRole("listitem")).toHaveLength(2); ``` ### 5. Use Data Attributes Wisely Use `data-testid` for elements that are hard to query semantically: ```javascript // When semantic queries are insufficient // Query by test ID instead of position expect(screen.getByTestId('primary-action')).toHaveTextContent('Submit'); expect(screen.getByTestId('secondary-action')).toBeDisabled(); ``` ### 6. Handle Dynamic Lists Properly For dynamic content, test behavior rather than specific positions: ```javascript // Instead of fixed positions const items = screen.getAllByRole("listitem"); expect(items[0]).toHaveTextContent("Most Recent"); // Test the sorting/filtering behavior await user.click(screen.getByRole("button", { name: "Sort by Date" })); expect(screen.getByText("Most Recent")).toBeInTheDocument(); const sortedItems = screen.getAllByRole("listitem"); expect(sortedItems[0]).toHaveAttribute("data-date", mostRecentDate); ``` ## Framework-Specific Examples ### React Testing Library ```javascript // ❌ Index-based queries const buttons = screen.getAllByRole("button"); fireEvent.click(buttons[0]); // ✅ Semantic queries fireEvent.click(screen.getByRole("button", { name: "Submit" })); // ✅ Use within() for complex structures const form = screen.getByRole("form"); const submitButton = within(form).getByRole("button", { name: "Submit" }); ``` ### Cypress ```javascript // ❌ Index-based selectors cy.get("button").eq(0).click(); // ✅ Content-based selectors cy.contains("button", "Submit").click(); cy.get('[data-cy="submit-btn"]').click(); // ✅ Use aliases for complex selectors cy.get('[data-testid="user-list"]').as("userList"); cy.get("@userList").contains("John Doe").click(); ``` ### Playwright ```javascript // ❌ Index-based locators await page.locator("button").nth(0).click(); // ✅ Semantic locators await page.getByRole("button", { name: "Submit" }).click(); await page.getByText("Submit").click(); // ✅ Use page.locator with specific attributes await page.locator('[data-testid="submit-button"]').click(); ``` ## Common Anti-patterns ### Arrays with Index Access ```javascript // ❌ Fragile const items = screen.getAllByRole("listitem"); expect(items[0]).toHaveTextContent("First"); // ✅ Robust expect(screen.getByRole("listitem", { name: "First" })).toBeInTheDocument(); ``` ### CSS nth-child Selectors ```javascript // ❌ Position-dependent cy.get("li:nth-child(1)").should("contain", "First Item"); // ✅ Content-dependent cy.contains("li", "First Item").should("be.visible"); ``` ### First/Last Element Shortcuts ```javascript // ❌ Order-dependent cy.get(".item").first().click(); // ✅ Semantically meaningful cy.get('[data-testid="primary-item"]').click(); ``` ## When Not To Use It This rule may not be suitable if: - You're testing components with guaranteed stable order - You're specifically testing sorting or ordering functionality - You're working with table structures where position is semantically meaningful - You're testing pagination or carousel components In these cases: ```javascript // Disable for specific scenarios // eslint-disable-next-line test-flakiness/no-index-queries expect(sortedItems[0]).toHaveTextContent('A-item'); // Or configure stable containers { "test-flakiness/no-index-queries": ["error", { "allowInStableContainers": true }] } ``` ## Related Rules - [no-long-text-match](./no-long-text-match.md) - Encourages partial text matching - [no-viewport-dependent](./no-viewport-dependent.md) - Prevents viewport-dependent queries - [no-immediate-assertions](./no-immediate-assertions.md) - Prevents assertions without waiting ## Further Reading - [Testing Library - Queries](https://testing-library.com/docs/queries/about) - [Testing Library - Priority](https://testing-library.com/docs/queries/about#priority) - [Playwright - Locators](https://playwright.dev/docs/locators) - [Cypress - Best Practices](https://docs.cypress.io/guides/references/best-practices) - [ARIA - Roles and Properties](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA)