174 lines
6.8 KiB
TypeScript
174 lines
6.8 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test';
|
|
|
|
import { type CapturedApiRequest, installMultiTenantSessionFixture } from './support/multi-tenant-session.fixture';
|
|
import { tenantSwitchPageMatrix } from './support/tenant-switch-page-matrix';
|
|
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
test.describe('Multi-tenant switch matrix', () => {
|
|
test('switches tenant from header and persists across primary sections (desktop)', async ({ page }) => {
|
|
const fixture = await installMultiTenantSessionFixture(page);
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
|
|
await go(page, '/mission-control/board');
|
|
await expectTenantLabelContains(page, 'alpha');
|
|
|
|
await switchTenant(page, 'Tenant Bravo');
|
|
await expectTenantLabelContains(page, 'bravo');
|
|
|
|
fixture.clearCapturedApiRequests();
|
|
|
|
for (const entry of tenantSwitchPageMatrix) {
|
|
await navigateInApp(page, entry.route);
|
|
await expect(page.locator('main')).toBeVisible();
|
|
await expectTenantLabelContains(page, 'bravo');
|
|
}
|
|
|
|
const tenantScopedRequests = fixture.capturedApiRequests.filter((request) => {
|
|
const url = request.url.toLowerCase();
|
|
return !url.includes('/api/auth/') &&
|
|
!url.includes('/health') &&
|
|
!url.includes('/ready') &&
|
|
!url.includes('/metrics');
|
|
});
|
|
|
|
expect(tenantScopedRequests.length).toBeGreaterThan(0);
|
|
for (const request of tenantScopedRequests) {
|
|
expect(request.tenantId).toBe('tenant-bravo');
|
|
}
|
|
});
|
|
|
|
test('keeps tenant selector usable and persistent on mobile viewport', async ({ page }) => {
|
|
await installMultiTenantSessionFixture(page);
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
|
|
await go(page, '/mission-control/board');
|
|
await expect(page.locator('.topbar__tenant-btn')).toBeVisible();
|
|
await switchTenant(page, 'Tenant Bravo');
|
|
await expectTenantLabelContains(page, 'bravo');
|
|
|
|
await navigateInApp(page, '/setup/identity-access');
|
|
await expectTenantLabelContains(page, 'bravo');
|
|
|
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
|
|
await expectTenantLabelContains(page, 'bravo');
|
|
});
|
|
|
|
for (const entry of tenantSwitchPageMatrix) {
|
|
test(`applies selected tenant on ${entry.section} route`, async ({ page }) => {
|
|
const fixture = await installMultiTenantSessionFixture(page);
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
|
|
await go(page, '/mission-control/board');
|
|
await switchTenant(page, 'Tenant Bravo');
|
|
await expectTenantLabelContains(page, 'bravo');
|
|
|
|
fixture.clearCapturedApiRequests();
|
|
|
|
await navigateInApp(page, entry.route);
|
|
await expect.poll(() => new URL(page.url()).pathname).toBe(entry.route);
|
|
const routeHints = buildRouteHints(entry.section, entry.expectedBreadcrumb);
|
|
await expect
|
|
.poll(async () => {
|
|
const mainText = (await page.locator('main').innerText()).toLowerCase();
|
|
return routeHints.some((hint) => mainText.includes(hint.toLowerCase()));
|
|
})
|
|
.toBe(true);
|
|
await expectTenantLabelContains(page, 'bravo');
|
|
|
|
const tenantScopedRequests = toTenantScopedRequests(fixture.capturedApiRequests);
|
|
const crossTenantRequests = tenantScopedRequests.filter((request) => request.tenantId && request.tenantId !== 'tenant-bravo');
|
|
expect(crossTenantRequests.length).toBe(0);
|
|
for (const request of tenantScopedRequests) {
|
|
expect(request.tenantId).toBe('tenant-bravo');
|
|
}
|
|
});
|
|
}
|
|
|
|
test('switches tenant in both directions without stale request headers', async ({ page }) => {
|
|
const fixture = await installMultiTenantSessionFixture(page);
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
|
|
await go(page, '/mission-control/board');
|
|
|
|
await switchTenant(page, 'Tenant Bravo');
|
|
await expectTenantLabelContains(page, 'bravo');
|
|
|
|
fixture.clearCapturedApiRequests();
|
|
await navigateInApp(page, '/setup/topology/overview');
|
|
const bravoRequests = toTenantScopedRequests(fixture.capturedApiRequests);
|
|
const crossTenantBravoRequests = bravoRequests.filter((request) => request.tenantId && request.tenantId !== 'tenant-bravo');
|
|
expect(crossTenantBravoRequests.length).toBe(0);
|
|
for (const request of bravoRequests) {
|
|
expect(request.tenantId).toBe('tenant-bravo');
|
|
}
|
|
|
|
await switchTenant(page, 'Tenant Alpha');
|
|
await expectTenantLabelContains(page, 'alpha');
|
|
|
|
fixture.clearCapturedApiRequests();
|
|
await navigateInApp(page, '/security/posture');
|
|
const alphaRequests = toTenantScopedRequests(fixture.capturedApiRequests);
|
|
const crossTenantAlphaRequests = alphaRequests.filter((request) => request.tenantId && request.tenantId !== 'tenant-alpha');
|
|
expect(crossTenantAlphaRequests.length).toBe(0);
|
|
for (const request of alphaRequests) {
|
|
expect(request.tenantId).toBe('tenant-alpha');
|
|
}
|
|
});
|
|
});
|
|
|
|
function toTenantScopedRequests(capturedApiRequests: readonly CapturedApiRequest[]): CapturedApiRequest[] {
|
|
return capturedApiRequests.filter((request) => {
|
|
const url = request.url.toLowerCase();
|
|
return !url.includes('/api/auth/') &&
|
|
!url.includes('/health') &&
|
|
!url.includes('/ready') &&
|
|
!url.includes('/metrics');
|
|
});
|
|
}
|
|
|
|
function buildRouteHints(section: string, expectedBreadcrumb: string): readonly string[] {
|
|
const hints = new Set<string>();
|
|
const sectionHint = section.trim();
|
|
const singularSectionHint = sectionHint.endsWith('s') ? sectionHint.slice(0, -1) : sectionHint;
|
|
hints.add(expectedBreadcrumb.trim());
|
|
hints.add(sectionHint);
|
|
hints.add(singularSectionHint);
|
|
return [...hints].filter((hint) => hint.length > 0);
|
|
}
|
|
|
|
async function switchTenant(page: Page, displayName: string): Promise<void> {
|
|
const trigger = page.locator('.topbar__tenant-btn');
|
|
await trigger.click();
|
|
|
|
const option = page.locator('.topbar__tenant-option-name', {
|
|
hasText: displayName,
|
|
});
|
|
await expect(option).toBeVisible();
|
|
await option.click();
|
|
|
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
|
|
}
|
|
|
|
async function go(page: Page, path: string): Promise<void> {
|
|
await page.goto(path, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
|
|
}
|
|
|
|
async function navigateInApp(page: Page, path: string): Promise<void> {
|
|
await page.evaluate((nextPath) => {
|
|
window.history.pushState({}, '', nextPath);
|
|
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
}, path);
|
|
|
|
await expect.poll(() => new URL(page.url()).pathname).toBe(path);
|
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
|
|
}
|
|
|
|
async function expectTenantLabelContains(page: Page, tenantHint: string): Promise<void> {
|
|
await expect
|
|
.poll(async () => (await page.locator('.topbar__tenant-btn').innerText()).toLowerCase())
|
|
.toContain(tenantHint.toLowerCase());
|
|
}
|