SPB Git

Repository Public

sharing.spec.ts 24.7 KB 356 lines typescript

1// Owner side of sharing: the share dialog and the Shares screen, the portal dialog, the Requests screen and the detail2// of a portal with its deposits. Visitors and depositors are separate anonymous contexts or plain HTTP clients.3import { type APIRequestContext, type Browser, expect, type Page, test } from '@playwright/test';4import { mkdirSync } from 'node:fs';5import { apiSignUp } from './helpers.ts';67test.use({ locale: 'fr-CA' });8const BASE = process.env.SPD_E2E_BASE ?? 'http://localhost:5183';9const SHOTS = process.env.SPD_SHOTS ? '/tmp/spd-shots/sharing' : '';10const shot = async (page: Page, name: string, info: { project: { name: string } }) => { if (SHOTS) { mkdirSync(SHOTS, { recursive: true }); await page.screenshot({ path: `${SHOTS}/owner-${name}-${info.project.name}.png` }); } };11const clientIp = () => `10.${Math.floor(Math.random() * 250) + 1}.${Math.floor(Math.random() * 250) + 1}.${Math.floor(Math.random() * 250) + 1}`;1213async function call<T = any>(page: Page, method: string, path: string, body?: unknown): Promise<T> {14  return page.evaluate(async ([m, p, b]) => {15    const r = await fetch(`/api/v1${p}`, { method: m as string, headers: { 'X-SPD-CSRF': '1', ...(b !== undefined ? { 'Content-Type': 'application/json' } : {}) }, body: b !== undefined ? JSON.stringify(b) : m === 'GET' ? undefined : '{}' });16    const j = await r.json().catch(() => null);17    if (!r.ok) throw new Error(`${m} ${p}: ${r.status} ${JSON.stringify(j)}`);18    return j;19  }, [method, path, body] as const);20}21async function upload(page: Page, folderId: string, name: string, text: string): Promise<{ id: string }> {22  const r = await page.evaluate(async ([folder, n, body]) => {23    const h = { 'X-SPD-CSRF': '1' }, bytes = new TextEncoder().encode(body);24    const u = await (await fetch('/api/v1/uploads', { method: 'POST', headers: { ...h, 'Content-Type': 'application/json' }, body: JSON.stringify({ folderId: folder, name: n, size: bytes.length }) })).json();25    await fetch(`/api/v1/uploads/${u.id}?offset=0`, { method: 'PATCH', headers: { ...h, 'Content-Type': 'application/octet-stream' }, body: bytes });26    return (await fetch(`/api/v1/uploads/${u.id}/complete`, { method: 'POST', headers: { ...h, 'Content-Type': 'application/json' }, body: '{}' })).json();27  }, [folderId, name, text] as const);28  return r.file;29}30/** An anonymous visitor in its own browser context. */31async function visitor(browser: Browser, info: { project: { use: object } }): Promise<Page> {32  const ctx = await browser.newContext({ ...info.project.use, locale: 'fr-CA', baseURL: BASE, extraHTTPHeaders: { 'X-Forwarded-For': clientIp() } });33  return ctx.newPage();34}35const pathOf = (url: string) => new URL(url).pathname;36const menu = async (scope: ReturnType<Page['locator']>, page: Page, item: string) => { await scope.getByRole('button', { name: /^Actions pour/ }).click(); await page.getByRole('menuitem', { name: item }).click(); };3738test('share links: create, visit, journal, edit, rotate, revoke and delete', async ({ context, page, browser }, info) => {39  await apiSignUp(context, page);40  const folder = await call(page, 'POST', '/folders', { parentId: 'root', name: 'Comptabilité' });41  const file = await upload(page, folder.id, 'Bilan 2025.txt', 'Actif = Passif');42  await call(page, 'POST', '/shares', { fileId: file.id, label: 'Premier lien' });43  await call(page, 'POST', '/shares', { folderId: folder.id, label: 'Dossier pour le conseil' });4445  await page.goto('/shares');46  const screen = page.getByTestId('shares-screen');47  await expect(screen.getByRole('heading', { level: 1, name: 'Liens de partage' })).toBeVisible();48  await expect(screen.getByTestId('share-item')).toHaveCount(2);49  // a folder link leads to the folder; a file link opens the share dialog of the file50  await expect(screen.getByRole('link', { name: 'Comptabilité' })).toHaveAttribute('href', `/folder/${folder.id}`);51  await screen.getByRole('button', { name: 'Bilan 2025.txt' }).click();52  const dialog = page.getByRole('dialog', { name: 'Partager « Bilan 2025.txt »' });53  await expect(dialog.getByTestId('share-item')).toHaveCount(1);5455  // --- create ---56  await dialog.getByRole('button', { name: 'Créer un lien' }).click();57  await dialog.getByLabel('Libellé').fill('Pour Julie');58  await dialog.getByRole('switch', { name: 'Autoriser le téléchargement' }).click();59  await expect(dialog.getByText(/Cela n’empêche pas une capture d’écran/)).toBeVisible();60  await dialog.getByLabel('Mot de passe (facultatif)').fill('abc');61  await dialog.getByRole('radio', { name: '7 jours' }).click();62  await dialog.getByLabel('Consultations max.').fill('5');63  await dialog.getByRole('button', { name: 'Créer un lien' }).click();64  await expect(dialog.getByText('Le mot de passe doit compter au moins 4 caractères.')).toBeVisible();65  await dialog.getByLabel('Mot de passe (facultatif)').fill('hibou-12');66  await dialog.getByRole('button', { name: 'Créer un lien' }).click();67  const once = dialog.getByTestId('link-once');68  await expect(once.getByText(/il ne sera plus jamais affiché/)).toBeVisible();69  await shot(page, 'share-created', info);70  const url = await once.getByRole('textbox', { name: 'Lien' }).inputValue();71  expect(url).toMatch(/\/s\/[A-Za-z0-9_-]{43}$/);72  await dialog.getByRole('button', { name: 'Terminé' }).click();73  await expect(dialog.getByTestId('share-item')).toHaveCount(2);74  await expect(dialog.getByTestId('link-once')).toHaveCount(0); // the link is not shown a second time75  expect(await dialog.innerHTML()).not.toContain(pathOf(url));7677  // --- a visitor uses it: one wrong password, then in ---78  const guest = await visitor(browser, info);79  await guest.goto(pathOf(url));80  await guest.getByLabel('Mot de passe').fill('raté');81  await guest.getByRole('button', { name: 'Ouvrir' }).click();82  await expect(guest.getByText('Mot de passe incorrect.')).toBeVisible();83  await guest.getByLabel('Mot de passe').fill('hibou-12');84  await guest.getByRole('button', { name: 'Ouvrir' }).click();85  await expect(guest.getByRole('heading', { level: 1, name: 'Bilan 2025.txt' })).toBeVisible();86  await expect(guest.getByRole('button', { name: /Télécharger/ })).toHaveCount(0); // view-only8788  // --- the owner sees the visit ---89  await dialog.getByRole('button', { name: 'Fermer' }).click();90  await page.reload();91  const item = screen.getByTestId('share-item').filter({ hasText: 'Pour Julie' });92  await expect(item.getByTestId('share-counters')).toContainText('1 consultation / 5');93  await shot(page, 'shares', info);94  await expect(item).toContainText('Lecture seule');95  await expect(item).toContainText('Mot de passe');96  await menu(item, page, 'Journal d’accès');97  const journal = page.getByRole('dialog', { name: /Journal d’accès — Pour Julie/ });98  await expect(journal.getByTestId('share-events').getByRole('listitem')).toHaveCount(2);99  await shot(page, 'share-journal', info);100  await expect(journal.locator('[data-kind="open"][data-ok="true"]')).toContainText('Ouverture');101  await expect(journal.locator('[data-kind="password"][data-ok="false"]')).toContainText('Refusé');102  await expect(journal.locator('[data-kind="open"]')).toContainText(/10\.\d+\.\d+\.\d+/);103  await journal.getByRole('button', { name: 'Fermer' }).last().click();104105  // --- edit ---106  await menu(item, page, 'Modifier les réglages');107  const edit = page.getByRole('dialog', { name: 'Réglages du lien' });108  await expect(edit.getByLabel('Libellé')).toHaveValue('Pour Julie');109  await shot(page, 'share-edit', info);110  await edit.getByLabel('Libellé').fill('Pour Julie B.');111  await edit.getByRole('switch', { name: 'Autoriser le téléchargement' }).click();112  await edit.getByLabel('Téléchargements max.').fill('3');113  await edit.getByRole('button', { name: 'Enregistrer' }).click();114  const edited = screen.getByTestId('share-item').filter({ hasText: 'Pour Julie B.' });115  await expect(edited).toContainText('Téléchargement autorisé');116  await expect(edited.getByTestId('share-counters')).toContainText('0 téléchargement / 3');117118  // --- rotate: a new link once, the old one dead ---119  await menu(edited, page, 'Nouveau lien');120  await page.getByRole('dialog', { name: 'Nouveau lien' }).getByRole('button', { name: 'Générer un nouveau lien' }).click();121  const fresh = await page.getByTestId('link-once').getByRole('textbox', { name: 'Lien' }).inputValue();122  expect(fresh).not.toBe(url);123  await page.getByRole('button', { name: 'Terminé' }).click();124  await guest.goto(pathOf(url));125  await expect(guest.getByRole('heading', { name: 'Ce lien n’est pas disponible' })).toBeVisible();126  await guest.goto(pathOf(fresh));127  await expect(guest.getByRole('heading', { name: 'Ce contenu est protégé' })).toBeVisible();128129  // --- revoke: immediate ---130  await menu(edited, page, 'Révoquer');131  await page.getByRole('dialog', { name: 'Révoquer' }).getByRole('button', { name: 'Révoquer' }).click();132  await expect(edited).toHaveAttribute('data-status', 'revoked');133  await guest.reload();134  await expect(guest.getByRole('heading', { name: 'Ce lien n’est pas disponible' })).toBeVisible();135  await guest.context().close();136137  // --- filter by state, then delete ---138  await screen.getByRole('button', { name: /^Révoqué/ }).click();139  await expect(screen.getByTestId('share-item')).toHaveCount(1);140  await menu(edited, page, 'Supprimer');141  await page.getByRole('dialog', { name: 'Supprimer' }).getByRole('button', { name: 'Supprimer' }).click();142  await expect(screen.getByText('Pour Julie B.')).toHaveCount(0);143  expect((await call(page, 'GET', '/shares')).map((s: { label: string }) => s.label).sort()).toEqual(['Dossier pour le conseil', 'Premier lien']);144});145146/** A depositor without a browser: the public endpoints, as the deposit page calls them. */147async function deposit(request: APIRequestContext, token: string, form: Record<string, unknown>, files: { name: string; body: Buffer }[]): Promise<{ ref: string; status: string }> {148  const headers = { 'X-SPD-CSRF': '1', Origin: BASE, 'X-Forwarded-For': clientIp() };149  const res = await request.post(`/api/v1/public/requests/${token}/submissions`, { headers, data: { ...form, files: files.map((f) => ({ name: f.name, size: f.body.length })) } });150  expect(res.status(), await res.text()).toBe(200);151  const sub = await res.json();152  const auth = { ...headers, Authorization: `Bearer ${sub.submissionToken}` };153  for (const [i, it] of sub.items.entries()) expect((await request.patch(`/api/v1/public/uploads/${it.uploadId}?offset=0`, { headers: { ...auth, 'Content-Type': 'application/octet-stream' }, data: files[i]!.body })).status()).toBe(200);154  expect((await request.post('/api/v1/public/submissions/finalize', { headers: auth, data: {} })).status()).toBe(200);155  for (let i = 0; i < 60; i++) {156    const s = await (await request.get('/api/v1/public/submissions/status', { headers: auth })).json();157    if (s.status === 'completed' || s.status === 'rejected') return { ref: sub.ref, status: s.status };158    await new Promise((r) => setTimeout(r, 500));159  }160  throw new Error('the deposit was not processed in time');161}162163test('deposit portals: create with every section, pause, deposits and their journal, rotate, edit, revoke, delete', async ({ context, page, browser, request }, info) => {164  await apiSignUp(context, page);165  const clients = await call(page, 'POST', '/folders', { parentId: 'root', name: 'Clients' });166  const target = await call(page, 'POST', '/folders', { parentId: clients.id, name: 'Dossier Roy' });167168  await page.goto('/requests');169  const screen = page.getByTestId('requests-screen');170  await expect(screen.getByRole('heading', { name: 'Aucun portail de dépôt' })).toBeVisible();171  await expect(screen.getByText(/Un portail est une page publique/)).toBeVisible();172  await screen.getByRole('button', { name: 'Nouveau portail' }).click();173174  // --- folder picker: lazy tree, root included ---175  const picker = page.getByRole('dialog', { name: 'Dossier de destination' });176  await expect(picker.getByRole('button', { name: 'Mon Drive', exact: true })).toBeVisible();177  await expect(picker.getByRole('button', { name: 'Choisir ce dossier' })).toBeDisabled();178  await picker.getByRole('button', { name: 'Développer Clients' }).click();179  await picker.getByRole('button', { name: 'Dossier Roy', exact: true }).click();180  await picker.getByRole('button', { name: 'Choisir ce dossier' }).click();181182  // --- every section of the form ---183  const form = page.getByRole('dialog', { name: 'Nouveau portail de dépôt' });184  await expect(form.getByText('Les fichiers déposés arrivent dans « Dossier Roy »')).toBeVisible();185  await expect(form.getByLabel('Taille max. par fichier')).toHaveValue('200'); // defaults are visible186  await expect(form.getByLabel('Volume total du portail')).toHaveValue('5');187  await form.getByRole('button', { name: 'Créer le portail' }).click();188  await expect(form.getByText('Donnez un titre au portail.')).toBeVisible();189  await form.getByLabel('Titre').fill('Pièces pour votre déclaration');190  await form.getByLabel('Description').fill('Déposez ici vos relevés et reçus.');191  await form.getByLabel('Consignes').fill('Un fichier par document.\nPDF de préférence.');192  await form.getByLabel('Nom affiché').fill('Cabinet Roy & associés');193  await form.getByRole('button', { name: '#0f766e' }).click();194  await expect(form.getByTestId('brand-preview')).toContainText('Pièces pour votre déclaration');195  await expect(form.getByTestId('brand-preview')).toContainText('Cabinet Roy & associés');196  await shot(page, 'portal-form-top', info);197  await form.getByLabel('Mot de passe (facultatif)').fill('depot-2026');198  const inTenDays = new Date(Date.now() + 10 * 86_400_000);199  await form.getByLabel('Date d’expiration').fill(`${inTenDays.getFullYear()}-${String(inTenDays.getMonth() + 1).padStart(2, '0')}-${String(inTenDays.getDate()).padStart(2, '0')}T17:00`);200  await form.getByLabel('Taille max. par fichier').fill('50');201  await form.getByLabel('Fichiers par dépôt').fill('5');202  await form.getByLabel('Volume total du portail').fill('1');203  await form.getByLabel('Nombre max. de dépôts').fill('10');204  await form.getByLabel('Dépôts par heure et par adresse').fill('6');205  await form.getByRole('button', { name: 'Documents', exact: true }).click();206  await form.getByLabel('Ajouter une extension').fill('exe');207  await form.getByRole('button', { name: 'Ajouter', exact: true }).click();208  await expect(form.getByText('.exe est un type exécutable : il est toujours refusé.')).toBeVisible();209  await form.getByLabel('Ajouter une extension').fill('.JPG, png');210  await form.getByRole('button', { name: 'Ajouter', exact: true }).click();211  await expect(form.getByTestId('ext-chips')).toContainText('.jpg');212  await expect(form.getByTestId('ext-chips')).toContainText('.pdf');213  await form.getByLabel('Nom', { exact: true }).selectOption('required');214  await form.getByLabel('Courriel', { exact: true }).selectOption('optional');215  await form.getByLabel('Référence', { exact: true }).selectOption('required');216  await form.getByRole('button', { name: 'Ajouter un champ personnalisé' }).click();217  await form.getByRole('textbox', { name: 'Champ personnalisé 1' }).fill('Numéro de client');218  await form.getByTestId('custom-field').getByRole('checkbox').check();219  await form.getByRole('switch', { name: 'Exiger un consentement' }).click();220  await form.getByLabel('Texte du consentement').fill('J’autorise le cabinet à traiter ces documents.');221  await form.getByLabel('Note de conservation').fill('Conservés 7 ans.');222  await expect(form.getByRole('switch', { name: 'Un sous-dossier par dépôt' })).toHaveAttribute('aria-checked', 'true');223  await expect(form.getByRole('switch', { name: 'Me notifier à chaque dépôt' })).toHaveAttribute('aria-checked', 'true');224  await form.getByLabel('Message de confirmation').fill('Merci, nous avons bien reçu vos pièces.');225  await shot(page, 'portal-form-bottom', info);226  await form.getByRole('button', { name: 'Créer le portail' }).click();227228  // --- the link, once, with its QR code ---229  const created = page.getByRole('dialog', { name: 'Portail créé' });230  await expect(created.getByText(/il ne sera plus jamais affiché/)).toBeVisible();231  await expect(created.getByTestId('qr')).toBeVisible();232  await shot(page, 'portal-created', info);233  const url = await created.getByRole('textbox', { name: 'Lien' }).inputValue();234  expect(url).toMatch(/\/request\/[A-Za-z0-9_-]{43}$/);235  await created.getByRole('button', { name: 'Terminé' }).click();236237  const [saved] = await call(page, 'GET', '/requests');238  expect(saved).toMatchObject({239    folderId: target.id, title: 'Pièces pour votre déclaration', description: 'Déposez ici vos relevés et reçus.', instructions: 'Un fichier par document.\nPDF de préférence.', brandName: 'Cabinet Roy & associés', brandColor: '#0f766e',240    hasPassword: true, active: true, maxFileBytes: 50 * 1024 ** 2, maxFilesPerSubmission: 5, maxTotalBytes: 1024 ** 3, maxSubmissions: 10, submissionsPerHour: 6, requireConsent: true,241    consentText: 'J’autorise le cabinet à traiter ces documents.', retentionNote: 'Conservés 7 ans.', subfolderPerSubmission: true, notifyOwner: true, confirmationMessage: 'Merci, nous avons bien reçu vos pièces.', state: 'open',242    fields: { name: 'required', email: 'optional', reference: 'required', custom: [{ key: 'numero_de_client', label: 'Numéro de client', required: true }] },243  });244  expect(saved.allowedExtensions).toEqual(expect.arrayContaining(['pdf', 'docx', 'jpg', 'png']));245  expect(new Date(saved.expiresAt).getTime()).toBeGreaterThan(Date.now() + 8 * 86_400_000);246247  // --- the card, pause and reactivate ---248  const card = screen.getByTestId('portal-card');249  await expect(card).toHaveCount(1);250  await expect(card.getByTestId('portal-state')).toHaveText('Ouvert');251  await expect(card.getByRole('link', { name: 'Dossier Roy' })).toHaveAttribute('href', `/folder/${target.id}`);252  await card.getByRole('button', { name: 'Mettre en pause' }).click();253  await expect(card.getByTestId('portal-state')).toHaveText('En pause');254  const guest = await visitor(browser, info);255  await guest.goto(pathOf(url));256  await expect(guest.getByRole('heading', { name: 'Ce portail n’accepte plus de dépôts' })).toBeVisible();257  await card.getByRole('button', { name: 'Réactiver' }).click();258  await expect(card.getByTestId('portal-state')).toHaveText('Ouvert');259  await guest.reload();260  await expect(guest.getByRole('heading', { level: 1, name: 'Pièces pour votre déclaration' })).toBeVisible();261  await expect(guest.getByTestId('portal-banner')).toContainText('Cabinet Roy & associés');262  await expect(guest.getByTestId('portal-banner')).toHaveCSS('background-color', 'rgb(15, 118, 110)'); // the portal's accent colour263  await expect(guest.getByTestId('portal-banner')).toHaveCSS('color', 'rgb(255, 255, 255)');264  await expect(guest.getByLabel('Numéro de client')).toBeVisible();265266  // --- a deposit: one good file, one program disguised as an image ---267  const token = pathOf(url).split('/').pop()!;268  const exe = Buffer.concat([Buffer.from('MZ\x90\x00\x03\x00\x00\x00', 'latin1'), Buffer.alloc(1024, 1)]);269  const done = await deposit(request, token, { password: 'depot-2026', name: 'Hélène Roy', email: 'helene@example.test', reference: 'T1-2025', custom: { numero_de_client: 'C-0077' }, consent: true },270    [{ name: 'T4.pdf', body: Buffer.from('%PDF-1.4\n%%EOF\n') }, { name: 'photo.jpg', body: exe }]);271  expect(done.status).toBe('completed');272273  // --- the deposits of the portal ---274  await card.getByRole('button', { name: 'Voir les dépôts' }).click();275  const detail = page.getByTestId('request-detail');276  await expect(detail.getByRole('heading', { level: 1, name: 'Pièces pour votre déclaration' })).toBeVisible();277  await expect(detail.getByTestId('portal-facts')).toContainText('50 Mo par fichier');278  await expect(detail.getByTestId('portal-facts')).toContainText('Numéro de client (obligatoire)');279  const row = detail.getByTestId('submissions').locator('tbody tr');280  await expect(row).toHaveCount(1);281  await expect(row).toContainText(done.ref);282  await expect(row).toContainText('Hélène Roy');283  await expect(row).toContainText('Terminé');284  await expect(row).toContainText('1 accepté');285  await expect(row).toContainText('1 refusé');286  await shot(page, 'portal-detail', info);287  await expect(row.getByRole('link', { name: 'Ouvrir le dossier' })).toHaveAttribute('href', /\/folder\/[0-9a-f-]{36}$/);288  await row.getByRole('button', { name: `Détail du dépôt ${done.ref}` }).click();289  const sub = page.getByRole('dialog', { name: `Dépôt ${done.ref}` });290  await expect(sub.getByTestId('answers')).toContainText('Hélène Roy');291  await expect(sub.getByTestId('answers')).toContainText('T1-2025');292  await expect(sub.getByTestId('answers')).toContainText('Numéro de client');293  await expect(sub.getByTestId('answers')).toContainText('C-0077');294  await expect(sub.getByTestId('answers')).toContainText(/10\.\d+\.\d+\.\d+/);295  await expect(sub.getByTestId('sub-items').locator('[data-status="placed"]')).toContainText('T4.pdf');296  await expect(sub.getByTestId('sub-items').locator('[data-status="rejected"]')).toContainText('Refusé — le contenu est un programme ou un script.');297  const events = sub.getByTestId('sub-events').getByRole('listitem');298  await expect(events).toHaveCount(4);299  await expect(events.nth(0)).toContainText('Dépôt commencé : 2 fichier(s)');300  await expect(events.nth(1)).toContainText('Envoi terminé : 2 fichier(s)');301  await expect(events.nth(2)).toContainText('« photo.jpg » refusé');302  await expect(events.nth(3)).toContainText('Dépôt terminé : 1 fichier(s) rangé(s), 1 refusé(s).');303  await shot(page, 'submission', info);304  await sub.getByRole('button', { name: 'Fermer' }).last().click();305306  // --- rotate from the list: new link once, old link dead ---307  await detail.getByRole('link', { name: 'Portails de dépôt' }).click();308  await expect(card.getByTestId('portal-deposits')).toHaveText('1 / 10');309  await shot(page, 'requests', info);310  await menu(card, page, 'Nouveau lien');311  await page.getByRole('dialog', { name: 'Générer un nouveau lien' }).getByRole('button', { name: 'Générer un nouveau lien' }).click();312  const fresh = await page.getByTestId('link-once').getByRole('textbox', { name: 'Lien' }).inputValue();313  expect(fresh).not.toBe(url);314  await expect(page.getByTestId('qr')).toBeVisible();315  await page.getByRole('button', { name: 'Terminé' }).click();316  await guest.goto(pathOf(url));317  await expect(guest.getByRole('heading', { name: 'Ce lien de dépôt n’est pas disponible' })).toBeVisible();318  await guest.goto(pathOf(fresh));319  await expect(guest.getByRole('heading', { level: 1, name: 'Pièces pour votre déclaration' })).toBeVisible();320321  // --- edit: values are back, the link is not ---322  await menu(card, page, 'Modifier');323  const edit = page.getByRole('dialog', { name: 'Modifier le portail' });324  await expect(edit.getByLabel('Titre')).toHaveValue('Pièces pour votre déclaration');325  await expect(edit.getByRole('textbox', { name: 'Champ personnalisé 1' })).toHaveValue('Numéro de client');326  await expect(edit.getByTestId('link-once')).toHaveCount(0);327  await expect(edit.getByText(/Le lien public ne peut pas être réaffiché/)).toBeVisible();328  await expect(edit.getByRole('button', { name: 'Générer un nouveau lien' })).toBeVisible();329  await edit.getByLabel('Titre').fill('Pièces — déclaration 2025');330  await edit.getByRole('button', { name: 'Enregistrer' }).click();331  await expect(card.getByRole('link', { name: 'Pièces — déclaration 2025' })).toBeVisible();332  const [after] = await call(page, 'GET', '/requests');333  expect(after).toMatchObject({ title: 'Pièces — déclaration 2025', hasPassword: true, maxSubmissions: 10, fields: { custom: [{ key: 'numero_de_client' }] } });334  expect(after.expiresAt).toBe(saved.expiresAt);335336  // --- revoke, then delete ---337  await menu(card, page, 'Révoquer');338  const revoke = page.getByRole('dialog', { name: 'Révoquer le portail' });339  await expect(revoke.getByText(/refusés à leur prochain bloc/)).toBeVisible();340  await revoke.getByRole('button', { name: 'Révoquer' }).click();341  await expect(card.getByTestId('portal-state')).toHaveText('Révoqué');342  await expect(card.getByRole('button', { name: 'Mettre en pause' })).toHaveCount(0);343  await guest.reload();344  await expect(guest.getByRole('heading', { name: 'Ce lien de dépôt n’est pas disponible' })).toBeVisible();345  await guest.context().close();346  await menu(card, page, 'Supprimer');347  const purge = page.getByRole('dialog', { name: 'Supprimer le portail' });348  await expect(purge.getByText(/Les fichiers déjà déposés restent dans votre dossier/)).toBeVisible();349  await purge.getByRole('button', { name: 'Supprimer' }).click();350  await expect(screen.getByRole('heading', { name: 'Aucun portail de dépôt' })).toBeVisible();351  // the deposited file is still in the owner's folder352  const inTarget = await call(page, 'GET', `/folders/${target.id}`);353  expect(inTarget.folders).toHaveLength(1);354  expect((await call(page, 'GET', `/folders/${inTarget.folders[0].id}`)).files.map((f: { name: string }) => f.name)).toEqual(['T4.pdf']);355});356
Files main
Menu

Theme