Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 237 additions & 20 deletions playwright/editing-dashboard.spec.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { test, expect, Page } from '@playwright/test';
import { Page, expect, test } from '@playwright/test';
import { disableCookiePrompt } from '@redhat-cloud-services/playwright-test-auth';

const DASHBOARD_HUB_URL = '/dashboard-hub';
const TABLE_SELECTOR = '[data-ouia-component-id="DashboardsTable"]';
import { DASHBOARD_HUB_URL, TABLE_SELECTOR, PAGE_LOAD_TIMEOUT_MS, WIDGET_LOAD_TIMEOUT_MS, deleteTestDashboard } from './helpers';

const navigateToDashboardHub = async (page: Page) => {
await page.goto(DASHBOARD_HUB_URL);
await page.locator(TABLE_SELECTOR).waitFor({ state: 'visible', timeout: 30000 });
await page.locator(TABLE_SELECTOR).waitFor({ state: 'visible', timeout: PAGE_LOAD_TIMEOUT_MS });
await page.locator(TABLE_SELECTOR).locator('a').first().waitFor({ state: 'visible', timeout: WIDGET_LOAD_TIMEOUT_MS });
};

const navigateToGenericDashboard = async (page: Page, dashboardName: string) => {
await navigateToDashboardHub(page);
await page.getByRole('link', { name: dashboardName, exact: true }).click();
await page.getByRole('button', { name: 'Add widgets' }).waitFor({ state: 'visible', timeout: 30000 });
await page.getByRole('button', { name: 'Add widgets' }).waitFor({ state: 'visible', timeout: PAGE_LOAD_TIMEOUT_MS });
};

const openKebabDropdown = async (page: Page) => {
await page.getByRole('button', { name: 'kebab dropdown toggle' }).click();
await page.getByRole('menuitem', { name: 'Set as homepage' }).waitFor({ state: 'visible' });
};


