Files
pn-new-crm/tests/e2e/smoke/04-documents-hub-upload-into-entity.spec.ts
Matt 221ae5784e chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
2026-05-23 00:52:59 +02:00

199 lines
7.7 KiB
TypeScript

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 <span> elements inside <li> 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 });
});
});