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(); 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 { 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 { await page.goto(path, { waitUntil: 'domcontentloaded' }); await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null); } async function navigateInApp(page: Page, path: string): Promise { 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 { await expect .poll(async () => (await page.locator('.topbar__tenant-btn').innerText()).toLowerCase()) .toContain(tenantHint.toLowerCase()); }