const findDashboardNames = (page: Page) => {
return page.evaluate((selector) => {
const table = document.querySelector(selector);
Expand All @@ -44,19 +44,22 @@ const findDashboardNames = (page: Page) => {
};

const hasHomeIcon = async (page: Page, dashboardName: string) => {
return page.evaluate(({ selector, name }) => {
const table = document.querySelector(selector);
if (!table) return false;
const rows = table.querySelectorAll('tbody tr');
for (const row of rows) {
const link = row.querySelector(':scope > td:nth-child(2) a');
if (link?.textContent === name) {
const firstTd = row.querySelector(':scope > td:first-child');
return !!firstTd?.querySelector('svg');
return page.evaluate(
({ selector, name }) => {
const table = document.querySelector(selector);
if (!table) return false;
const rows = table.querySelectorAll('tbody tr');
for (const row of rows) {
const link = row.querySelector(':scope > td:nth-child(2) a');
if (link?.textContent === name) {
const firstTd = row.querySelector(':scope > td:first-child');
return !!firstTd?.querySelector('svg');
}
}
}
return false;
}, { selector: TABLE_SELECTOR, name: dashboardName });
return false;
},
{ selector: TABLE_SELECTOR, name: dashboardName }
);
};

test.describe('Set Dashboard as Homepage from Generic Page', () => {
Expand Down Expand Up @@ -155,8 +158,8 @@ test.describe('Inline Editing Dashboard Name', () => {
await input.fill(newName);
await page.getByRole('button', { name: 'Confirm name' }).click();

await expect(input).not.toBeVisible({ timeout: 5000 });
await expect(page.locator('h1').filter({ hasText: newName })).toBeVisible({ timeout: 5000 });
await expect(input).not.toBeVisible({ timeout: WIDGET_LOAD_TIMEOUT_MS });
await expect(page.locator('h1').filter({ hasText: newName })).toBeVisible({ timeout: WIDGET_LOAD_TIMEOUT_MS });

await navigateToDashboardHub(page);
await expect(page.getByRole('link', { name: newName })).toBeVisible({ timeout: 10000 });
Expand Down Expand Up @@ -193,3 +196,217 @@ test.describe('Inline Editing Dashboard Name', () => {
await expect(page.locator('h1').filter({ hasText: nonDefaultName })).toBeVisible({ timeout: 5000 });
});
});

test.describe('Dashboard Hub - Page Rendering', () => {
test.beforeEach(async ({ page }) => {
await disableCookiePrompt(page);
});

test('should render the dashboard hub with table and headers', async ({ page }) => {
await navigateToDashboardHub(page);

await expect(page.getByRole('heading', { name: 'Dashboard Hub', level: 1 })).toBeVisible();
await expect(page.getByRole('button', { name: 'Create dashboard' })).toBeVisible();

const table = page.locator(TABLE_SELECTOR);
await expect(table).toBeVisible();

await expect(table.getByRole('columnheader', { name: 'Homepage' })).toBeVisible();
await expect(table.getByRole('columnheader', { name: 'Name' })).toBeVisible();
await expect(table.getByRole('columnheader', { name: 'Last Modified' })).toBeVisible();

// Wait for data rows to load (table renders before data arrives)
const firstLink = table.locator('a').first();
await expect(firstLink).toBeVisible({ timeout: WIDGET_LOAD_TIMEOUT_MS });
});
});

test.describe('Dashboard Hub - Create Blank Dashboard', () => {
test.beforeEach(async ({ page }) => {
await disableCookiePrompt(page);
});

test('should create a blank dashboard and see it in the hub table', async ({ page }) => {
const TEST_NAME = `__e2e_create_${Date.now()}`;
await navigateToDashboardHub(page);

try {
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: 'Create dashboard' }).click();
await page.getByRole('menuitem', { name: 'Create from blank' }).click();

const modal = page.locator('[data-ouia-component-id="CreateBlankDashboardModal"]');
await modal.waitFor({ state: 'visible', timeout: WIDGET_LOAD_TIMEOUT_MS });
await modal.getByPlaceholder('from-scratch dashboard').fill(TEST_NAME);

const createBtn = modal.getByRole('button', { name: 'Create dashboard' });
await expect(createBtn).toBeEnabled({ timeout: WIDGET_LOAD_TIMEOUT_MS });
await createBtn.click();

await page.getByRole('link', { name: TEST_NAME, exact: true }).waitFor({ state: 'visible', timeout: PAGE_LOAD_TIMEOUT_MS });
} finally {
await deleteTestDashboard(page, TEST_NAME);
}
});
});

test.describe('Dashboard Hub - Duplicate Dashboard', () => {
test.beforeEach(async ({ page }) => {
await disableCookiePrompt(page);
});

test('should duplicate an existing dashboard from the hub table', async ({ page }) => {
const DUPLICATE_NAME = `__e2e_duplicate_${Date.now()}`;
await navigateToDashboardHub(page);

// Wait for data rows to load
const table = page.locator(TABLE_SELECTOR);
const firstLink = table.locator('a').first();
await expect(firstLink).toBeVisible({ timeout: WIDGET_LOAD_TIMEOUT_MS });

// Get the Duplicate button from the first row's actions
const firstDuplicateBtn = table.getByRole('button', { name: 'Duplicate' }).first();
await page.waitForLoadState('networkidle');

try {
await firstDuplicateBtn.click();

const modal = page.locator('[data-ouia-component-id="DuplicateDashboardModal"]');
await expect(modal).toBeVisible({ timeout: WIDGET_LOAD_TIMEOUT_MS });

await modal.getByPlaceholder('duplicate dashboard').fill(DUPLICATE_NAME);
const duplicateBtn = modal.getByRole('button', { name: 'Duplicate dashboard' });
await expect(duplicateBtn).toBeEnabled({ timeout: WIDGET_LOAD_TIMEOUT_MS });
await duplicateBtn.click();

// Verify success notification
await page.getByText(`Dashboard '${DUPLICATE_NAME}' duplicated successfully`).waitFor({ state: 'visible', timeout: WIDGET_LOAD_TIMEOUT_MS });

// Table doesn't auto-refresh after duplication — reload to verify
await page.goto(DASHBOARD_HUB_URL);
await page.locator(TABLE_SELECTOR).waitFor({ state: 'visible', timeout: PAGE_LOAD_TIMEOUT_MS });
await expect(page.getByRole('link', { name: DUPLICATE_NAME, exact: true })).toBeVisible({ timeout: WIDGET_LOAD_TIMEOUT_MS });
} finally {
await deleteTestDashboard(page, DUPLICATE_NAME);
}
});
});

test.describe('Dashboard Hub - Delete Dashboard', () => {
test.beforeEach(async ({ page }) => {
await disableCookiePrompt(page);
});

test('should delete a dashboard and see it removed from the hub', async ({ page }) => {
const DELETE_NAME = `__e2e_delete_${Date.now()}`;
await navigateToDashboardHub(page);

// Create a dashboard to delete
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: 'Create dashboard' }).click();
await page.getByRole('menuitem', { name: 'Create from blank' }).click();

const modal = page.locator('[data-ouia-component-id="CreateBlankDashboardModal"]');
await modal.waitFor({ state: 'visible', timeout: WIDGET_LOAD_TIMEOUT_MS });
await modal.getByPlaceholder('from-scratch dashboard').fill(DELETE_NAME);
const createBtn = modal.getByRole('button', { name: 'Create dashboard' });
await expect(createBtn).toBeEnabled({ timeout: WIDGET_LOAD_TIMEOUT_MS });
await createBtn.click();

await page.getByRole('link', { name: DELETE_NAME, exact: true }).waitFor({ state: 'visible', timeout: PAGE_LOAD_TIMEOUT_MS });

// Navigate to the dashboard
await page.getByRole('link', { name: DELETE_NAME, exact: true }).click();
await page.getByRole('button', { name: 'Add widgets' }).waitFor({ state: 'visible', timeout: PAGE_LOAD_TIMEOUT_MS });

// Delete it
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: 'kebab dropdown toggle' }).click();
await page.getByRole('menuitem', { name: 'Delete dashboard' }).click();

await page.getByText(/Deleting the dashboard will remove/).waitFor({ state: 'visible', timeout: WIDGET_LOAD_TIMEOUT_MS });
await page.getByRole('checkbox', { name: /I understand that this action cannot be undone/i }).check();
await page.getByRole('button', { name: 'Delete dashboard' }).click();

// Verify redirected to hub and dashboard is gone
await page.locator(TABLE_SELECTOR).waitFor({ state: 'visible', timeout: PAGE_LOAD_TIMEOUT_MS });
await expect(page.getByRole('link', { name: DELETE_NAME, exact: true })).not.toBeVisible();
});
});

