Skip to content

Commit b3f488c

Browse files
author
Mohammod Al Amin Ashik
committed
Add E2E page objects and specs (navigation, dashboard, settings)
1 parent 92afc2d commit b3f488c

8 files changed

Lines changed: 351 additions & 0 deletions

File tree

tests/e2e/pages/DashboardPage.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Page, Locator, expect } from '@playwright/test';
2+
import { BasePage } from './BasePage';
3+
4+
/**
5+
* Dashboard/Home page object
6+
*/
7+
export class DashboardPage extends BasePage {
8+
readonly heading: Locator;
9+
readonly gatewayStatus: Locator;
10+
readonly gatewayToggleButton: Locator;
11+
readonly serverCountCard: Locator;
12+
readonly featureSetsCard: Locator;
13+
readonly clientsCard: Locator;
14+
readonly activeSpaceCard: Locator;
15+
readonly configCopyButton: Locator;
16+
17+
constructor(page: Page) {
18+
super(page);
19+
this.heading = page.getByRole('heading', { name: 'Dashboard' });
20+
this.gatewayStatus = page.locator('text=Gateway:').first();
21+
this.gatewayToggleButton = page.getByRole('button', { name: /Start|Stop/ });
22+
this.serverCountCard = page.locator('text=Servers').first();
23+
this.featureSetsCard = page.locator('text=FeatureSets').first();
24+
this.clientsCard = page.locator('text=Clients').first();
25+
this.activeSpaceCard = page.locator('text=Active Space').first();
26+
this.configCopyButton = page.getByRole('button', { name: /Copy/ });
27+
}
28+
29+
async navigate() {
30+
await this.goto('/');
31+
await this.waitForLoad();
32+
}
33+
34+
async isGatewayRunning(): Promise<boolean> {
35+
const text = await this.gatewayStatus.textContent();
36+
return text?.includes('Running') ?? false;
37+
}
38+
39+
async toggleGateway() {
40+
await this.gatewayToggleButton.click();
41+
// Wait for status to change
42+
await this.page.waitForTimeout(500);
43+
}
44+
45+
async getServerCount(): Promise<string> {
46+
const card = this.page.locator('text=Connected / Installed').locator('..');
47+
const countText = await card.locator('.text-3xl').textContent();
48+
return countText || '0/0';
49+
}
50+
51+
async copyConfig() {
52+
await this.configCopyButton.click();
53+
}
54+
}

tests/e2e/pages/SettingsPage.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Page, Locator } from '@playwright/test';
2+
import { BasePage } from './BasePage';
3+
4+
/**
5+
* Settings page object
6+
*/
7+
export class SettingsPage extends BasePage {
8+
readonly heading: Locator;
9+
readonly lightThemeButton: Locator;
10+
readonly darkThemeButton: Locator;
11+
readonly systemThemeButton: Locator;
12+
readonly openLogsButton: Locator;
13+
readonly logsPath: Locator;
14+
15+
constructor(page: Page) {
16+
super(page);
17+
this.heading = page.getByRole('heading', { name: 'Settings' });
18+
this.lightThemeButton = page.getByRole('button', { name: 'Light' });
19+
this.darkThemeButton = page.getByRole('button', { name: 'Dark' });
20+
this.systemThemeButton = page.getByRole('button', { name: 'System' });
21+
this.openLogsButton = page.getByRole('button', { name: /Open Logs/i });
22+
this.logsPath = page.locator('.font-mono').filter({ hasText: /logs|mcpmux/i });
23+
}
24+
25+
async selectTheme(theme: 'light' | 'dark' | 'system') {
26+
switch (theme) {
27+
case 'light':
28+
await this.lightThemeButton.click();
29+
break;
30+
case 'dark':
31+
await this.darkThemeButton.click();
32+
break;
33+
case 'system':
34+
await this.systemThemeButton.click();
35+
break;
36+
}
37+
}
38+
39+
async getActiveTheme(): Promise<string> {
40+
// Check which button has the primary variant
41+
if (await this.lightThemeButton.getAttribute('class').then(c => c?.includes('primary'))) {
42+
return 'light';
43+
}
44+
if (await this.darkThemeButton.getAttribute('class').then(c => c?.includes('primary'))) {
45+
return 'dark';
46+
}
47+
return 'system';
48+
}
49+
}

