382 lines
16 KiB
TypeScript
382 lines
16 KiB
TypeScript
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 });
|
|
}
|
|
|
|
// Helper to get all visible row text content (from first gridcell in each row)
|
|
async function getVisibleRowNames(page: any): Promise<string[]> {
|
|
const cells = page.locator('[role="row"] [role="gridcell"]:first-child');
|
|
const count = await cells.count();
|
|
const names: string[] = [];
|
|
for (let i = 0; i < count; i++) {
|
|
const text = await cells.nth(i).innerText();
|
|
names.push(text.trim());
|
|
}
|
|
return names;
|
|
}
|
|
|
|
// Helper to click the expand button within a row that contains the given text
|
|
async function clickExpandButton(page: any, rowText: string) {
|
|
const row = page.locator('[role="row"]', { hasText: rowText });
|
|
const expandBtn = row.locator('button').first();
|
|
await expandBtn.click();
|
|
}
|
|
|
|
// ─── 1. Tree Nested Mode ──────────────────────────────────────────────────────
|
|
|
|
test.describe('Tree Nested Mode', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await gotoStory(page, 'tree-nested-mode');
|
|
});
|
|
|
|
test('renders root-level rows collapsed by default', async ({ page }) => {
|
|
const rows = page.locator('[role="row"]');
|
|
// Header row + 3 root rows (Engineering, Design, Sales)
|
|
await expect(rows).toHaveCount(4);
|
|
|
|
await expect(page.locator('[role="row"]', { hasText: 'Engineering' })).toBeVisible();
|
|
await expect(page.locator('[role="row"]', { hasText: 'Design' })).toBeVisible();
|
|
await expect(page.locator('[role="row"]', { hasText: 'Sales' })).toBeVisible();
|
|
});
|
|
|
|
test('expanding a root node reveals children', async ({ page }) => {
|
|
await clickExpandButton(page, 'Engineering');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Frontend Team' })).toBeVisible();
|
|
await expect(page.locator('[role="row"]', { hasText: 'Backend Team' })).toBeVisible();
|
|
});
|
|
|
|
test('expanding a child node reveals leaf nodes', async ({ page }) => {
|
|
await clickExpandButton(page, 'Engineering');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Frontend Team' })).toBeVisible();
|
|
|
|
await clickExpandButton(page, 'Frontend Team');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Alice Johnson' })).toBeVisible();
|
|
await expect(page.locator('[role="row"]', { hasText: 'Bob Smith' })).toBeVisible();
|
|
});
|
|
|
|
test('collapsing a parent hides all children', async ({ page }) => {
|
|
// Expand Engineering
|
|
await clickExpandButton(page, 'Engineering');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Frontend Team' })).toBeVisible();
|
|
|
|
// Collapse Engineering
|
|
await clickExpandButton(page, 'Engineering');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Frontend Team' })).not.toBeVisible();
|
|
await expect(page.locator('[role="row"]', { hasText: 'Backend Team' })).not.toBeVisible();
|
|
});
|
|
|
|
test('leaf nodes have no expand button', async ({ page }) => {
|
|
await clickExpandButton(page, 'Engineering');
|
|
await clickExpandButton(page, 'Frontend Team');
|
|
|
|
const leafRow = page.locator('[role="row"]', { hasText: 'Alice Johnson' });
|
|
await expect(leafRow).toBeVisible();
|
|
// Leaf nodes render a <span> instead of <button> for the expand area
|
|
const buttons = leafRow.locator('button');
|
|
// Should have no expand button (only possible checkbox button, not tree expand)
|
|
const spanExpand = leafRow.locator('span[style*="cursor: default"]');
|
|
await expect(spanExpand).toBeVisible();
|
|
});
|
|
|
|
test('rows are indented based on depth level', async ({ page }) => {
|
|
await clickExpandButton(page, 'Engineering');
|
|
await clickExpandButton(page, 'Frontend Team');
|
|
|
|
// Get the first gridcell of root vs child vs leaf
|
|
const rootCell = page
|
|
.locator('[role="row"]', { hasText: 'Engineering' })
|
|
.locator('[role="gridcell"]')
|
|
.first();
|
|
const childCell = page
|
|
.locator('[role="row"]', { hasText: 'Frontend Team' })
|
|
.locator('[role="gridcell"]')
|
|
.first();
|
|
const leafCell = page
|
|
.locator('[role="row"]', { hasText: 'Alice Johnson' })
|
|
.locator('[role="gridcell"]')
|
|
.first();
|
|
|
|
const rootPadding = await rootCell.evaluate((el: HTMLElement) =>
|
|
parseInt(getComputedStyle(el).paddingLeft, 10)
|
|
);
|
|
const childPadding = await childCell.evaluate((el: HTMLElement) =>
|
|
parseInt(getComputedStyle(el).paddingLeft, 10)
|
|
);
|
|
const leafPadding = await leafCell.evaluate((el: HTMLElement) =>
|
|
parseInt(getComputedStyle(el).paddingLeft, 10)
|
|
);
|
|
|
|
// Each level should be more indented than the previous
|
|
expect(childPadding).toBeGreaterThan(rootPadding);
|
|
expect(leafPadding).toBeGreaterThan(childPadding);
|
|
});
|
|
});
|
|
|
|
// ─── 2. Tree Flat Mode ────────────────────────────────────────────────────────
|
|
|
|
test.describe('Tree Flat Mode', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await gotoStory(page, 'tree-flat-mode');
|
|
});
|
|
|
|
test('renders root nodes from flat data', async ({ page }) => {
|
|
await expect(page.locator('[role="row"]', { hasText: 'Engineering' })).toBeVisible();
|
|
await expect(page.locator('[role="row"]', { hasText: 'Design' })).toBeVisible();
|
|
});
|
|
|
|
test('expanding shows correctly nested children', async ({ page }) => {
|
|
await clickExpandButton(page, 'Engineering');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Frontend Team' })).toBeVisible();
|
|
await expect(page.locator('[role="row"]', { hasText: 'Backend Team' })).toBeVisible();
|
|
|
|
await clickExpandButton(page, 'Frontend Team');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Alice Johnson' })).toBeVisible();
|
|
await expect(page.locator('[role="row"]', { hasText: 'Bob Smith' })).toBeVisible();
|
|
});
|
|
|
|
test('structure matches expected parent-child relationships', async ({ page }) => {
|
|
await clickExpandButton(page, 'Design');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Product Design' })).toBeVisible();
|
|
|
|
await clickExpandButton(page, 'Product Design');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Frank Miller' })).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ─── 3. Tree Lazy Mode ───────────────────────────────────────────────────────
|
|
|
|
test.describe('Tree Lazy Mode', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await gotoStory(page, 'tree-lazy-mode');
|
|
});
|
|
|
|
test('root nodes render immediately', async ({ page }) => {
|
|
await expect(page.locator('[role="row"]', { hasText: 'Engineering' })).toBeVisible();
|
|
await expect(page.locator('[role="row"]', { hasText: 'Design' })).toBeVisible();
|
|
await expect(page.locator('[role="row"]', { hasText: 'Sales' })).toBeVisible();
|
|
});
|
|
|
|
test('expanding a node shows loading then children', async ({ page }) => {
|
|
await clickExpandButton(page, 'Engineering');
|
|
|
|
// Wait for lazy-loaded children to appear (800ms delay in story)
|
|
await expect(page.locator('[role="row"]', { hasText: 'Engineering - Team A' })).toBeVisible({
|
|
timeout: 5000,
|
|
});
|
|
await expect(page.locator('[role="row"]', { hasText: 'Engineering - Team B' })).toBeVisible();
|
|
});
|
|
|
|
test('lazy-loaded children are expandable', async ({ page }) => {
|
|
await clickExpandButton(page, 'Engineering');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Engineering - Team A' })).toBeVisible({
|
|
timeout: 5000,
|
|
});
|
|
|
|
await clickExpandButton(page, 'Engineering - Team A');
|
|
await expect(
|
|
page.locator('[role="row"]', { hasText: 'Person 1 (Engineering - Team A)' })
|
|
).toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
test('re-collapsing and re-expanding uses cached data', async ({ page }) => {
|
|
// First expand
|
|
await clickExpandButton(page, 'Engineering');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Engineering - Team A' })).toBeVisible({
|
|
timeout: 5000,
|
|
});
|
|
|
|
// Collapse
|
|
await clickExpandButton(page, 'Engineering');
|
|
await expect(
|
|
page.locator('[role="row"]', { hasText: 'Engineering - Team A' })
|
|
).not.toBeVisible();
|
|
|
|
// Re-expand — should appear quickly without loading spinner (cached)
|
|
await clickExpandButton(page, 'Engineering');
|
|
// Children should appear nearly instantly from cache
|
|
await expect(page.locator('[role="row"]', { hasText: 'Engineering - Team A' })).toBeVisible({
|
|
timeout: 1000,
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── 4. Tree with Search Auto-Expand ─────────────────────────────────────────
|
|
|
|
test.describe('Tree with Search Auto-Expand', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await gotoStory(page, 'tree-with-search');
|
|
});
|
|
|
|
test('Ctrl+F opens search overlay', async ({ page }) => {
|
|
await page.keyboard.press('Control+f');
|
|
await expect(page.locator('[aria-label="Search grid"]')).toBeVisible({ timeout: 3000 });
|
|
});
|
|
|
|
test('searching for a leaf node auto-expands ancestors', async ({ page }) => {
|
|
await page.keyboard.press('Control+f');
|
|
const searchInput = page.locator('[aria-label="Search grid"]');
|
|
await searchInput.fill('Alice');
|
|
|
|
// Alice Johnson should become visible (ancestors auto-expanded)
|
|
await expect(page.locator('[role="row"]', { hasText: 'Alice Johnson' })).toBeVisible({
|
|
timeout: 5000,
|
|
});
|
|
// Parent nodes should also be visible
|
|
await expect(page.locator('[role="row"]', { hasText: 'Engineering' })).toBeVisible();
|
|
await expect(page.locator('[role="row"]', { hasText: 'Frontend Team' })).toBeVisible();
|
|
});
|
|
|
|
test('clearing search preserves expanded state', async ({ page }) => {
|
|
await page.keyboard.press('Control+f');
|
|
const searchInput = page.locator('[aria-label="Search grid"]');
|
|
await searchInput.fill('Alice');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Alice Johnson' })).toBeVisible({
|
|
timeout: 5000,
|
|
});
|
|
|
|
// Clear the search
|
|
await searchInput.fill('');
|
|
|
|
// Previously expanded nodes should still be visible
|
|
await expect(page.locator('[role="row"]', { hasText: 'Engineering' })).toBeVisible();
|
|
await expect(page.locator('[role="row"]', { hasText: 'Frontend Team' })).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ─── 5. Tree Custom Icons ─────────────────────────────────────────────────────
|
|
|
|
test.describe('Tree Custom Icons', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await gotoStory(page, 'tree-custom-icons');
|
|
});
|
|
|
|
test('custom expand/collapse icons render', async ({ page }) => {
|
|
// Collapsed nodes should show 📁
|
|
const collapsedRow = page.locator('[role="row"]', { hasText: 'Engineering' });
|
|
await expect(collapsedRow.locator('text=📁')).toBeVisible();
|
|
|
|
// Expand and check for 📂
|
|
await clickExpandButton(page, 'Engineering');
|
|
await expect(collapsedRow.locator('text=📂')).toBeVisible();
|
|
});
|
|
|
|
test('leaf nodes show leaf icon', async ({ page }) => {
|
|
await clickExpandButton(page, 'Engineering');
|
|
await clickExpandButton(page, 'Frontend Team');
|
|
|
|
const leafRow = page.locator('[role="row"]', { hasText: 'Alice Johnson' });
|
|
await expect(leafRow.locator('text=👤')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ─── 6. Tree Deep with MaxDepth ──────────────────────────────────────────────
|
|
|
|
test.describe('Tree Deep with MaxDepth', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await gotoStory(page, 'tree-deep-with-max-depth');
|
|
});
|
|
|
|
test('tree renders with depth limiting', async ({ page }) => {
|
|
await expect(page.locator('[role="row"]', { hasText: 'Level 1 (Root)' })).toBeVisible();
|
|
});
|
|
|
|
test('expanding nodes works up to maxDepth', async ({ page }) => {
|
|
await clickExpandButton(page, 'Level 1 (Root)');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Level 2' })).toBeVisible();
|
|
|
|
await clickExpandButton(page, 'Level 2');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Level 3' })).toBeVisible();
|
|
});
|
|
|
|
test('nodes beyond maxDepth are not rendered', async ({ page }) => {
|
|
await clickExpandButton(page, 'Level 1 (Root)');
|
|
await clickExpandButton(page, 'Level 2');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Level 3' })).toBeVisible();
|
|
|
|
// Level 3 is at depth 2 (0-indexed), maxDepth is 3
|
|
// Level 3 should either not be expandable or Level 4 should not appear
|
|
// Check that Level 4 row doesn't exist after trying to expand Level 3
|
|
const level3Row = page.locator('[role="row"]', { hasText: 'Level 3' });
|
|
const expandBtn = level3Row.locator('button');
|
|
const expandBtnCount = await expandBtn.count();
|
|
|
|
if (expandBtnCount > 0) {
|
|
await expandBtn.first().click();
|
|
// Even after clicking, Level 4 should not appear (beyond maxDepth)
|
|
}
|
|
|
|
// Level 5 Item should never be visible regardless
|
|
await expect(page.locator('[role="row"]', { hasText: 'Level 5 Item' })).not.toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ─── 7. Keyboard Navigation ──────────────────────────────────────────────────
|
|
|
|
test.describe('Tree Keyboard Navigation', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await gotoStory(page, 'tree-nested-mode');
|
|
});
|
|
|
|
test('ArrowRight on collapsed node expands it', async ({ page }) => {
|
|
// Click the Engineering row to focus it
|
|
await page.locator('[role="row"]', { hasText: 'Engineering' }).click();
|
|
await page.keyboard.press('ArrowRight');
|
|
|
|
// Children should now be visible
|
|
await expect(page.locator('[role="row"]', { hasText: 'Frontend Team' })).toBeVisible({
|
|
timeout: 3000,
|
|
});
|
|
});
|
|
|
|
test('ArrowRight on expanded node moves focus to first child', async ({ page }) => {
|
|
// Expand Engineering first
|
|
await clickExpandButton(page, 'Engineering');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Frontend Team' })).toBeVisible();
|
|
|
|
// Click Engineering row to focus it, then ArrowRight to move to first child
|
|
await page.locator('[role="row"]', { hasText: 'Engineering' }).click();
|
|
await page.keyboard.press('ArrowRight');
|
|
|
|
// Frontend Team row should be focused (check via :focus-within or active state)
|
|
// We verify by pressing ArrowRight again which should expand Frontend Team
|
|
await page.keyboard.press('ArrowRight');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Alice Johnson' })).toBeVisible({
|
|
timeout: 3000,
|
|
});
|
|
});
|
|
|
|
test('ArrowLeft on expanded node collapses it', async ({ page }) => {
|
|
// Expand Engineering
|
|
await clickExpandButton(page, 'Engineering');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Frontend Team' })).toBeVisible();
|
|
|
|
// Click Engineering row, then ArrowLeft to collapse
|
|
await page.locator('[role="row"]', { hasText: 'Engineering' }).click();
|
|
await page.keyboard.press('ArrowLeft');
|
|
|
|
await expect(page.locator('[role="row"]', { hasText: 'Frontend Team' })).not.toBeVisible({
|
|
timeout: 3000,
|
|
});
|
|
});
|
|
|
|
test('ArrowLeft on child node moves focus to parent', async ({ page }) => {
|
|
// Expand Engineering
|
|
await clickExpandButton(page, 'Engineering');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Frontend Team' })).toBeVisible();
|
|
|
|
// Focus the child row
|
|
await page.locator('[role="row"]', { hasText: 'Frontend Team' }).click();
|
|
// ArrowLeft on a collapsed child should move focus to parent
|
|
await page.keyboard.press('ArrowLeft');
|
|
|
|
// Verify parent is focused by pressing ArrowLeft again (should collapse Engineering)
|
|
await page.keyboard.press('ArrowLeft');
|
|
await expect(page.locator('[role="row"]', { hasText: 'Frontend Team' })).not.toBeVisible({
|
|
timeout: 3000,
|
|
});
|
|
});
|
|
});
|