import { expect, test } from '@playwright/test' import { DOC_PATH, INCIDENT_NAME, MEMORY_NAME, mockNexusApi, PROJECT_ID, PROPOSAL_ID, } from './support/nexusApi' const coreRoutes = [ { path: '/dashboard', heading: 'Live-Orchestrierung' }, { path: '/agents', heading: 'Agents' }, { path: '/agents/iris', heading: 'Iris' }, { path: '/agents/new', heading: 'Agent vorschlagen' }, { path: `/agents/proposals/${PROPOSAL_ID}`, heading: 'Release Sentinel' }, { path: '/projects', heading: 'Projects' }, { path: `/projects/${PROJECT_ID}`, heading: 'Release Readiness' }, { path: '/tasks', heading: 'Aufgaben' }, { path: '/tasks/task-open-1', heading: 'E2E Backlog task' }, { path: '/runs', heading: 'Run Control' }, { path: '/runs/run-e2e-1', heading: 'Run detail' }, { path: '/calendar', heading: 'Calendar' }, { path: '/memory', heading: 'Memory' }, { path: '/docs', heading: 'Docs' }, { path: '/incidents', heading: 'Incidents' }, { path: '/models', heading: 'Models' }, { path: '/activity', heading: 'Activity' }, { path: '/notifications', heading: 'Benachrichtigungen' }, { path: '/security', heading: 'Security Center' }, { path: '/settings', heading: 'Einstellungen' }, ] as const test.describe('authenticated route and deep-link smoke', () => { test('redirects an unauthenticated deep link to login and preserves the target', async ({ page }) => { await mockNexusApi(page, { authenticated: false }) await page.goto(`/agents/proposals/${PROPOSAL_ID}`) await expect(page).toHaveURL(/\/login\?redirect=/) const loginUrl = new URL(page.url()) expect(loginUrl.pathname).toBe('/login') expect(loginUrl.searchParams.get('redirect')).toBe(`/agents/proposals/${PROPOSAL_ID}`) await expect(page.getByRole('button', { name: 'Anmelden' })).toBeVisible() }) test('does not present the rolling login window as a lock while attempts remain', async ({ page }) => { await mockNexusApi(page, { authenticated: false }) await page.route('**/api/v1/auth/login', async route => { await route.fulfill({ status: 401, contentType: 'application/problem+json', headers: { 'X-RateLimit-Remaining': '4', 'X-RateLimit-Reset': String(Math.ceil(Date.now() / 1000) + 60), }, body: JSON.stringify({ message: 'Invalid email or password.', remaining: 4, retryAfterSeconds: 60, }), }) }) await page.goto('/login') await page.getByLabel('E-Mail').fill('release-smoke@example.invalid') await page.getByLabel('Passwort', { exact: true }).fill('not-a-real-password') await page.getByRole('button', { name: 'Anmelden' }).click() await expect(page.getByRole('alert')).toContainText('4 Versuche verbleibend') await expect(page.getByText(/Entsperrt in/)).toHaveCount(0) await expect(page.getByRole('button', { name: 'Anmelden' })).toBeEnabled() await expect(page.getByRole('button', { name: /Gesperrt/ })).toHaveCount(0) }) test('renders the core owner deep links without a client-side exception', async ({ page }) => { test.setTimeout(90_000) await mockNexusApi(page, { role: 'owner' }) const pageErrors: string[] = [] page.on('pageerror', error => pageErrors.push(error.message)) for (const route of coreRoutes) { await page.goto(route.path) await expect(page).toHaveURL(new RegExp(`${route.path.replaceAll('/', '\\/')}$`)) await expect(page.getByRole('heading', { name: route.heading, exact: true }).first()).toBeVisible() } expect(pageErrors).toEqual([]) }) test('links the Projects index to a directly addressable project result', async ({ page }) => { await mockNexusApi(page, { role: 'owner' }) await page.goto('/projects') await expect(page.getByRole('heading', { name: 'Projects', exact: true })).toBeVisible() const projectLink = page.getByRole('link', { name: /Release Readiness/ }) await expect(projectLink).toHaveAttribute('href', `/projects/${PROJECT_ID}`) await projectLink.click() await expect(page).toHaveURL(`/projects/${PROJECT_ID}`) await expect(page.getByRole('heading', { name: 'Release Readiness', exact: true })).toBeVisible() await expect(page.getByRole('link', { name: /E2E Project task/ })).toBeVisible() await expect(page.getByText('E2E Unrelated task', { exact: true })).toHaveCount(0) await expect(page.getByRole('link', { name: /Agent iris/ })).toHaveAttribute( 'href', '/agents/iris', ) await expect(page.getByRole('link', { name: /Run Release readiness verification/, })).toHaveAttribute( 'href', '/runs/33333333-3333-4333-8333-333333333333', ) await expect(page.getByText( /A project reference is not delivered by the current incident contract/, )).toBeVisible() // The detail route is also a durable deep link, not only a client-side // transition from the index. await page.goto(`/projects/${PROJECT_ID}`) await expect(page.getByRole('heading', { name: 'Release Readiness', exact: true })).toBeVisible() }) test('deduplicates the shared OpenClaw overview across dashboard consumers', async ({ page }) => { const api = await mockNexusApi(page, { role: 'owner' }) await page.goto('/dashboard') await expect(page.getByRole('heading', { name: 'Live-Orchestrierung', exact: true, })).toBeVisible() await expect.poll(() => api.openClawOverviewRequestCount).toBe(1) expect(api.browserTelemetryRequestCount).toBe(0) }) test('distinguishes a dependency outage and recovers without hiding the route', async ({ page }, testInfo) => { await mockNexusApi(page, { role: 'owner', apiFailure: { path: '/api/v1/projects', status: 503, // Vue Query performs two bounded retries for safe reads. The visible // recovery action is the next, explicit request. attempts: 3, }, }) await page.goto('/projects') const recovery = page.locator('[data-state="offline"][data-problem-kind="offline"]') await expect(recovery).toBeVisible() await expect(recovery).toContainText('Abhängigkeit nicht erreichbar') await page.screenshot({ path: testInfo.outputPath('dependency-recovery.png'), fullPage: true, }) await recovery.getByRole('button', { name: 'Erneut prüfen' }).click() await expect(page.getByRole('link', { name: /Release Readiness/ })).toBeVisible() await expect(recovery).toHaveCount(0) }) test('keeps Iris and run mutation entry points unavailable to non-owners', async ({ page }) => { await mockNexusApi(page, { role: 'member' }) await page.goto('/dashboard?iris=1') await expect(page).toHaveURL('/dashboard') await expect(page.getByRole('button', { name: 'Iris Chat ist nur für Owner verfügbar', })).toBeDisabled() await expect(page.locator('#iris-chat-dialog')).toHaveCount(0) await page.keyboard.press('Control+k') const commandDialog = page.getByRole('dialog', { name: 'Mission Control commands', }) await expect(commandDialog).toBeVisible() await expect(commandDialog.getByRole('button', { name: /Ask Iris/ })).toHaveCount(0) await expect(commandDialog.getByRole('button', { name: /Start OpenClaw run/ })).toHaveCount(0) await page.keyboard.press('Escape') await page.goto('/tasks?iris=1') await expect(page).toHaveURL('/tasks') await expect(page.getByRole('button', { name: 'Iris chat is available to owners only', })).toBeDisabled() await expect(page.locator('#iris-chat-dialog')).toHaveCount(0) }) test('resolves Activity and Notification results to the linked task', async ({ page }) => { await mockNexusApi(page, { role: 'owner' }) await page.goto('/activity') await page.getByRole('button', { name: /Nexus task deep link is ready/ }).click() const activityDialog = page.getByRole('dialog', { name: 'task.updated' }) const activityTaskLink = activityDialog.getByRole('link', { name: /E2E Backlog task/, }) await expect(activityTaskLink).toHaveAttribute('href', '/tasks/task-open-1') await activityTaskLink.click() await expect(page).toHaveURL('/tasks/task-open-1') await page.goto('/notifications') await page.getByRole('button', { name: /Inspect E2E backlog task/, }).click() await expect(page).toHaveURL('/tasks/task-open-1') await expect(page.getByRole('heading', { level: 1, name: 'E2E Backlog task', })).toBeVisible() }) test('links a Dashboard focus result directly to its task detail', async ({ page }) => { await mockNexusApi(page, { role: 'owner' }) await page.goto('/dashboard') const taskLink = page.getByRole('link', { name: /E2E Backlog task/, }) await expect(taskLink).toHaveAttribute('href', '/tasks/task-open-1') await taskLink.click() await expect(page).toHaveURL('/tasks/task-open-1') await expect(page.getByRole('heading', { level: 1, name: 'E2E Backlog task', })).toBeVisible() }) test('opens OpenClaw content with its Iris source deep link', async ({ page }) => { await mockNexusApi(page, { role: 'owner' }) await page.goto('/docs') await page.getByRole('button', { name: new RegExp(DOC_PATH.split('/').at(-1)!) }).click() const docContent = page.locator('.memory-rendered') await expect(docContent.getByRole('heading', { name: 'Mission Control Runbook', })).toBeVisible() await expect(docContent).toContainText('OpenClaw-backed documentation is visible in Nexus.') await expect(page.locator('.source-context').getByRole('link', { name: 'iris', exact: true, })).toHaveAttribute('href', '/agents/iris') await expect(page.locator('.source-context')).toContainText(DOC_PATH) await page.goto('/memory') await page.getByRole('button', { name: new RegExp(MEMORY_NAME) }).click() const memoryContent = page.locator('.memory-rendered') await expect(memoryContent.getByRole('heading', { name: 'Daily Memory', })).toBeVisible() await expect(memoryContent).toContainText('Agent-first context is loaded from Iris.') await expect(page.locator('.source-context').getByRole('link', { name: 'iris', exact: true, })).toHaveAttribute('href', '/agents/iris') await expect(page.locator('.source-context')).toContainText(`memory/${MEMORY_NAME}`) await page.goto('/incidents') await page.getByRole('button', { name: /Gateway Timeout Recovered/ }).click() const incidentContent = page.locator('.incident-rendered') await expect(incidentContent.getByRole('heading', { name: 'Gateway Timeout Recovered', })).toBeVisible() await expect(incidentContent).toContainText( 'The gateway recovered without duplicate provisioning.', ) await expect(page.locator('.source-context').getByRole('link', { name: 'iris', exact: true, })).toHaveAttribute('href', '/agents/iris') await expect(page.locator('.source-context')).toContainText( `memory/incidents/${INCIDENT_NAME}`, ) }) }) const shellViewports = [ { width: 375, height: 812 }, { width: 768, height: 900 }, { width: 1024, height: 900 }, { width: 1440, height: 900 }, { width: 1920, height: 1080 }, ] for (const viewport of shellViewports) { test(`keeps every registered core route inside the ${viewport.width}px page shell`, async ({ page }) => { test.setTimeout(120_000) await page.setViewportSize(viewport) await mockNexusApi(page, { role: 'owner' }) for (const route of coreRoutes) { await page.goto(route.path) await expect(page.getByRole('heading', { name: route.heading, exact: true }).first()).toBeVisible() const geometry = await page.evaluate(() => ({ viewportWidth: document.documentElement.clientWidth, rootOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth, bodyOverflow: document.body.scrollWidth - document.documentElement.clientWidth, })) expect( geometry.rootOverflow, `${route.path} overflows the root at ${viewport.width}px`, ).toBeLessThanOrEqual(1) expect( geometry.bodyOverflow, `${route.path} overflows the body at ${viewport.width}px`, ).toBeLessThanOrEqual(1) } }) }