feat(tests): add comprehensive tree structure tests for various modes
This commit is contained in:
381
tests/e2e/tree-hierarchical.spec.ts
Normal file
381
tests/e2e/tree-hierarchical.spec.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user