import { expect, test } from '@playwright/test'; // Helper to navigate to a story inside the Storybook iframe async function gotoStory(page: any, storyId: string) { await page.goto(`/iframe.html?id=components-griddy--${storyId}&viewMode=story`); await page.waitForSelector('[role="grid"]', { timeout: 10000 }); } // ─── 1. Basic Rendering ──────────────────────────────────────────────────────── test.describe('Basic Rendering', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'basic'); }); test('renders the grid with rows and columns', async ({ page }) => { await expect(page.locator('[role="grid"]')).toBeVisible(); // Header row should exist const headers = page.locator('[role="columnheader"]'); await expect(headers.first()).toBeVisible(); // Should have all 9 column headers await expect(headers).toHaveCount(9); }); test('renders correct column headers', async ({ page }) => { await expect(page.locator('[role="columnheader"]', { hasText: 'ID' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'First Name' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Last Name' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Email' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Age' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Department' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Salary' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Start Date' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Active' })).toBeVisible(); }); test('renders data rows', async ({ page }) => { // Should render multiple data rows (20 in small dataset) const rows = page.locator('[role="row"]'); // At least header + some data rows const count = await rows.count(); expect(count).toBeGreaterThan(1); }); test('clicking a column header sorts data', async ({ page }) => { const idHeader = page.locator('[role="columnheader"]', { hasText: 'ID' }); await idHeader.click(); // After clicking, should have a sort indicator const sortAttr = await idHeader.getAttribute('aria-sort'); expect(sortAttr).toBeTruthy(); expect(['ascending', 'descending']).toContain(sortAttr); }); test('clicking column header twice reverses sort', async ({ page }) => { const idHeader = page.locator('[role="columnheader"]', { hasText: 'ID' }); await idHeader.click(); const firstSort = await idHeader.getAttribute('aria-sort'); await idHeader.click(); const secondSort = await idHeader.getAttribute('aria-sort'); expect(firstSort).not.toEqual(secondSort); }); }); // ─── 2. Large Dataset (Virtualization) ───────────────────────────────────────── test.describe('Large Dataset', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'large-dataset'); }); test('renders without crashing on 10k rows', async ({ page }) => { await expect(page.locator('[role="grid"]')).toBeVisible(); const rows = page.locator('[role="row"]'); const count = await rows.count(); // Virtualized: should render far fewer rows than 10,000 expect(count).toBeGreaterThan(1); expect(count).toBeLessThan(200); }); test('grid aria-rowcount reflects total data size', async ({ page }) => { const grid = page.locator('[role="grid"]'); const rowCount = await grid.getAttribute('aria-rowcount'); expect(Number(rowCount)).toBe(10000); }); }); // ─── 3. Single Selection ──────────────────────────────────────────────────────── test.describe('Single Selection', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'single-selection'); }); test('renders checkboxes in each row', async ({ page }) => { const checkboxes = page.locator('[role="row"] input[type="checkbox"]'); await expect(checkboxes.first()).toBeVisible({ timeout: 5000 }); const count = await checkboxes.count(); expect(count).toBeGreaterThan(0); }); test('clicking a row selects it', async ({ page }) => { const firstDataRow = page.locator('[role="row"]').nth(1); await firstDataRow.click(); await page.waitForTimeout(300); // Selection state should show in the debug output const selectionText = page.locator('text=Selected:'); await expect(selectionText).toBeVisible(); }); test('clicking another row deselects previous in single mode', async ({ page }) => { // Click first data row const firstDataRow = page.locator('[role="row"]').nth(1); await firstDataRow.click(); await page.waitForTimeout(300); // Click second data row const secondDataRow = page.locator('[role="row"]').nth(2); await secondDataRow.click(); await page.waitForTimeout(300); // In single selection, only one should be selected const checkboxes = page.locator('[role="row"] input[type="checkbox"]:checked'); const checkedCount = await checkboxes.count(); expect(checkedCount).toBeLessThanOrEqual(1); }); }); // ─── 4. Multi Selection ───────────────────────────────────────────────────────── test.describe('Multi Selection', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'multi-selection'); }); test('renders select-all checkbox in header', async ({ page }) => { const selectAllCheckbox = page.locator('[aria-label="Select all rows"]'); await expect(selectAllCheckbox).toBeVisible({ timeout: 5000 }); }); test('selecting multiple rows shows count', async ({ page }) => { // Click first data row await page.locator('[role="row"]').nth(1).click(); await page.waitForTimeout(200); // Shift-click third row to extend selection await page.locator('[role="row"]').nth(3).click({ modifiers: ['Shift'] }); await page.waitForTimeout(300); // The selected count should show in the debug output const countText = page.locator('text=/Selected \\(\\d+ rows\\)/'); await expect(countText).toBeVisible({ timeout: 3000 }); }); test('select-all checkbox selects all rows', async ({ page }) => { const selectAll = page.locator('[aria-label="Select all rows"]'); await selectAll.click(); await page.waitForTimeout(300); // Should show all 20 rows selected await expect(page.locator('text=/Selected \\(20 rows\\)/')).toBeVisible({ timeout: 3000 }); }); }); // ─── 5. Search ────────────────────────────────────────────────────────────────── test.describe('Search', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-search'); }); test('Ctrl+F opens search overlay', async ({ page }) => { await page.locator('[role="grid"] [tabindex="0"]').click(); await page.keyboard.press('Control+f'); await expect(page.locator('[aria-label="Search grid"]')).toBeVisible({ timeout: 3000 }); }); test('search filters rows', async ({ page }) => { const initialRowCount = await page.locator('[role="row"]').count(); await page.locator('[role="grid"] [tabindex="0"]').click(); await page.keyboard.press('Control+f'); const searchInput = page.locator('[aria-label="Search grid"]'); await searchInput.fill('Alice'); await page.waitForTimeout(500); const filteredRowCount = await page.locator('[role="row"]').count(); expect(filteredRowCount).toBeLessThan(initialRowCount); }); test('Escape closes search overlay', async ({ page }) => { await page.locator('[role="grid"] [tabindex="0"]').click(); await page.keyboard.press('Control+f'); await expect(page.locator('[aria-label="Search grid"]')).toBeVisible({ timeout: 3000 }); await page.keyboard.press('Escape'); await expect(page.locator('[aria-label="Search grid"]')).not.toBeVisible({ timeout: 3000 }); }); test('clearing search restores all rows', async ({ page }) => { const initialRowCount = await page.locator('[role="row"]').count(); await page.locator('[role="grid"] [tabindex="0"]').click(); await page.keyboard.press('Control+f'); const searchInput = page.locator('[aria-label="Search grid"]'); await searchInput.fill('Alice'); await page.waitForTimeout(500); // Clear the search await searchInput.fill(''); await page.waitForTimeout(500); const restoredRowCount = await page.locator('[role="row"]').count(); expect(restoredRowCount).toBeGreaterThanOrEqual(initialRowCount); }); }); // ─── 6. Keyboard Navigation ──────────────────────────────────────────────────── test.describe('Keyboard Navigation', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'keyboard-navigation'); }); test('ArrowDown moves focus to next row', async ({ page }) => { // Focus the grid container await page.locator('[role="grid"] [tabindex="0"]').click(); await page.keyboard.press('ArrowDown'); await page.keyboard.press('ArrowDown'); // Grid should still have focus (not lost) await expect(page.locator('[role="grid"]')).toBeVisible(); }); test('Space toggles row selection', async ({ page }) => { await page.locator('[role="grid"] [tabindex="0"]').click(); // Move to first data row await page.keyboard.press('ArrowDown'); await page.keyboard.press('Space'); await page.waitForTimeout(300); // Should show "Selected: 1 rows" in the debug text await expect(page.locator('text=/Selected: \\d+ rows/')).toBeVisible({ timeout: 3000 }); }); test('Ctrl+A selects all rows', async ({ page }) => { await page.locator('[role="grid"] [tabindex="0"]').click(); await page.keyboard.press('Control+a'); await page.waitForTimeout(300); await expect(page.locator('text=/Selected: 20 rows/')).toBeVisible({ timeout: 3000 }); }); test('Ctrl+F opens search from keyboard nav story', async ({ page }) => { await page.locator('[role="grid"] [tabindex="0"]').click(); await page.keyboard.press('Control+f'); await expect(page.locator('[aria-label="Search grid"]')).toBeVisible({ timeout: 3000 }); }); }); // ─── 7. Inline Editing ────────────────────────────────────────────────────────── test.describe('Inline Editing', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-inline-editing'); }); test('double-click on editable cell enters edit mode', async ({ page }) => { // Find a First Name cell (column index 1, since ID is 0) const firstNameCell = page.locator('[role="row"]').nth(1).locator('[role="gridcell"]').nth(1); await firstNameCell.dblclick(); // An input should appear const input = page.locator('[role="row"]').nth(1).locator('input'); await expect(input).toBeVisible({ timeout: 3000 }); }); test('Enter commits the edit', async ({ page }) => { const firstNameCell = page.locator('[role="row"]').nth(1).locator('[role="gridcell"]').nth(1); const originalText = await firstNameCell.innerText(); await firstNameCell.dblclick(); const input = page.locator('[role="row"]').nth(1).locator('input').first(); await expect(input).toBeVisible({ timeout: 3000 }); await input.fill('TestName'); await page.keyboard.press('Enter'); await page.waitForTimeout(300); // The cell text should now show the new value const updatedText = await firstNameCell.innerText(); expect(updatedText).toContain('TestName'); }); test('Escape cancels the edit', async ({ page }) => { const firstNameCell = page.locator('[role="row"]').nth(1).locator('[role="gridcell"]').nth(1); const originalText = await firstNameCell.innerText(); await firstNameCell.dblclick(); const input = page.locator('[role="row"]').nth(1).locator('input').first(); await expect(input).toBeVisible({ timeout: 3000 }); await input.fill('CancelledValue'); await page.keyboard.press('Escape'); await page.waitForTimeout(300); // The cell should revert to original value const restoredText = await firstNameCell.innerText(); expect(restoredText).toBe(originalText); }); }); // ─── 8. Client-Side Pagination ────────────────────────────────────────────────── test.describe('Client-Side Pagination', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-client-side-pagination'); }); test('renders pagination controls', async ({ page }) => { await expect(page.locator('text=Page 1 of')).toBeVisible({ timeout: 5000 }); await expect(page.locator('text=Rows per page:')).toBeVisible(); }); test('shows correct initial page info', async ({ page }) => { // 10,000 rows / 25 per page = 400 pages await expect(page.locator('text=Page 1 of 400')).toBeVisible({ timeout: 5000 }); }); test('next page button navigates forward', async ({ page }) => { await expect(page.locator('text=Page 1 of 400')).toBeVisible({ timeout: 5000 }); // Click the "next page" button (single chevron right) const nextPageBtn = page.locator('[class*="griddy-pagination"] button').nth(2); await nextPageBtn.click(); await page.waitForTimeout(300); await expect(page.locator('text=Page 2 of 400')).toBeVisible(); }); test('last page button navigates to end', async ({ page }) => { await expect(page.locator('text=Page 1 of 400')).toBeVisible({ timeout: 5000 }); // Click the "last page" button (double chevron right) const lastPageBtn = page.locator('[class*="griddy-pagination"] button').nth(3); await lastPageBtn.click(); await page.waitForTimeout(300); await expect(page.locator('text=Page 400 of 400')).toBeVisible(); }); test('first page button is disabled on first page', async ({ page }) => { await expect(page.locator('text=Page 1 of 400')).toBeVisible({ timeout: 5000 }); // First page button (double chevron left) should be disabled const firstPageBtn = page.locator('[class*="griddy-pagination"] button').first(); await expect(firstPageBtn).toBeDisabled(); }); test('page size selector changes rows per page', async ({ page }) => { await expect(page.locator('text=Page 1 of 400')).toBeVisible({ timeout: 5000 }); // Click the page size select (Mantine Select renders as textbox) const pageSizeSelect = page.getByRole('textbox'); await pageSizeSelect.click(); // Select 50 rows per page await page.locator('[role="option"]', { hasText: '50' }).click(); await page.waitForTimeout(300); // 10,000 / 50 = 200 pages await expect(page.locator('text=Page 1 of 200')).toBeVisible({ timeout: 3000 }); }); }); // ─── 9. Server-Side Pagination ────────────────────────────────────────────────── test.describe('Server-Side Pagination', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-server-side-pagination'); }); test('renders data after initial load', async ({ page }) => { // Wait for server-side data to load (300ms delay) await expect(page.locator('text=Displayed Rows: 25')).toBeVisible({ timeout: 5000 }); const rows = page.locator('[role="row"]'); const count = await rows.count(); expect(count).toBeGreaterThan(1); }); test('shows server state info', async ({ page }) => { await expect(page.locator('text=Total Rows: 10000')).toBeVisible({ timeout: 5000 }); await expect(page.locator('text=Displayed Rows: 25')).toBeVisible(); }); test('navigating pages updates server state', async ({ page }) => { await expect(page.locator('text=Current Page: 1')).toBeVisible({ timeout: 5000 }); // Navigate to next page const nextPageBtn = page.locator('[class*="griddy-pagination"] button').nth(2); await nextPageBtn.click(); await expect(page.locator('text=Current Page: 2')).toBeVisible({ timeout: 5000 }); }); }); // ─── 10. Toolbar ──────────────────────────────────────────────────────────────── test.describe('Toolbar', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-toolbar'); }); test('renders export button', async ({ page }) => { const exportBtn = page.locator('[aria-label="Export to CSV"]'); await expect(exportBtn).toBeVisible({ timeout: 5000 }); }); test('renders column toggle button', async ({ page }) => { const columnToggleBtn = page.locator('[aria-label="Toggle columns"]'); await expect(columnToggleBtn).toBeVisible({ timeout: 5000 }); }); test('column toggle menu shows all columns', async ({ page }) => { await page.locator('[aria-label="Toggle columns"]').click(); await expect(page.locator('text=Toggle Columns')).toBeVisible({ timeout: 3000 }); // Should show checkboxes for each column const checkboxes = page.locator('.mantine-Menu-dropdown input[type="checkbox"]'); const count = await checkboxes.count(); expect(count).toBeGreaterThanOrEqual(8); }); test('toggling column visibility hides a column', async ({ page }) => { // Verify "Email" header is initially visible await expect(page.locator('[role="columnheader"]', { hasText: 'Email' })).toBeVisible({ timeout: 5000 }); // Open column toggle menu await page.locator('[aria-label="Toggle columns"]').click(); await expect(page.locator('text=Toggle Columns')).toBeVisible({ timeout: 3000 }); // Uncheck the "Email" column const emailCheckbox = page.locator('.mantine-Checkbox-root', { hasText: 'Email' }).locator('input[type="checkbox"]'); await emailCheckbox.click(); await page.waitForTimeout(300); // Close the menu by clicking elsewhere await page.locator('[role="grid"]').click(); await page.waitForTimeout(300); // Email header should now be hidden await expect(page.locator('[role="columnheader"]', { hasText: 'Email' })).not.toBeVisible(); }); }); // ─── 11. Column Pinning ───────────────────────────────────────────────────────── test.describe('Column Pinning', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-column-pinning'); }); test('renders grid with pinned columns', async ({ page }) => { // ID and First Name should be visible (pinned left) await expect(page.locator('[role="columnheader"]', { hasText: 'ID' })).toBeVisible({ timeout: 5000 }); await expect(page.locator('[role="columnheader"]', { hasText: 'First Name' })).toBeVisible(); // Active should be visible (pinned right) await expect(page.locator('[role="columnheader"]', { hasText: 'Active' })).toBeVisible(); }); test('all column headers render', async ({ page }) => { const headers = page.locator('[role="columnheader"]'); const count = await headers.count(); expect(count).toBeGreaterThanOrEqual(8); }); }); // ─── 12. Header Grouping ──────────────────────────────────────────────────────── test.describe('Header Grouping', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-header-grouping'); }); test('renders group headers', async ({ page }) => { await expect(page.locator('[role="columnheader"]', { hasText: 'Personal Info' })).toBeVisible({ timeout: 5000 }); await expect(page.locator('[role="columnheader"]', { hasText: 'Contact' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Employment' })).toBeVisible(); }); test('renders child column headers under groups', async ({ page }) => { await expect(page.locator('[role="columnheader"]', { hasText: 'First Name' })).toBeVisible({ timeout: 5000 }); await expect(page.locator('[role="columnheader"]', { hasText: 'Last Name' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Age' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Email' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Department' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Salary' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Start Date' })).toBeVisible(); }); test('has multiple header rows', async ({ page }) => { // Should have at least 2 header rows (group + column) const headerRows = page.locator('[role="row"]').filter({ has: page.locator('[role="columnheader"]') }); const count = await headerRows.count(); expect(count).toBeGreaterThanOrEqual(2); }); }); // ─── 13. Data Grouping ────────────────────────────────────────────────────────── test.describe('Data Grouping', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-data-grouping'); }); test('renders grouped rows by department', async ({ page }) => { // Should show department names as group headers await expect(page.locator('[role="row"]', { hasText: 'Engineering' })).toBeVisible({ timeout: 5000 }); await expect(page.locator('[role="row"]', { hasText: 'Marketing' })).toBeVisible(); }); test('expanding a group shows its members', async ({ page }) => { // Click on Engineering group to expand it const engineeringRow = page.locator('[role="row"]', { hasText: 'Engineering' }); await engineeringRow.locator('button').first().click(); await page.waitForTimeout(300); // Should show individual people from Engineering department // From generateData: index 0 is Engineering (Alice Smith) const rows = page.locator('[role="row"]'); const count = await rows.count(); // After expanding one group, should have more rows visible expect(count).toBeGreaterThan(8); // 8 department groups }); }); // ─── 14. Column Reordering ────────────────────────────────────────────────────── test.describe('Column Reordering', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-column-reordering'); }); test('renders all column headers', async ({ page }) => { await expect(page.locator('[role="columnheader"]', { hasText: 'ID' })).toBeVisible({ timeout: 5000 }); await expect(page.locator('[role="columnheader"]', { hasText: 'First Name' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Last Name' })).toBeVisible(); await expect(page.locator('[role="columnheader"]', { hasText: 'Email' })).toBeVisible(); }); test('column headers have draggable attribute', async ({ page }) => { // Column headers should be draggable for reordering const headers = page.locator('[role="columnheader"][draggable="true"]'); const count = await headers.count(); // At least some headers should be draggable (selection column is excluded) expect(count).toBeGreaterThan(0); }); }); // ─── 15. Infinite Scroll ──────────────────────────────────────────────────────── test.describe('Infinite Scroll', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-infinite-scroll'); }); test('renders initial batch of rows', async ({ page }) => { // Initial batch is 50 rows await expect(page.locator('text=Current: 50 rows')).toBeVisible({ timeout: 5000 }); }); test('scrolling to bottom loads more data', async ({ page }) => { await expect(page.locator('text=Current: 50 rows')).toBeVisible({ timeout: 5000 }); // Scroll to bottom of the grid container const container = page.locator('[role="grid"] [tabindex="0"]'); await container.click(); // Use End key to jump to last row, triggering infinite scroll await page.keyboard.press('End'); await page.waitForTimeout(2000); // Wait for the 1000ms load delay + buffer // Should now have more than 50 rows await expect(page.locator('text=Current: 100 rows')).toBeVisible({ timeout: 5000 }); }); }); // ─── 16. Text Filtering ───────────────────────────────────────────────────────── test.describe('Text Filtering', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-text-filtering'); }); test('filterable columns show filter icon', async ({ page }) => { const firstNameHeader = page.locator('[role="columnheader"]').filter({ hasText: 'First Name' }); await expect(firstNameHeader).toBeVisible({ timeout: 5000 }); const filterIcon = firstNameHeader.locator('[aria-label="Open column filter"]'); await expect(filterIcon).toBeVisible({ timeout: 3000 }); }); test('opening filter popover shows text filter UI', async ({ page }) => { const firstNameHeader = page.locator('[role="columnheader"]').filter({ hasText: 'First Name' }); const filterIcon = firstNameHeader.locator('[aria-label="Open column filter"]'); await filterIcon.click(); await expect(page.locator('text=Filter: firstName')).toBeVisible({ timeout: 5000 }); }); test('non-filterable columns have no filter icon', async ({ page }) => { const idHeader = page.locator('[role="columnheader"]').filter({ hasText: 'ID' }); await expect(idHeader).toBeVisible({ timeout: 5000 }); const filterIcon = idHeader.locator('[aria-label="Open column filter"]'); await expect(filterIcon).not.toBeVisible(); }); }); // ─── 17. Number Filtering ─────────────────────────────────────────────────────── test.describe('Number Filtering', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-number-filtering'); }); test('age column has filter icon', async ({ page }) => { const ageHeader = page.locator('[role="columnheader"]').filter({ hasText: 'Age' }); await expect(ageHeader).toBeVisible({ timeout: 5000 }); const filterIcon = ageHeader.locator('[aria-label="Open column filter"]'); await expect(filterIcon).toBeVisible({ timeout: 3000 }); }); test('salary column has filter icon', async ({ page }) => { const salaryHeader = page.locator('[role="columnheader"]').filter({ hasText: 'Salary' }); await expect(salaryHeader).toBeVisible({ timeout: 5000 }); const filterIcon = salaryHeader.locator('[aria-label="Open column filter"]'); await expect(filterIcon).toBeVisible({ timeout: 3000 }); }); test('opening number filter shows numeric filter UI', async ({ page }) => { const ageHeader = page.locator('[role="columnheader"]').filter({ hasText: 'Age' }); const filterIcon = ageHeader.locator('[aria-label="Open column filter"]'); await filterIcon.click(); await expect(page.locator('text=Filter: age')).toBeVisible({ timeout: 5000 }); }); }); // ─── 18. Enum Filtering ───────────────────────────────────────────────────────── test.describe('Enum Filtering', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-enum-filtering'); }); test('department column has filter icon', async ({ page }) => { const deptHeader = page.locator('[role="columnheader"]').filter({ hasText: 'Department' }); await expect(deptHeader).toBeVisible({ timeout: 5000 }); const filterIcon = deptHeader.locator('[aria-label="Open column filter"]'); await expect(filterIcon).toBeVisible({ timeout: 3000 }); }); test('opening enum filter shows the filter popover', async ({ page }) => { const deptHeader = page.locator('[role="columnheader"]').filter({ hasText: 'Department' }); const filterIcon = deptHeader.locator('[aria-label="Open column filter"]'); await filterIcon.click(); await expect(page.locator('text=Filter: department')).toBeVisible({ timeout: 5000 }); }); }); // ─── 19. Boolean Filtering ────────────────────────────────────────────────────── test.describe('Boolean Filtering', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-boolean-filtering'); }); test('active column has filter icon', async ({ page }) => { const activeHeader = page.locator('[role="columnheader"]').filter({ hasText: 'Active' }); await expect(activeHeader).toBeVisible({ timeout: 5000 }); const filterIcon = activeHeader.locator('[aria-label="Open column filter"]'); await expect(filterIcon).toBeVisible({ timeout: 3000 }); }); test('opening boolean filter shows the filter popover', async ({ page }) => { const activeHeader = page.locator('[role="columnheader"]').filter({ hasText: 'Active' }); const filterIcon = activeHeader.locator('[aria-label="Open column filter"]'); await filterIcon.click(); await expect(page.locator('text=Filter: active')).toBeVisible({ timeout: 5000 }); }); }); // ─── 20. Date Filtering ───────────────────────────────────────────────────────── test.describe('Date Filtering', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-date-filtering'); }); test('start date column has filter icon', async ({ page }) => { const dateHeader = page.locator('[role="columnheader"]').filter({ hasText: 'Start Date' }); await expect(dateHeader).toBeVisible({ timeout: 5000 }); const filterIcon = dateHeader.locator('[aria-label="Open column filter"]'); await expect(filterIcon).toBeVisible({ timeout: 3000 }); }); test('opening date filter shows the filter popover', async ({ page }) => { const dateHeader = page.locator('[role="columnheader"]').filter({ hasText: 'Start Date' }); const filterIcon = dateHeader.locator('[aria-label="Open column filter"]'); await filterIcon.click(); await expect(page.locator('text=Filter: startDate')).toBeVisible({ timeout: 5000 }); }); }); // ─── 21. All Filter Types ─────────────────────────────────────────────────────── test.describe('All Filter Types', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-all-filter-types'); }); test('all filterable columns show filter icons', async ({ page }) => { const filterableColumns = ['First Name', 'Last Name', 'Age', 'Department', 'Start Date', 'Active']; for (const colName of filterableColumns) { const header = page.locator('[role="columnheader"]').filter({ hasText: colName }); await expect(header).toBeVisible({ timeout: 5000 }); const filterIcon = header.locator('[aria-label="Open column filter"]'); await expect(filterIcon).toBeVisible({ timeout: 3000 }); } }); test('non-filterable columns have no filter icon', async ({ page }) => { const nonFilterableColumns = ['ID', 'Email', 'Salary']; for (const colName of nonFilterableColumns) { const header = page.locator('[role="columnheader"]').filter({ hasText: colName }); await expect(header).toBeVisible({ timeout: 5000 }); const filterIcon = header.locator('[aria-label="Open column filter"]'); await expect(filterIcon).not.toBeVisible(); } }); }); // ─── 22. Server-Side Filtering & Sorting ──────────────────────────────────────── test.describe('Server-Side Filtering & Sorting', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'server-side-filtering-sorting'); }); test('renders data from simulated server', async ({ page }) => { // Wait for server data to load (300ms simulated delay) await expect(page.locator('text=Loading: false')).toBeVisible({ timeout: 5000 }); const rows = page.locator('[role="row"]'); const count = await rows.count(); expect(count).toBeGreaterThan(1); }); test('shows server-side mode indicator', async ({ page }) => { await expect(page.locator('text=Server-Side Mode:')).toBeVisible({ timeout: 5000 }); }); test('sorting updates server state', async ({ page }) => { await expect(page.locator('[role="grid"]')).toBeVisible({ timeout: 5000 }); // Wait for initial load await page.waitForTimeout(500); // Click First Name header to sort const firstNameHeader = page.locator('[role="columnheader"]', { hasText: 'First Name' }); await firstNameHeader.click(); await page.waitForTimeout(500); // Active Sorting section should now show sorting state await expect(page.locator('text=Active Sorting:')).toBeVisible(); }); }); // ─── 23. Large Dataset with Filtering ─────────────────────────────────────────── test.describe('Large Dataset with Filtering', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'large-dataset-with-filtering'); }); test('renders large dataset with filter columns', async ({ page }) => { await expect(page.locator('[role="grid"]')).toBeVisible({ timeout: 5000 }); const rows = page.locator('[role="row"]'); const count = await rows.count(); expect(count).toBeGreaterThan(1); }); test('all expected filter columns have filter icons', async ({ page }) => { const filterableColumns = ['First Name', 'Last Name', 'Age', 'Department', 'Start Date', 'Active']; for (const colName of filterableColumns) { const header = page.locator('[role="columnheader"]').filter({ hasText: colName }); await expect(header).toBeVisible({ timeout: 5000 }); const filterIcon = header.locator('[aria-label="Open column filter"]'); await expect(filterIcon).toBeVisible({ timeout: 3000 }); } }); });