test.describe('Dashboard Hub - Navigation', () => {
test.beforeEach(async ({ page }) => {
await disableCookiePrompt(page);
});

test('should navigate from hub to dashboard and back via breadcrumb', async ({ page }) => {
await navigateToDashboardHub(page);

const table = page.locator(TABLE_SELECTOR);
const firstLink = table.locator('a').first();
await expect(firstLink).toBeVisible({ timeout: WIDGET_LOAD_TIMEOUT_MS });
const dashboardName = await firstLink.textContent();

if (!dashboardName) {
test.skip(true, 'No dashboard found to navigate to');
return;
}

// Navigate to the dashboard
await firstLink.click();
await page.getByRole('button', { name: 'Add widgets' }).waitFor({ state: 'visible', timeout: PAGE_LOAD_TIMEOUT_MS });

// Verify breadcrumb shows dashboard name
const breadcrumb = page.getByRole('navigation', { name: 'Breadcrumb' });
await expect(breadcrumb.getByText(dashboardName)).toBeVisible();
await expect(breadcrumb.getByRole('link', { name: 'Dashboard Hub' })).toBeVisible();

// Navigate back via breadcrumb
await breadcrumb.getByRole('link', { name: 'Dashboard Hub' }).click();

// Verify back on hub
await expect(page.getByRole('heading', { name: 'Dashboard Hub', level: 1 })).toBeVisible({ timeout: PAGE_LOAD_TIMEOUT_MS });
await expect(table).toBeVisible();
});
});

