feat(tests): add comprehensive tree structure tests for various modes

This commit is contained in:
2026-02-17 00:06:15 +02:00
parent 78468455eb
commit 9ddc960578

View 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,
});
});
});