Files
git.stella-ops.org/src/Web/StellaOps.Web/tests/e2e/tenant-switch-matrix.spec.ts

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());
}