Skip to content
Open
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
24 changes: 24 additions & 0 deletions frontend/e2e/clients/kubernetes-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,4 +664,28 @@ export default class KubernetesClient {
2_000,
);
}

async createResourceQuota(
name: string,
namespace: string,
spec: k8s.V1ResourceQuotaSpec,
): Promise<void> {
await this.k8sApi.createNamespacedResourceQuota({
namespace,
body: {
metadata: { name, namespace },
spec,
},
});
}

async deleteResourceQuota(name: string, namespace: string): Promise<void> {
try {
await this.k8sApi.deleteNamespacedResourceQuota({ name, namespace });
} catch (err) {
if (!isNotFound(err)) {
throw err;
}
}
}
}
3 changes: 3 additions & 0 deletions frontend/e2e/fixtures/cleanup-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ export function createCleanupFixture(testName: string): CleanupFixture {
case 'Secret':
await client.deleteSecret(resource.name, resource.namespace);
break;
case 'ResourceQuota':
await client.deleteResourceQuota(resource.name, resource.namespace);
break;
default:
console.warn(
`[Cleanup] Unhandled core resource type ${resource.type} "${resource.name}" — skipping`,
Expand Down
61 changes: 60 additions & 1 deletion frontend/e2e/pages/cluster-dashboard-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@ import { expect } from '@playwright/test';
import BasePage from './base-page';

export class ClusterDashboardPage extends BasePage {
private readonly statusCard = this.page.locator('[data-test-id="status-card"]');
private readonly detailsCard = this.page.getByTestId('details-card');
private readonly statusCard = this.page.getByTestId('status-card');
private readonly inventoryCard = this.page.getByTestId('inventory-card');
private readonly utilizationCard = this.page.getByTestId('utilization-card');
private readonly detailItemTitle = this.page.getByTestId('detail-item-title');
private readonly detailItemValue = this.page.getByTestId('detail-item-value');
private readonly viewSettingsLink = this.page.getByTestId('details-card-view-settings');
private readonly viewAlertsLink = this.page.getByTestId('status-card-view-alerts');
private readonly resourceInventoryItem = this.page.getByTestId('resource-inventory-item');
private readonly utilizationItem = this.page.getByTestId('utilization-item');
private readonly utilizationItemTitle = this.page.getByTestId('utilization-item-title');
private readonly durationSelect = this.page.getByTestId('duration-select');
private readonly insightsHealthItem = this.page.locator(
'[data-item-id="Insights-health-item"]',
);
Expand All @@ -26,6 +37,54 @@ export class ClusterDashboardPage extends BasePage {
});
}

getDetailsCard(): Locator {
return this.detailsCard;
}

getStatusCard(): Locator {
return this.statusCard;
}

getInventoryCard(): Locator {
return this.inventoryCard;
}

getUtilizationCard(): Locator {
return this.utilizationCard;
}

getDetailItemTitle(): Locator {
return this.detailItemTitle;
}

getDetailItemValue(): Locator {
return this.detailItemValue;
}

getViewSettingsLink(): Locator {
return this.viewSettingsLink;
}

getViewAlertsLink(): Locator {
return this.viewAlertsLink;
}

getResourceInventoryItem(): Locator {
return this.resourceInventoryItem;
}

getUtilizationItem(): Locator {
return this.utilizationItem;
}

getUtilizationItemTitle(): Locator {
return this.utilizationItemTitle;
}

getDurationSelect(): Locator {
return this.durationSelect;
}

getInsightsHealthItem(): Locator {
return this.insightsHealthItem;
}
Expand Down
95 changes: 95 additions & 0 deletions frontend/e2e/pages/project-dashboard-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { Locator } from '@playwright/test';

import BasePage from './base-page';

export class ProjectDashboardPage extends BasePage {
private readonly detailsCard = this.page.getByTestId('details-card');
private readonly statusCard = this.page.getByTestId('status-card');
private readonly inventoryCard = this.page.getByTestId('inventory-card');
private readonly utilizationCard = this.page.getByTestId('utilization-card');
private readonly launcherCard = this.page.getByTestId('launcher-card');
private readonly resourceQuotasCard = this.page.getByTestId('resource-quotas-card');
private readonly detailItemTitle = this.page.getByTestId('detail-item-title');
private readonly detailItemValue = this.page.getByTestId('detail-item-value');
private readonly viewAllLink = this.page.getByTestId('details-card-view-all');
private readonly projectStatus = this.page.getByTestId('project-status');
private readonly resourceInventoryItem = this.page.getByTestId('resource-inventory-item');
private readonly utilizationItem = this.page.getByTestId('utilization-item');
private readonly utilizationItemTitle = this.page.getByTestId('utilization-item-title');
private readonly durationSelect = this.page.getByTestId('duration-select');
private readonly launcherItem = this.page.getByTestId('launcher-item');
private readonly resourceQuotaLink = this.page.getByTestId('resource-quota-link');
private readonly resourceQuotaGaugeChart = this.page.getByTestId('resource-quota-gauge-chart');

async navigateToProject(projectName: string): Promise<void> {
await this.goTo(`/k8s/cluster/projects/${projectName}`);
}

getDetailsCard(): Locator {
return this.detailsCard;
}

getStatusCard(): Locator {
return this.statusCard;
}

getInventoryCard(): Locator {
return this.inventoryCard;
}

getUtilizationCard(): Locator {
return this.utilizationCard;
}

getLauncherCard(): Locator {
return this.launcherCard;
}

getResourceQuotasCard(): Locator {
return this.resourceQuotasCard;
}

getDetailItemTitle(): Locator {
return this.detailItemTitle;
}

getDetailItemValue(): Locator {
return this.detailItemValue;
}

getViewAllLink(): Locator {
return this.viewAllLink;
}

getProjectStatus(): Locator {
return this.projectStatus;
}

getResourceInventoryItem(): Locator {
return this.resourceInventoryItem;
}

getUtilizationItem(): Locator {
return this.utilizationItem;
}

getUtilizationItemTitle(): Locator {
return this.utilizationItemTitle;
}

getDurationSelect(): Locator {
return this.durationSelect;
}

getLauncherItem(): Locator {
return this.launcherItem;
}

getResourceQuotaLink(): Locator {
return this.resourceQuotaLink;
}

getResourceQuotaGaugeChart(): Locator {
return this.resourceQuotaGaugeChart;
}
}
102 changes: 102 additions & 0 deletions frontend/e2e/tests/console/dashboards/cluster-dashboard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { test, expect } from '../../../fixtures';
import { ClusterDashboardPage } from '../../../pages/cluster-dashboard-page';

test.describe('Cluster Dashboard', { tag: ['@admin', '@smoke'] }, () => {
let dashboard: ClusterDashboardPage;

test.beforeEach(async ({ page }) => {
dashboard = new ClusterDashboardPage(page);
await dashboard.navigateToDashboard();
});

test.describe('Details Card', () => {
test('has all fields populated', async () => {
await expect(dashboard.getDetailsCard()).toBeVisible();

const expectedTitles = [
'Cluster API address',
'Cluster ID',
'Infrastructure provider',
'OpenShift version',
'Service Level Agreement (SLA)',
'Update channel',
];

await expect(dashboard.getDetailItemTitle()).toHaveCount(expectedTitles.length);

for (let i = 0; i < expectedTitles.length; i++) {
await expect(dashboard.getDetailItemTitle().nth(i)).toHaveText(expectedTitles[i]);
}

await expect(dashboard.getDetailItemValue()).toHaveCount(expectedTitles.length);
await expect(dashboard.getDetailItemValue().nth(0)).toContainText('https://');
await expect(dashboard.getDetailItemValue().nth(1)).toContainText('-');
await expect(dashboard.getDetailItemValue().nth(2)).not.toBeEmpty();
await expect(dashboard.getDetailItemValue().nth(3)).toContainText('.');
await expect(dashboard.getDetailItemValue().nth(4)).not.toBeEmpty();
await expect(dashboard.getDetailItemValue().nth(5)).not.toBeEmpty();
});

test('has View settings link', async () => {
await expect(dashboard.getViewSettingsLink()).toBeVisible();
await expect(dashboard.getViewSettingsLink()).toHaveAttribute('href', '/settings/cluster/');
});
});

test.describe('Status Card', () => {
test('has View alerts link', async () => {
await expect(dashboard.getViewAlertsLink()).toBeVisible();
await expect(dashboard.getViewAlertsLink()).toHaveAttribute('href', '/monitoring/alerts');
});

test('has health indicators', async () => {
await dashboard.waitForStatusCardLoaded();
await expect(dashboard.getStatusCard()).toBeVisible();

const expectedTitles = ['Cluster', 'Control Plane', 'Operators', 'Dynamic Plugins'];
for (const title of expectedTitles) {
await expect(dashboard.getStatusCard().getByTestId(title).first()).toContainText(title);
}
});
});

test.describe('Inventory Card', () => {
test('has all items', async () => {
await expect(dashboard.getInventoryCard()).toBeVisible();

const inventoryItems = [
{ title: 'Node', link: '/k8s/cluster/nodes' },
{ title: 'Pod', link: '/k8s/all-namespaces/pods' },
{ title: 'StorageClass', link: '/k8s/cluster/storageclasses' },
{ title: 'PersistentVolumeClaim', link: '/k8s/all-namespaces/persistentvolumeclaims' },
];

for (let i = 0; i < inventoryItems.length; i++) {
await expect(dashboard.getResourceInventoryItem().nth(i)).toContainText(
inventoryItems[i].title,
);
await expect(dashboard.getResourceInventoryItem().nth(i)).toHaveAttribute(
'href',
inventoryItems[i].link,
);
}
});
});

test.describe('Utilization Card', () => {
test('has all items', async () => {
await expect(dashboard.getUtilizationCard()).toBeVisible();

const utilizationItems = ['CPU', 'Memory', 'Filesystem', 'Network transfer', 'Pod count'];
await expect(dashboard.getUtilizationItem()).toHaveCount(utilizationItems.length);

for (let i = 0; i < utilizationItems.length; i++) {
await expect(dashboard.getUtilizationItemTitle().nth(i)).toHaveText(utilizationItems[i]);
}
});

test('has duration dropdown defaulting to 1 hour', async () => {
await expect(dashboard.getDurationSelect()).toContainText('1 hour');
});
});
});
Loading