/** * GitOps workplace: the hub-owned portfolio destination. * * Covers the navigation contract the surface promises (first-class launcher * entry, direct URL), the masthead-led render, the shared filter row * (collapsible search + the Filters popover), and the phone layout. The suite * does not assert portfolio contents: CI starts from a fresh database, so the * meaningful assertions here are structural while row-level classification is * pinned by the backend unit suites. */ import { test, expect } from '@playwright/test'; import { loginAs, waitForShellReady } from './helpers'; test.describe('GitOps workplace', () => { test('is reachable from the launcher as a first-class destination', async ({ page }) => { await loginAs(page); await page.getByRole('button', { name: 'Open navigation launcher' }).click(); const panel = page.getByRole('menu').filter({ has: page.getByText('Navigate', { exact: true }) }); await expect(panel.getByText('GitOps', { exact: true })).toBeVisible({ timeout: 5_000 }); }); test('renders the portfolio at its own URL', async ({ page }) => { await loginAs(page); await page.goto('/nodes/local/gitops'); await waitForShellReady(page); // The filter row is the surface's stable landmark (the masthead's state // word varies with what is in the portfolio). await expect(page.getByRole('button', { name: 'Search applications' })).toBeVisible({ timeout: 10_000 }); await expect(page.getByRole('region', { name: 'GitOps applications' })).toBeVisible({ timeout: 10_000 }); // The count tiles live in the masthead, independent of which rows exist. await expect(page.getByText('APPLICATIONS', { exact: true })).toBeVisible(); }); test('the filters popover narrows the list and resets through the same control', async ({ page }) => { await loginAs(page); await page.goto('/nodes/local/gitops'); await waitForShellReady(page); // Anchored: once a filter is set, the "Clear filters" button also matches a plain name. const filters = page.getByRole('button', { name: /^Filters/ }); await expect(filters).toBeVisible({ timeout: 10_000 }); await filters.click(); const popover = page.locator('[role="dialog"]'); await popover.getByRole('button', { name: 'Attention required' }).click(); await expect(page).toHaveURL(/attention=1/); await expect(filters).toContainText('1'); // Resetting through the same control returns the question to the whole set. await popover.getByRole('group', { name: 'Attention' }).getByRole('button', { name: 'All', exact: true }).click(); await expect(page).not.toHaveURL(/attention=1/); }); test('the search accordion expands, filters, and collapses when cleared', async ({ page }) => { await loginAs(page); await page.goto('/nodes/local/gitops'); await waitForShellReady(page); await page.getByRole('button', { name: 'Search applications' }).click(); const input = page.getByPlaceholder('Search applications...'); await expect(input).toBeVisible(); await input.fill('no-such-application-anywhere'); await expect(page).toHaveURL(/q=no-such-application-anywhere/); // Which empty state shows depends on whether earlier specs left Git // sources behind: an empty portfolio says so, a populated one says the // search matched nothing. Either way the list must be empty. await expect( page.getByText(/No GitOps applications yet|No GitOps application matches the current filters/), ).toBeVisible({ timeout: 10_000 }); await input.fill(''); await input.blur(); await expect(input).toBeHidden(); }); test('the phone layout keeps the workplace readable', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await loginAs(page); await page.goto('/nodes/local/gitops'); // No shell-ready probe here: bespoke phone screens drop the global TopBar, // so the masthead's own kicker is the readiness signal. await expect(page.getByText('GITOPS ยท PORTFOLIO')).toBeVisible({ timeout: 15_000 }); await expect(page.getByPlaceholder('Search applications')).toBeVisible(); await expect(page.getByRole('button', { name: 'Attention', exact: true }).first()).toBeVisible(); }); });