import { test, expect } from '@playwright/test'; import { login, navigateTo, apiHeaders } from './helpers'; import path from 'path'; import fs from 'fs'; /** * Smoke spec — Upload a file into an entity folder (Wave 11.B) * * Strategy: Option B (API-fixture approach). * * The EntityFolderView's upload button is not wired into the new hub UI — * the hub focuses on aggregated read + signing. We therefore drive the * upload path via the API directly (`page.request`) and then assert that * the file surfaces in the entity folder view. * * This is the principled approach: it tests the behaviour (file appears in * entity view after upload) rather than a specific UI affordance. */ test.describe('Documents hub — upload into entity folder', () => { test.beforeEach(async ({ page }) => { await login(page, 'super_admin'); }); test('file uploaded with clientId appears in the client entity folder view', async ({ page }) => { const headers = await apiHeaders(page); // 1. Create a client. // 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 clientRes = await page.request.post('/api/v1/clients', { headers, data: { firstName: 'UploadSmoke', lastName: `Test${Date.now()}`, email: `uploadsmoke${Date.now()}@e2e.test`, }, }); // Playwright doesn't expose vitest's `expect.fail`; throw to fail loud. if (!clientRes.ok()) { throw new Error( `Client create returned ${clientRes.status()} ${await clientRes.text()} — upload test cannot proceed`, ); } const { data: client } = (await clientRes.json()) as { data: { id: string; firstName: string; lastName: string }; }; // 2. Upload a file associated with this client via the multipart upload endpoint. // The upload validator accepts `clientId` to associate the file with the client. // ensureEntityFolder is called by the files service after upload to guarantee // the folder exists. const fixturePath = path.resolve('tests/e2e/fixtures/test-document.txt'); const fileBuffer = fs.readFileSync(fixturePath); const uploadForm = new FormData(); uploadForm.append('file', new Blob([fileBuffer], { type: 'text/plain' }), 'test-document.txt'); uploadForm.append('filename', 'test-document.txt'); uploadForm.append('clientId', client.id); // Use fetch directly because Playwright's request doesn't yet have great // multipart support for Buffer payloads. The cookie is carried by the // browser context's cookie jar automatically when we use `page.request`. const uploadRes = await page.request.fetch('/api/v1/files/upload', { method: 'POST', headers: { 'X-Port-Id': headers['X-Port-Id']!, }, multipart: { file: { name: 'test-document.txt', mimeType: 'text/plain', buffer: Buffer.from(fileBuffer), }, filename: 'test-document.txt', clientId: client.id, }, }); expect(uploadRes.ok(), `Upload failed: ${uploadRes.status()} ${await uploadRes.text()}`).toBe( true, ); const uploadBody = (await uploadRes.json()) as { data: { id: string; filename: string } }; const uploadedFileId = uploadBody.data.id; expect(uploadedFileId).toBeTruthy(); // 3. Navigate to the documents hub. await navigateTo(page, '/documents'); await page.waitForLoadState('networkidle'); // 4. Expand the Clients system root. const expandBtn = page.locator('aside').getByRole('button', { name: 'Expand' }).first(); await expect(expandBtn).toBeVisible({ timeout: 10_000 }); await expandBtn.click(); // 5. Click the entity folder for the client we created. const entityFolderBtn = page .locator('aside button') .filter({ hasText: `${client.firstName} ${client.lastName}` }) .first(); await expect(entityFolderBtn).toBeVisible({ timeout: 10_000 }); await entityFolderBtn.click(); // 6. The EntityFolderView should show the Files section with at least one row. const filesSection = page.getByText('Files').first(); await expect(filesSection).toBeVisible({ timeout: 10_000 }); // The file we uploaded should appear somewhere in the entity view. // AggregatedSection renders filenames in elements inside
  • rows. await expect(page.getByText('test-document.txt')).toBeVisible({ timeout: 10_000 }); }); test('file uploaded with folderId (entity folder) auto-sets client_id', async ({ page }) => { const headers = await apiHeaders(page); // 1. Create a client — ensureEntityFolder is triggered server-side when // the first file is uploaded with clientId or directly by the create 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 clientRes = await page.request.post('/api/v1/clients', { headers, data: { firstName: 'FolderIdSmoke', lastName: `Test${Date.now()}`, email: `folderidsmoke${Date.now()}@e2e.test`, }, }); if (!clientRes.ok()) { throw new Error( `Client create returned ${clientRes.status()} ${await clientRes.text()} — folderId test cannot proceed`, ); } const { data: client } = (await clientRes.json()) as { data: { id: string; firstName: string; lastName: string }; }; // 2. Obtain the entity folder id for this client by uploading once with // clientId to trigger ensureEntityFolder, then listing files. const seedUpload = await page.request.fetch('/api/v1/files/upload', { method: 'POST', headers: { 'X-Port-Id': headers['X-Port-Id']! }, multipart: { file: { name: 'seed.txt', mimeType: 'text/plain', buffer: Buffer.from('seed'), }, filename: 'seed.txt', clientId: client.id, }, }); // Seed upload failing means the files API is broken — fail loud so the // infra regression surfaces in CI instead of staying green-skipped. if (!seedUpload.ok()) { throw new Error( `Seed upload returned ${seedUpload.status()} ${await seedUpload.text()} — folderId test cannot proceed`, ); } // 3. List files for this client to discover the folder id. const listRes = await page.request.get( `/api/v1/files?entityType=client&entityId=${client.id}`, { headers, }, ); if (!listRes.ok()) { throw new Error( `File list returned ${listRes.status()} ${await listRes.text()} — folderId test cannot proceed`, ); } // 4. Navigate and verify — folder view shows the client entity sections. await navigateTo(page, '/documents'); await page.waitForLoadState('networkidle'); // Expand Clients and click entity folder. const expandBtn = page.locator('aside').getByRole('button', { name: 'Expand' }).first(); await expect(expandBtn).toBeVisible({ timeout: 10_000 }); await expandBtn.click(); const entityFolderBtn = page .locator('aside button') .filter({ hasText: `${client.firstName} ${client.lastName}` }) .first(); await expect(entityFolderBtn).toBeVisible({ timeout: 10_000 }); await entityFolderBtn.click(); // Entity folder view is rendered (Signing + Files sections). await expect(page.getByText('Signing in progress')).toBeVisible({ timeout: 10_000 }); await expect(page.getByText('Files')).toBeVisible({ timeout: 5_000 }); // The seeded file appears. await expect(page.getByText('seed.txt')).toBeVisible({ timeout: 10_000 }); }); });