import { type Page, expect } from '@playwright/test'; export const PORT_SLUG = 'port-nimara'; export const USERS = { super_admin: { email: 'admin@portnimara.test', password: 'SuperAdmin12345!', }, sales_agent: { email: 'agent@portnimara.test', password: 'SalesAgent12345!', }, viewer: { email: 'viewer@portnimara.test', password: 'ViewerUser12345!', }, }; /** * Log in as a specific user via the UI login page. * Waits for the dashboard to load after successful login. */ export async function login(page: Page, role: keyof typeof USERS = 'super_admin') { const user = USERS[role]; await page.goto('/login'); await page.waitForSelector('#email', { state: 'visible' }); await page.fill('#email', user.email); await page.fill('#password', user.password); await page.click('button[type="submit"]'); // Wait for redirect away from /login await page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 15_000, }); } /** * Log out via the topbar user menu. * Falls back to navigating to /login if the logout button isn't found. */ export async function logout(page: Page) { // Try clicking a logout button/link if visible const logoutBtn = page.getByRole('button', { name: /log\s?out|sign\s?out/i }); if (await logoutBtn.isVisible({ timeout: 2000 }).catch(() => false)) { await logoutBtn.click(); await page.waitForURL('**/login**', { timeout: 10_000 }); return; } // Fallback: clear cookies and navigate to login await page.context().clearCookies(); await page.goto('/login'); await page.waitForSelector('#email', { state: 'visible' }); } /** * Navigate to a page within the current port context. */ export async function navigateTo(page: Page, path: string) { const url = `/${PORT_SLUG}${path.startsWith('/') ? path : `/${path}`}`; await page.goto(url); await page.waitForLoadState('networkidle'); } /** * Wait for a toast notification and verify its text. */ export async function expectToast(page: Page, textPattern: string | RegExp) { const toast = page.locator('[data-sonner-toast]').last(); await expect(toast).toBeVisible({ timeout: 10_000 }); if (typeof textPattern === 'string') { await expect(toast).toContainText(textPattern); } else { await expect(toast).toHaveText(textPattern); } } /** * Wait for a sheet (slide-in panel) to be visible. */ export async function waitForSheet(page: Page) { await page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5_000, }); } /** * Resolve the port-nimara port ID via API. * * Used by tests that drive the JSON API directly with `page.request.*`. * The server-side `withAuth` helper resolves port context from the * `X-Port-Id` header (or the user's default-port preference), so any * direct API call outside a port-scoped URL has to set the header. This * caches the lookup per page so the lookup happens once. */ const portIdCache = new WeakMap(); export async function getPortId(page: Page): Promise { const cached = portIdCache.get(page); if (cached) return cached; const res = await page.request.get('/api/v1/admin/ports'); if (!res.ok()) { throw new Error(`Failed to resolve port id: ${res.status()} ${await res.text()}`); } const body = (await res.json()) as { data?: Array<{ id: string; slug: string }> }; const port = body.data?.find((p) => p.slug === PORT_SLUG); if (!port) { throw new Error(`Port ${PORT_SLUG} not in admin ports response`); } portIdCache.set(page, port.id); return port.id; } /** * Build headers for direct JSON-API calls inside tests, including the * `X-Port-Id` that the auth helper requires. */ export async function apiHeaders(page: Page): Promise> { return { 'Content-Type': 'application/json', 'X-Port-Id': await getPortId(page), }; }