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`) // Wait for the grid root to render await page.waitForSelector('[role="grid"]', { timeout: 10000 }) } // ─── 1. Error Boundary ────────────────────────────────────────────────────── test.describe('Error Boundary', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-error-boundary') }) test('should render grid normally before error', async ({ page }) => { await expect(page.locator('[role="grid"]')).toBeVisible() await expect(page.locator('[role="row"]').first()).toBeVisible() }) test('should show error fallback when error is triggered', async ({ page }) => { await page.locator('button:has-text("Trigger Error")').click() await expect(page.locator('text=Something went wrong rendering the grid.')).toBeVisible({ timeout: 5000 }) await expect(page.locator('text=Intentional render error for testing')).toBeVisible() await expect(page.locator('button:has-text("Retry")')).toBeVisible() }) test('should recover when retry is clicked', async ({ page }) => { await page.locator('button:has-text("Trigger Error")').click() await expect(page.locator('text=Something went wrong rendering the grid.')).toBeVisible({ timeout: 5000 }) await page.locator('button:has-text("Retry")').click() // The error boundary defers state reset via setTimeout to let parent onRetry flush first // Wait for the error message to disappear, then verify grid re-renders await expect(page.locator('text=Something went wrong rendering the grid.')).not.toBeVisible({ timeout: 10000 }) await expect(page.locator('[role="grid"]')).toBeVisible({ timeout: 5000 }) }) }) // ─── 2. Loading States ────────────────────────────────────────────────────── test.describe('Loading States', () => { test('should show skeleton when loading with no data', async ({ page }) => { // Navigate directly - don't wait for grid role since it may show skeleton first await page.goto('/iframe.html?id=components-griddy--with-loading-states&viewMode=story') // Skeleton uses shimmer animation - look for the skeleton bar elements const skeleton = page.locator('[class*="skeleton"]') await expect(skeleton.first()).toBeVisible({ timeout: 5000 }) }) test('should show grid after data loads', async ({ page }) => { await page.goto('/iframe.html?id=components-griddy--with-loading-states&viewMode=story') // Wait for data to load (3s delay in story) await expect(page.locator('[role="row"]').first()).toBeVisible({ timeout: 6000 }) }) test('should show skeleton again after reload', async ({ page }) => { await page.goto('/iframe.html?id=components-griddy--with-loading-states&viewMode=story') // Wait for initial load await expect(page.locator('[role="row"]').first()).toBeVisible({ timeout: 6000 }) // Click reload await page.locator('button:has-text("Reload Data")').click() // Skeleton should appear const skeleton = page.locator('[class*="skeleton"]') await expect(skeleton.first()).toBeVisible({ timeout: 3000 }) // Then data should load again await expect(page.locator('[role="row"]').first()).toBeVisible({ timeout: 5000 }) }) }) // ─── 3. Custom Cell Renderers ─────────────────────────────────────────────── test.describe('Custom Cell Renderers', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-custom-renderers') }) test('should render badge elements for department column', async ({ page }) => { const badges = page.locator('[class*="renderer-badge"]') await expect(badges.first()).toBeVisible({ timeout: 5000 }) expect(await badges.count()).toBeGreaterThan(0) }) test('should render progress bar elements', async ({ page }) => { const progressBars = page.locator('[class*="renderer-progress"]') await expect(progressBars.first()).toBeVisible({ timeout: 5000 }) expect(await progressBars.count()).toBeGreaterThan(0) const innerBar = page.locator('[class*="renderer-progress-bar"]').first() await expect(innerBar).toBeVisible() }) test('should render sparkline SVGs', async ({ page }) => { const sparklines = page.locator('[class*="renderer-sparkline"]') await expect(sparklines.first()).toBeVisible({ timeout: 5000 }) expect(await sparklines.count()).toBeGreaterThan(0) const polyline = page.locator('[class*="renderer-sparkline"] polyline').first() await expect(polyline).toBeVisible() }) }) // ─── 4. Quick Filters ────────────────────────────────────────────────────── test.describe('Quick Filters', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-quick-filters') }) test('should show quick filter dropdown in filter popover', async ({ page }) => { // The Department column has a filter button - click it const departmentHeader = page.locator('[role="columnheader"]').filter({ hasText: 'Department' }) await expect(departmentHeader).toBeVisible({ timeout: 5000 }) // Click the filter icon inside the Department header const filterIcon = departmentHeader.locator('[aria-label="Open column filter"]') await expect(filterIcon).toBeVisible({ timeout: 3000 }) await filterIcon.click() // The popover should show the "Filter: department" text from ColumnFilterPopover await expect(page.locator('text=Filter: department')).toBeVisible({ timeout: 5000 }) // Quick filter section text should also appear await expect(page.getByText('Quick Filter', { exact: true })).toBeVisible({ timeout: 3000 }) }) test('should show unique values as checkboxes', async ({ page }) => { const departmentHeader = page.locator('[role="columnheader"]').filter({ hasText: 'Department' }) await expect(departmentHeader).toBeVisible({ timeout: 5000 }) // Click the filter icon const filterIcon = departmentHeader.locator('[aria-label="Open column filter"]') await filterIcon.click() // Wait for the filter popover to open await expect(page.locator('text=Filter: department')).toBeVisible({ timeout: 5000 }) // The Quick Filter section should have the search values input await expect(page.locator('input[placeholder="Search values..."]')).toBeVisible({ timeout: 3000 }) // Should have checkbox labels for department values (e.g., Design, Engineering, Finance...) const checkboxCount = await page.locator('input[type="checkbox"]').count() expect(checkboxCount).toBeGreaterThan(0) }) }) // ─── 5. Advanced Search ──────────────────────────────────────────────────── test.describe('Advanced Search', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-advanced-search') }) test('should render the advanced search panel', async ({ page }) => { // Look for the panel by its CSS class since "Advanced Search" text also appears in the info box const panel = page.locator('[class*="advanced-search"]') await expect(panel.first()).toBeVisible({ timeout: 5000 }) }) test('should have boolean operator selector', async ({ page }) => { // Use exact text matching for the segmented control labels const segmented = page.locator('.mantine-SegmentedControl-root') await expect(segmented).toBeVisible({ timeout: 5000 }) await expect(page.getByText('AND', { exact: true })).toBeVisible() await expect(page.getByText('OR', { exact: true })).toBeVisible() await expect(page.getByText('NOT', { exact: true })).toBeVisible() }) test('should have a condition row with column, operator, value', async ({ page }) => { await expect(page.locator('input[placeholder="Column"]')).toBeVisible({ timeout: 5000 }) await expect(page.locator('input[placeholder="Value"]')).toBeVisible() }) test('should add a new condition row when clicking Add', async ({ page }) => { const addBtn = page.locator('button:has-text("Add condition")') await expect(addBtn).toBeVisible({ timeout: 5000 }) const initialCount = await page.locator('input[placeholder="Value"]').count() await addBtn.click() const newCount = await page.locator('input[placeholder="Value"]').count() expect(newCount).toBe(initialCount + 1) }) test('should filter data when search is applied', async ({ page }) => { const initialRowCount = await page.locator('[role="row"]').count() await page.locator('input[placeholder="Column"]').click() await page.locator('[role="option"]:has-text("First Name")').click() await page.locator('input[placeholder="Value"]').fill('Alice') await page.locator('button:has-text("Search")').click() await page.waitForTimeout(500) const filteredRowCount = await page.locator('[role="row"]').count() expect(filteredRowCount).toBeLessThan(initialRowCount) }) test('should clear search when Clear is clicked', async ({ page }) => { await page.locator('input[placeholder="Column"]').click() await page.locator('[role="option"]:has-text("First Name")').click() await page.locator('input[placeholder="Value"]').fill('Alice') await page.locator('button:has-text("Search")').click() await page.waitForTimeout(500) const filteredCount = await page.locator('[role="row"]').count() await page.locator('[class*="advanced-search"] button:has-text("Clear")').click() await page.waitForTimeout(500) const clearedCount = await page.locator('[role="row"]').count() expect(clearedCount).toBeGreaterThanOrEqual(filteredCount) }) }) // ─── 6. Filter Presets ───────────────────────────────────────────────────── test.describe('Filter Presets', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-filter-presets') // Clear any existing presets from localStorage await page.evaluate(() => localStorage.removeItem('griddy-filter-presets-storybook-presets')) }) test('should show filter presets button in toolbar', async ({ page }) => { const presetsBtn = page.locator('[aria-label="Filter presets"]') await expect(presetsBtn).toBeVisible({ timeout: 5000 }) }) test('should open presets menu on click', async ({ page }) => { await page.locator('[aria-label="Filter presets"]').click() await expect(page.locator('text=Saved Presets')).toBeVisible({ timeout: 3000 }) await expect(page.locator('text=Save Current Filters')).toBeVisible() }) test('should show empty state when no presets saved', async ({ page }) => { await page.locator('[aria-label="Filter presets"]').click() await expect(page.locator('text=No presets saved')).toBeVisible({ timeout: 3000 }) }) test('should save a preset', async ({ page }) => { await page.locator('[aria-label="Filter presets"]').click() await expect(page.locator('text=Save Current Filters')).toBeVisible({ timeout: 3000 }) await page.locator('input[placeholder="Preset name"]').fill('My Test Preset') // Use a more specific selector to only match the actual Save button, not menu items await page.getByRole('button', { name: 'Save' }).click() // Reopen menu and check preset is listed await page.locator('[aria-label="Filter presets"]').click() await expect(page.locator('text=My Test Preset')).toBeVisible({ timeout: 3000 }) }) }) // ─── 7. Search History ───────────────────────────────────────────────────── test.describe('Search History', () => { test.beforeEach(async ({ page }) => { await gotoStory(page, 'with-search-history') // Clear any existing search history await page.evaluate(() => localStorage.removeItem('griddy-search-history-storybook-search-history')) }) test('should open search overlay with Ctrl+F', async ({ page }) => { const container = page.locator('[class*="griddy-container"]') await container.click() await page.keyboard.press('Control+f') await expect(page.locator('[class*="search-overlay"]')).toBeVisible({ timeout: 3000 }) }) test('should filter grid when typing in search', async ({ page }) => { const container = page.locator('[class*="griddy-container"]') await container.click() await page.keyboard.press('Control+f') const initialRows = await page.locator('[role="row"]').count() const searchInput = page.locator('[aria-label="Search grid"]') await searchInput.fill('Alice') await page.waitForTimeout(500) const filteredRows = await page.locator('[role="row"]').count() expect(filteredRows).toBeLessThan(initialRows) }) test('should close search with Escape', async ({ page }) => { const container = page.locator('[class*="griddy-container"]') await container.click() await page.keyboard.press('Control+f') await expect(page.locator('[class*="search-overlay"]')).toBeVisible({ timeout: 3000 }) await page.keyboard.press('Escape') await expect(page.locator('[class*="search-overlay"]')).not.toBeVisible({ timeout: 3000 }) }) test('should close search with X button', async ({ page }) => { const container = page.locator('[class*="griddy-container"]') await container.click() await page.keyboard.press('Control+f') await expect(page.locator('[class*="search-overlay"]')).toBeVisible({ timeout: 3000 }) await page.locator('[aria-label="Close search"]').click() await expect(page.locator('[class*="search-overlay"]')).not.toBeVisible({ timeout: 3000 }) }) test('should show search history on focus after previous search', async ({ page }) => { const container = page.locator('[class*="griddy-container"]') await container.click() await page.keyboard.press('Control+f') const searchInput = page.locator('[aria-label="Search grid"]') await searchInput.fill('Alice') await page.waitForTimeout(500) // Close and reopen search await page.keyboard.press('Escape') await expect(page.locator('[class*="search-overlay"]')).not.toBeVisible({ timeout: 3000 }) await container.click() await page.keyboard.press('Control+f') await expect(page.locator('[class*="search-overlay"]')).toBeVisible({ timeout: 3000 }) // Focus the input await searchInput.click() // History dropdown should appear await expect(page.locator('text=Recent searches')).toBeVisible({ timeout: 3000 }) await expect(page.locator('[class*="search-history-item"]').first()).toBeVisible() }) })