.rulesync/commands/docs-e2e-tests.md
Write or extend Playwright example.spec.ts tests for all examples on a documentation page, based on the user's instructions in ${ARGUMENTS}.
The argument should be a doc page name (e.g., aggregation-total-rows) or path. If the user provides a specific example name, focus on that example only.
yarn nx dev). Check node_modules/.cache/ag-watch-status.json for status.npx playwright install --with-deps chromium from documentation/ag-grid-docs/.Find the doc page directory under documentation/ag-grid-docs/src/content/docs/. The argument ${ARGUMENTS} may be:
aggregation-total-rowsdocs/aggregation-total-rowsRead the page's index.mdoc to understand:
{% gridExampleRunner ... %} tags. Each has a title and name attribute. The name maps to a folder in _examples/.List all subdirectories under the page's _examples/ directory. Each subdirectory is one example.
For each example, read:
main.ts — the primary source. Understand:
rowGroup, aggFunc, valueGetter, cellRenderer, etc.)grandTotalRow, groupTotalRow, getRowId, rowSelection, etc.)fetch() URL, or data.ts importaggFunc, valueFormatter, cellRenderer, etc.data.ts (if present) — understand the data shape and sample valuesstyles.css (if present) — any relevant custom stylingindex.html — check for external buttons/controls outside the gridIf the example fetches remote data (e.g., from ag-grid.com/example-assets/), read the corresponding file from documentation/ag-grid-docs/public/example-assets/ to understand the data shape and calculate expected values.
For each example, check if example.spec.ts already exists and whether it is:
PLACEHOLDER comment or only calls ensureGridReady/waitForGridContent/clickAllButtonsagIdFor, expect, etc.Create a plan listing each example that needs a test written or updated. For each example include:
Present this plan to the user before proceeding. Wait for approval.
For each example in the approved plan, use the playwright-expert subagent (via the Task tool) to write the example.spec.ts file. Provide the subagent with:
example.spec.ts files from neighbouring examples as style referenceThe subagent should write the test file and return it. You then write it to disk.
Run each spec with Playwright from the documentation/ag-grid-docs/ directory:
cd documentation/ag-grid-docs && FRAMEWORK=typescript npx playwright test --project=chromium "<example-folder-name>"
Important:
documentation/ag-grid-docs/ directory (Playwright config is there)."aggregation-overview"), NOT a glob pattern with **/ (Playwright treats * as regex).FRAMEWORK=typescript for a quick single-framework check.FRAMEWORK env var.If a test fails, diagnose the failure, fix it, and re-run. Common fixes:
.first() for strict mode violations.toContainText with a shorter substring for decimal values..first() on the locator (see Pitfall 1 in Test Reference).example.spec.ts with meaningful assertions (no placeholders).FRAMEWORK=typescript against chromium.example.spec.ts files for style).This section provides the full reference for writing tests. Include this when delegating to the playwright-expert subagent.
import { expect, test } from '@utils/grid/test-utils';
test.agExample(import.meta, () => {
test.eachFramework('Test Name', async ({ agIdFor, page }) => {
// Test body - runs against all frameworks
});
});
agIdFor Locator HelpersThe agIdFor object wraps AG Grid test IDs into Playwright locators. Key methods:
Rows and Cells:
agIdFor.rowNode(rowId) — locator for a rowagIdFor.cell(rowId, colId) — locator for a cellagIdFor.autoGroupCell(rowId) — shorthand for cell(rowId, 'ag-Grid-AutoColumn')Group Expand/Collapse:
agIdFor.groupContracted(rowId, colId) — the expand icon for a collapsed groupagIdFor.groupExpanded(rowId, colId) — the collapse icon for an expanded groupagIdFor.autoGroupContracted(rowId) — shorthand for auto group columnagIdFor.autoGroupExpanded(rowId) — shorthand for auto group columnHeaders:
agIdFor.headerCell(colId) — header cellagIdFor.headerGroupCell(colId) — header group cellFull API: See packages/ag-grid-community/src/testing/testIdUtils.ts for all available selectors.
AG Grid assigns row IDs based on row type:
| Row Type | ID Pattern | Example |
|---|---|---|
| Data row | 0, 1, 2, ... | agIdFor.cell('0', 'name') |
| Group row | row-group-{field}-{value} | 'row-group-country-Netherlands' |
| Group footer | rowGroupFooter_{groupRowId} | 'rowGroupFooter_row-group-country-Netherlands' |
| Grand total | rowGroupFooter_ROOT_NODE_ID | 'rowGroupFooter_ROOT_NODE_ID' |
Avoid the use of remoteGrid(page). Prefer using agIdFor locators and Playwright page interactions instead.
AG Grid renders rows in multiple viewport containers (pinned left, centre, pinned right, plus sticky rows). Some rows — especially grand total rows and pinned rows — can appear in multiple containers, causing the same test ID to match 2+ elements.
Fix: Use .first() on locators for rows that may be duplicated across containers:
// BAD - may match 2 elements for grand total / pinned rows
await expect(agIdFor.cell('rowGroupFooter_ROOT_NODE_ID', 'bronze')).toContainText('35');
// GOOD - disambiguates
await expect(agIdFor.cell('rowGroupFooter_ROOT_NODE_ID', 'bronze').first()).toContainText('35');
When to use .first(): Always use it for grand total rows (rowGroupFooter_ROOT_NODE_ID) and any pinned rows. Regular group rows and data rows typically don't need it.
Group footer rows display "Total {groupName}" in the auto group column. Grand total rows display just "Total" (with no group name).
// Group footer
await expect(agIdFor.autoGroupCell('rowGroupFooter_row-group-country-Netherlands')).toContainText('Total Netherlands', {
useInnerText: true,
});
// Grand total footer
await expect(agIdFor.autoGroupCell('rowGroupFooter_ROOT_NODE_ID').first()).toContainText('Total', {
useInnerText: true,
});
To expand a collapsed group, click the contracted icon:
await agIdFor.autoGroupContracted('row-group-country-Netherlands').click();
sum: displays the raw number (e.g., '35').avg: the display may be a long decimal (e.g., '1.2580645161290323'). Use toContainText with a stable prefix (e.g., '1.258') rather than matching the full number.count: returns an object whose toString() outputs the count.main.ts to understand the return value.toContainText over toHaveText for RobustnessPrefer toContainText for cell value assertions — it handles partial matching and is more resilient to formatting changes. Use { useInnerText: true } for auto group cells that contain nested elements.
When group rows have blank/empty aggregation cells (e.g., when groupSuppressBlankHeader is not set and a footer row is showing), assert with toHaveText('') for truly empty cells.
Path-based tree data (getDataPath) creates filler nodes for intermediate path segments that have no data entry (e.g., Desktop when only ['Desktop', 'file.txt'] exists in data). These filler nodes have auto-generated row IDs that are not predictable — you cannot use agIdFor helpers for them.
Fix: Use page-level locators to find filler group rows by their displayed group value text:
const findGroupRow = (name: string) =>
page
.locator('.ag-row')
.filter({ has: page.locator('.ag-group-value', { hasText: name }) })
.first();
// Expand/collapse filler nodes via DOM class selectors
await findGroupRow('Desktop').locator('.ag-group-contracted').click(); // expand
await findGroupRow('Desktop').locator('.ag-group-expanded').click(); // collapse
When agIdFor DOES work for tree data: Data rows (leaf nodes) still get sequential IDs ('0', '1', etc.) based on their index in the original data array. Provided group nodes (explicit entries with a path but no leaf data) also get IDs. Self-referential tree data rows use their provided ID field values.
Duplicate group names: Some datasets have the same folder name at multiple paths (e.g., ProjectAlpha under both Desktop and Documents/Work). Always use .first() on the locator or pick uniquely-named groups for assertions.
AG Grid uses virtual scrolling — rows not in the viewport are not in the DOM. Locators will timeout if the target row is off-screen.
Fix: Scroll .ag-body-viewport before asserting. Group assertions by scroll position.
const viewport = page.locator('.ag-body-viewport');
await viewport.evaluate((el) => (el.scrollTop = 600)); // specific position
await viewport.evaluate((el) => (el.scrollTop = el.scrollHeight)); // bottom
When testing scroll-related behaviour (e.g., ensureIndexVisible), the default viewport (1280x720) may be too tall. Shrink it: await page.setViewportSize({ width: 1280, height: 300 });
To verify row selection, check the .ag-row-selected class on the row element. To verify checkbox indeterminate state (partial selection), check .ag-indeterminate on the checkbox wrapper:
// Row selection
await expect(agIdFor.rowNode('0')).toHaveClass(/ag-row-selected/);
await expect(agIdFor.rowNode('1')).not.toHaveClass(/ag-row-selected/);
// Checkbox click on a group row (filler node)
await findGroupRow('Desktop').locator('.ag-checkbox-input').click();
// Indeterminate checkbox (some but not all descendants selected)
const checkbox = findGroupRow('Desktop').locator('.ag-checkbox-input-wrapper');
await expect(checkbox).toHaveClass(/ag-indeterminate/);
expect Does Not Have .poll()The custom expect from @utils/grid/test-utils does not support expect.poll(). Read values after the action settles and assert synchronously:
const scrollAfter = await viewport.evaluate((el) => el.scrollTop);
expect(scrollAfter).toBeGreaterThan(scrollBefore);
test.agExample(import.meta, () => {
test.eachFramework('Example', async ({ agIdFor }) => {
await expect(agIdFor.autoGroupCell('row-group-country-Netherlands')).toContainText('Netherlands (4)', {
useInnerText: true,
});
await expect(agIdFor.cell('row-group-country-Netherlands', 'bronze')).toContainText('4');
await agIdFor.autoGroupContracted('row-group-country-Netherlands').click();
await expect(agIdFor.autoGroupCell('rowGroupFooter_row-group-country-Netherlands')).toContainText(
'Total Netherlands',
{ useInnerText: true }
);
await expect(agIdFor.cell('rowGroupFooter_ROOT_NODE_ID', 'bronze').first()).toContainText('35');
});
});
Combines filler node locators (Pitfall 7), scrolling (Pitfall 8), and cell value assertions on group rows.
test.agExample(import.meta, () => {
test.eachFramework('Example', async ({ agIdFor, page }) => {
const findGroupRow = (name: string) =>
page
.locator('.ag-row')
.filter({ has: page.locator('.ag-group-value', { hasText: name }) })
.first();
// Leaf data rows use agIdFor with data array index
await expect(agIdFor.autoGroupCell('0')).toContainText('Proposal.docx', { useInnerText: true });
// Aggregated values on filler group rows via col-id locator
await expect(findGroupRow('Desktop').locator('[col-id="size"]')).toContainText('1.98 MB');
// Scroll to reach off-screen groups, then assert
const viewport = page.locator('.ag-body-viewport');
await viewport.evaluate((el) => (el.scrollTop = el.scrollHeight));
await expect(findGroupRow('Downloads').locator('[col-id="size"]')).toContainText('4 MB');
// Collapse/expand filler nodes and verify children hide/show
await findGroupRow('Desktop').locator('.ag-group-expanded').click();
await expect(agIdFor.autoGroupCell('0')).not.toBeVisible();
});
});