tests/e2e/pages/SidebarNav.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Page, Locator } from '@playwright/test';
2+
3+
/**
4+
* Sidebar navigation component
5+
*/
6+
export class SidebarNav {
7+
readonly page: Page;
8+
readonly dashboard: Locator;
9+
readonly myServers: Locator;
10+
readonly discover: Locator;
11+
readonly spaces: Locator;
12+
readonly featureSets: Locator;
13+
readonly clients: Locator;
14+
readonly settings: Locator;
15+
readonly spaceSwitcher: Locator;
16+
readonly themeToggle: Locator;
17+
18+
constructor(page: Page) {
19+
this.page = page;
20+
this.dashboard = page.getByRole('button', { name: 'Dashboard' });
21+
this.myServers = page.getByRole('button', { name: 'My Servers' });
22+
this.discover = page.getByRole('button', { name: 'Discover' });
23+
this.spaces = page.getByRole('button', { name: 'Spaces' });
24+
this.featureSets = page.getByRole('button', { name: 'FeatureSets' });
25+
this.clients = page.getByRole('button', { name: 'Clients' });
26+
this.settings = page.getByRole('button', { name: 'Settings' });
27+
this.spaceSwitcher = page.locator('[data-testid="space-switcher"]');
28+
this.themeToggle = page.locator('button[title*="mode"]');
29+
}
30+
31+
async goToDashboard() {
32+
await this.dashboard.click();
33+
}
34+
35+
async goToMyServers() {
36+
await this.myServers.click();
37+
}
38+
39+
async goToDiscover() {
40+
await this.discover.click();
41+
}
42+
43+
async goToSpaces() {
44+
await this.spaces.click();
45+
}
46+
47+
async goToFeatureSets() {
48+
await this.featureSets.click();
49+
}
50+
51+
async goToClients() {
52+
await this.clients.click();
53+
}
54+
55+
async goToSettings() {
56+
await this.settings.click();
57+
}
58+
59+
async toggleTheme() {
60+
await this.themeToggle.click();
61+
}
62+
}

tests/e2e/pages/SpacesPage.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Page, Locator } from '@playwright/test';
2+
import { BasePage } from './BasePage';
3+
4+
/**
5+
* Spaces management page object
6+
*/
7+
export class SpacesPage extends BasePage {
8+
readonly heading: Locator;
9+
readonly createSpaceButton: Locator;
10+
readonly spaceList: Locator;
11+
readonly spaceCards: Locator;
12+
13+
constructor(page: Page) {
14+
super(page);
15+
this.heading = page.getByRole('heading', { name: 'Spaces' });
16+
this.createSpaceButton = page.getByRole('button', { name: /Create|New Space/i });
17+
this.spaceList = page.locator('[data-testid="space-list"]');
18+
this.spaceCards = page.locator('[data-testid="space-card"]');
19+
}
20+
21+
async getSpaceCount(): Promise<number> {
22+
// Count space cards or list items
23+
const cards = this.page.locator('.space-card, [data-testid="space-card"]');
24+
return await cards.count();
25+
}
26+
27+
async getSpaceByName(name: string): Locator {
28+
return this.page.locator(`text="${name}"`).first();
29+
}
30+
31+
async createSpace(name: string) {
32+
await this.createSpaceButton.click();
33+
// Fill in the space name in the modal/form
34+
const nameInput = this.page.getByPlaceholder(/name/i);
35+
await nameInput.fill(name);
36+
await this.page.getByRole('button', { name: /Create|Save/i }).click();
37+
}
38+
39+
async deleteSpace(name: string) {
40+
const spaceRow = this.page.locator(`text="${name}"`).first().locator('..');
41+
await spaceRow.getByRole('button', { name: /Delete/i }).click();
42+
// Confirm deletion
43+
await this.page.getByRole('button', { name: /Confirm|Yes/i }).click();
44+
}
45+
}

tests/e2e/pages/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
export { BasePage } from './BasePage';
2+
export { DashboardPage } from './DashboardPage';
3+
export { SidebarNav } from './SidebarNav';
4+
export { SpacesPage } from './SpacesPage';
5+
export { SettingsPage } from './SettingsPage';