test.describe('Generic Dashboard Page - Rendering', () => {
test.beforeEach(async ({ page }) => {
await disableCookiePrompt(page);
});

test('should render the generic dashboard page with all controls', async ({ page }) => {
await navigateToDashboardHub(page);

const table = page.locator(TABLE_SELECTOR);
const firstLink = table.locator('a').first();
await expect(firstLink).toBeVisible({ timeout: WIDGET_LOAD_TIMEOUT_MS });
const dashboardName = await firstLink.textContent();

if (!dashboardName) {
test.skip(true, 'No dashboard found to navigate to');
return;
}

await navigateToGenericDashboard(page, dashboardName);

// Header controls
await expect(page.locator('h1').filter({ hasText: dashboardName })).toBeVisible();
await expect(page.getByRole('button', { name: 'Edit dashboard name' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Add widgets' })).toBeVisible();
await expect(page.getByRole('button', { name: 'kebab dropdown toggle' })).toBeVisible();

// Breadcrumb
const breadcrumb = page.getByRole('navigation', { name: 'Breadcrumb' });
await expect(breadcrumb.getByRole('link', { name: 'Home' })).toBeVisible();
await expect(breadcrumb.getByRole('link', { name: 'Dashboard Hub' })).toBeVisible();
await expect(breadcrumb.getByText(dashboardName)).toBeVisible();

// Kebab menu items
await openKebabDropdown(page);
await expect(page.getByRole('menuitem', { name: 'Set as homepage' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Duplicate dashboard' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Copy configuration string' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Delete dashboard' })).toBeVisible();
});
});
26 changes: 26 additions & 0 deletions playwright/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Page } from '@playwright/test';

export const DASHBOARD_HUB_URL = '/dashboard-hub';
export const TABLE_SELECTOR = '[data-ouia-component-id="DashboardsTable"]';
export const PAGE_LOAD_TIMEOUT_MS = 30000;
export const WIDGET_LOAD_TIMEOUT_MS = 10000;

export const deleteTestDashboard = async (page: Page, dashboardName: string) => {
await page.goto(DASHBOARD_HUB_URL);
await page.locator(TABLE_SELECTOR).waitFor({ state: 'visible', timeout: PAGE_LOAD_TIMEOUT_MS });
await page.locator(TABLE_SELECTOR).locator('a').first().waitFor({ state: 'visible', timeout: WIDGET_LOAD_TIMEOUT_MS });

const testLink = page.getByRole('link', { name: dashboardName, exact: true });
if (!(await testLink.isVisible())) return;

await testLink.click();
await page.getByRole('button', { name: 'Add widgets' }).waitFor({ state: 'visible', timeout: PAGE_LOAD_TIMEOUT_MS });
await page.waitForLoadState('networkidle');

await page.getByRole('button', { name: 'kebab dropdown toggle' }).click();
await page.getByRole('menuitem', { name: 'Delete dashboard' }).click();

await page.getByText(/Deleting the dashboard will remove/).waitFor({ state: 'visible', timeout: WIDGET_LOAD_TIMEOUT_MS });
await page.getByRole('checkbox', { name: /I understand that this action cannot be undone/i }).check();
await page.getByRole('button', { name: 'Delete dashboard' }).click();
};
Loading
Loading