import { test, expect } from '@playwright/test'; import { login, navigateTo } from './helpers'; /** * Smoke spec - Documents Hub aggregated view (Wave 11.B rebuild) * * Exercises the structural layout of the new DocumentsHub: * - FolderTreeSidebar with system-root folders (Clients / Companies / Yachts) * - HubRootView sections ("Signing in progress" + "Recent files") * - Expanding a system root to reveal entity sub-folders * - Selecting an entity sub-folder switches to EntityFolderView which * renders the AggregatedSection blocks * * Note: This spec intentionally avoids asserting on seeded file/workflow * counts - those vary by seed state. The integration tests (Tasks 8 + 9) * already verify service correctness; this spec guards UI-level rendering. */ test.describe('Documents hub - aggregated view', () => { test.beforeEach(async ({ page }) => { await login(page, 'super_admin'); }); test('hub root shows Signing-in-progress and Recent-files sections', async ({ page }) => { await navigateTo(page, '/documents'); await page.waitForLoadState('networkidle'); // HubRootView renders two labelled sections. await expect(page.getByText('Signing in progress')).toBeVisible({ timeout: 10_000 }); await expect(page.getByText('Recent files')).toBeVisible({ timeout: 5_000 }); }); test('sidebar shows All-documents and Root pseudo-rows', async ({ page }) => { await navigateTo(page, '/documents'); await page.waitForLoadState('networkidle'); // FolderTreeSidebar always renders these two pseudo-rows at the top. await expect(page.getByRole('button', { name: 'All documents' })).toBeVisible({ timeout: 10_000, }); await expect(page.getByRole('button', { name: 'Root (no folder)' })).toBeVisible({ timeout: 5_000, }); }); test('system-root folders appear in the sidebar with lock icons', async ({ page }) => { await navigateTo(page, '/documents'); await page.waitForLoadState('networkidle'); // ensureSystemRoots creates Clients / Companies / Yachts on port creation. // Each node has systemManaged=true → FolderRow renders a Lock icon beside // the name. We assert text presence; the SVG lock icon is decoration only // and aria-labeled "System folder" by the component. for (const rootName of ['Clients', 'Companies', 'Yachts']) { await expect(page.getByText(rootName)).toBeVisible({ timeout: 10_000 }); } // At least one system-folder lock icon should be in the sidebar. const lockIcons = page.locator('aside [aria-label="System folder"]'); await expect(lockIcons.first()).toBeVisible({ timeout: 5_000 }); }); test('clicking a system root selects it and breadcrumb updates', async ({ page }) => { await navigateTo(page, '/documents'); await page.waitForLoadState('networkidle'); // Click the Clients system root folder button (the label button inside FolderRow). // The chevron button and the label button are siblings - we target the button // that contains the text "Clients". const clientsBtn = page .locator('aside button') .filter({ hasText: /^Clients$/ }) .first(); await expect(clientsBtn).toBeVisible({ timeout: 10_000 }); await clientsBtn.click(); // The breadcrumb (nav[aria-label="breadcrumb"]) should now show "Clients". await expect(page.getByRole('navigation', { name: /breadcrumb/i })).toContainText('Clients', { timeout: 10_000, }); }); test('entity sub-folder view renders Signing-in-progress and Files sections', async ({ page, }) => { // Create a fresh client via the API so we have a guaranteed entity with a // folder. ensureEntityFolder is called by the createClient service hook. // page.request shares the browser context cookie jar (session cookie from // login()) - the bare `request` fixture is an isolated API context that // does not carry the session cookie and would 401. const res = await page.request.post('/api/v1/clients', { data: { firstName: 'HubAgg', lastName: `Smoke${Date.now()}`, email: `hubagg${Date.now()}@e2e.test`, }, }); // A non-2xx here means smoke setup is broken (port cookie / seed) or the // clients API regressed. Fail loud rather than skip green - a silent skip // masked an infra failure for weeks in the audit window. Playwright doesn't // expose vitest's `expect.fail`, so we throw a plain Error which the // runner promotes to a failing test the same way. if (!res.ok()) { throw new Error( `Client create returned ${res.status()} ${await res.text()} - entity sub-folder assertion cannot proceed`, ); } const { data: client } = (await res.json()) as { data: { id: string; firstName: string; lastName: string }; }; // Navigate to the documents hub. await navigateTo(page, '/documents'); await page.waitForLoadState('networkidle'); // Expand the Clients system root by clicking its chevron (Expand button). // The Expand chevron is the first button sibling inside the FolderRow div. // It is aria-labeled "Expand" when collapsed. const expandBtn = page.locator('aside').getByRole('button', { name: 'Expand' }).first(); await expect(expandBtn).toBeVisible({ timeout: 10_000 }); await expandBtn.click(); // The entity folder is named " ". const entityFolderBtn = page .locator('aside button') .filter({ hasText: `${client.firstName} ${client.lastName}` }) .first(); if (!(await entityFolderBtn.isVisible({ timeout: 8_000 }).catch(() => false))) { // ensureEntityFolder may not have run yet (async post-create); reload to // pick up the folder tree refresh. await navigateTo(page, '/documents'); await page.waitForLoadState('networkidle'); // Re-expand. const expandBtn2 = page.locator('aside').getByRole('button', { name: 'Expand' }).first(); await expandBtn2.click(); } await expect(entityFolderBtn).toBeVisible({ timeout: 10_000 }); await entityFolderBtn.click(); // EntityFolderView renders two AggregatedSection blocks with these headings. await expect(page.getByText('Signing in progress')).toBeVisible({ timeout: 10_000 }); await expect(page.getByText('Files')).toBeVisible({ timeout: 5_000 }); // TODO(Task 19 verification): assert SigningDetailsDialog trigger once // a signed-file fixture exists in the smoke seed. The button is only // rendered when files[].signedFromDocumentId is non-null (Task 15 fix-up). const detailsButton = page.getByRole('button', { name: /view signing details/i }); if ((await detailsButton.count()) > 0) { await detailsButton.first().click(); await expect(page.getByRole('dialog', { name: /Signing details/i })).toBeVisible(); } }); });