tests/e2e/specs/dashboard.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { test, expect } from '@playwright/test';
2+
import { DashboardPage } from '../pages';
3+
4+
test.describe('Dashboard', () => {
5+
test('should display gateway status', async ({ page }) => {
6+
const dashboard = new DashboardPage(page);
7+
await dashboard.navigate();
8+
9+
// Gateway status should be visible
10+
await expect(dashboard.gatewayStatus).toBeVisible();
11+
await expect(dashboard.gatewayToggleButton).toBeVisible();
12+
});
13+
14+
test('should display stats cards', async ({ page }) => {
15+
const dashboard = new DashboardPage(page);
16+
await dashboard.navigate();
17+
18+
// All stat cards should be visible
19+
await expect(dashboard.serverCountCard).toBeVisible();
20+
await expect(dashboard.featureSetsCard).toBeVisible();
21+
await expect(dashboard.clientsCard).toBeVisible();
22+
await expect(dashboard.activeSpaceCard).toBeVisible();
23+
});
24+
25+
test('should display client config section', async ({ page }) => {
26+
const dashboard = new DashboardPage(page);
27+
await dashboard.navigate();
28+
29+
// Config section should be visible
30+
await expect(page.locator('text=Connect Your Client')).toBeVisible();
31+
await expect(dashboard.configCopyButton).toBeVisible();
32+
});
33+
34+
test('should copy config to clipboard', async ({ page, context }) => {
35+
// Grant clipboard permissions
36+
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
37+
38+
const dashboard = new DashboardPage(page);
39+
await dashboard.navigate();
40+
41+
await dashboard.copyConfig();
42+
43+
// Check for success message
44+
await expect(page.locator('text=copied')).toBeVisible({ timeout: 2000 });
45+
});
46+
});

tests/e2e/specs/navigation.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { test, expect } from '@playwright/test';
2+
import { DashboardPage, SidebarNav, SettingsPage } from '../pages';
3+
4+
test.describe('Navigation', () => {
5+
test('should load the dashboard on startup', async ({ page }) => {
6+
const dashboard = new DashboardPage(page);
7+
await dashboard.navigate();
8+
9+
await expect(dashboard.heading).toBeVisible();
10+
await expect(page.locator('text=Welcome to McpMux')).toBeVisible();
11+
});
12+
13+
test('should navigate to settings page', async ({ page }) => {
14+
const dashboard = new DashboardPage(page);
15+
const sidebar = new SidebarNav(page);
16+
const settings = new SettingsPage(page);
17+
18+
await dashboard.navigate();
19+
await sidebar.goToSettings();
20+
21+
await expect(settings.heading).toBeVisible();
22+
await expect(page.locator('text=Configure McpMux')).toBeVisible();
23+
});
24+
25+
test('should navigate to all main pages', async ({ page }) => {
26+
const dashboard = new DashboardPage(page);
27+
const sidebar = new SidebarNav(page);
28+
29+
await dashboard.navigate();
30+
31+
// Navigate to each page and verify heading
32+
await sidebar.goToMyServers();
33+
await expect(page.getByRole('heading', { name: /Servers|My Servers/i })).toBeVisible();
34+
35+
await sidebar.goToDiscover();
36+
await expect(page.getByRole('heading', { name: /Discover|Registry/i })).toBeVisible();
37+
38+
await sidebar.goToSpaces();
39+
await expect(page.getByRole('heading', { name: 'Spaces' })).toBeVisible();
40+
41+
await sidebar.goToFeatureSets();
42+
await expect(page.getByRole('heading', { name: /FeatureSets|Feature Sets/i })).toBeVisible();
43+
44+
await sidebar.goToClients();
45+
await expect(page.getByRole('heading', { name: 'Clients' })).toBeVisible();
46+
47+
await sidebar.goToDashboard();
48+
await expect(dashboard.heading).toBeVisible();
49+
});
50+
});

tests/e2e/specs/settings.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { test, expect } from '@playwright/test';
2+
import { DashboardPage, SidebarNav, SettingsPage } from '../pages';
3+
4+
test.describe('Settings', () => {
5+
test.beforeEach(async ({ page }) => {
6+
const dashboard = new DashboardPage(page);
7+
const sidebar = new SidebarNav(page);
8+
9+
await dashboard.navigate();
10+
await sidebar.goToSettings();
11+
});
12+
13+
test('should display appearance settings', async ({ page }) => {
14+
const settings = new SettingsPage(page);
15+
16+
await expect(page.locator('text=Appearance')).toBeVisible();
17+
await expect(settings.lightThemeButton).toBeVisible();
18+
await expect(settings.darkThemeButton).toBeVisible();
19+
await expect(settings.systemThemeButton).toBeVisible();
20+
});
21+
22+
test('should display logs section', async ({ page }) => {
23+
const settings = new SettingsPage(page);
24+
25+
await expect(page.locator('text=Logs')).toBeVisible();
26+
await expect(settings.openLogsButton).toBeVisible();
27+
});
28+
29+
test('should switch between themes', async ({ page }) => {
30+
const settings = new SettingsPage(page);
31+
32+
// Switch to light theme
33+
await settings.selectTheme('light');
34+
// The HTML element should have light theme class
35+
await expect(page.locator('html')).not.toHaveClass(/dark/);
36+
37+
// Switch to dark theme
38+
await settings.selectTheme('dark');
39+
await expect(page.locator('html')).toHaveClass(/dark/);
40+
});
41+
});

0 commit comments

Comments
 (0)