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
199 lines
7.7 KiB
TypeScript
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 });
|
|
});
|
|
});
|