import { nanoid } from '@documenso/lib/universal/id'; import { prisma } from '@documenso/prisma'; import { expect, type Page, test } from '@playwright/test'; import { addEnvelopeItemPdf, getEnvelopeEditorSettingsTrigger, openDocumentEnvelopeEditor, openEmbeddedEnvelopeEditor, openTemplateEnvelopeEditor, persistEmbeddedEnvelope, type TEnvelopeEditorSurface, } from '../fixtures/envelope-editor'; import { expectToastTextToBeVisible } from '../fixtures/generic'; const TEST_ATTACHMENTS = { first: { label: 'E2E First Attachment', url: 'https://example.com/first-attachment', }, second: { label: 'E2E Second Attachment', url: 'https://example.com/second-attachment', }, third: { label: 'E2E Third Attachment', url: 'https://example.com/third-attachment', }, }; type AttachmentFlowResult = { externalId: string; expectedAttachments: Array<{ label: string; url: string }>; deletedAttachment: { label: string; url: string }; }; const openSettingsDialog = async (root: Page) => { await getEnvelopeEditorSettingsTrigger(root).click(); await expect(root.getByRole('heading', { name: 'Document Settings' })).toBeVisible(); }; const updateExternalId = async (surface: TEnvelopeEditorSurface, externalId: string) => { await openSettingsDialog(surface.root); await surface.root.locator('input[name="externalId"]').fill(externalId); await surface.root.getByRole('button', { name: 'Update' }).click(); if (!surface.isEmbedded) { await expectToastTextToBeVisible(surface.root, 'Envelope updated'); } }; const openAttachmentsPopover = async (root: Page) => { await root.getByRole('button', { name: 'Attachments' }).click(); await expect(root.getByRole('heading', { name: 'Attachments', level: 4 })).toBeVisible(); }; const addAttachment = async (root: Page, { label, url }: { label: string; url: string }) => { await root.getByRole('button', { name: 'Add Attachment' }).click(); await root.getByPlaceholder('Label').fill(label); await root.getByPlaceholder('URL').fill(url); await root.getByRole('button', { name: 'Add', exact: true }).click(); }; const getAttachmentItems = (root: Page) => root.locator('.rounded-md.border.border-border.p-2'); const getAttachmentDeleteButtons = (root: Page) => getAttachmentItems(root).locator('button'); const assertAttachmentVisibleInPopover = async (root: Page, { label, url }: { label: string; url: string }) => { const items = getAttachmentItems(root); const matchingItem = items.filter({ hasText: label }); await expect(matchingItem).toBeVisible(); await expect(matchingItem.locator(`a[href="${url}"]`)).toBeVisible(); }; const assertAttachmentNotVisibleInPopover = async (root: Page, label: string) => { await expect(getAttachmentItems(root).filter({ hasText: label })).toHaveCount(0); }; const assertAttachmentCount = async (root: Page, count: number) => { const button = root.getByRole('button', { name: 'Attachments' }); if (count > 0) { await expect(button).toContainText(`(${count})`); } else { await expect(button).not.toContainText('('); } }; const assertAttachmentsInDatabase = async ({ externalId, surface, expectedAttachments, deletedLabel, }: { externalId: string; surface: TEnvelopeEditorSurface; expectedAttachments: Array<{ label: string; url: string }>; deletedLabel?: string; }) => { const envelope = await prisma.envelope.findFirstOrThrow({ where: { externalId, userId: surface.userId, teamId: surface.teamId, type: surface.envelopeType, }, include: { envelopeAttachments: { orderBy: { createdAt: 'asc', }, }, }, orderBy: { createdAt: 'desc', }, }); expect(envelope.envelopeAttachments).toHaveLength(expectedAttachments.length); for (const expected of expectedAttachments) { const found = envelope.envelopeAttachments.find((a) => a.label === expected.label); expect(found).toBeDefined(); expect(found!.data).toBe(expected.url); expect(found!.type).toBe('link'); } if (deletedLabel) { expect(envelope.envelopeAttachments.some((a) => a.label === deletedLabel)).toBe(false); } }; const runAttachmentFlow = async (surface: TEnvelopeEditorSurface): Promise => { const externalId = `e2e-attachments-${nanoid()}`; await updateExternalId(surface, externalId); await openAttachmentsPopover(surface.root); // Create first attachment. await addAttachment(surface.root, TEST_ATTACHMENTS.first); await expectToastTextToBeVisible(surface.root, 'Attachment added successfully.'); await assertAttachmentVisibleInPopover(surface.root, TEST_ATTACHMENTS.first); await assertAttachmentCount(surface.root, 1); await assertAttachmentsInDatabase({ externalId, surface, expectedAttachments: [TEST_ATTACHMENTS.first], }); // Create second attachment. await addAttachment(surface.root, TEST_ATTACHMENTS.second); await expectToastTextToBeVisible(surface.root, 'Attachment added successfully.'); await assertAttachmentVisibleInPopover(surface.root, TEST_ATTACHMENTS.second); await assertAttachmentCount(surface.root, 2); await assertAttachmentsInDatabase({ externalId, surface, expectedAttachments: [TEST_ATTACHMENTS.first, TEST_ATTACHMENTS.second], }); // Create third attachment. await addAttachment(surface.root, TEST_ATTACHMENTS.third); await expectToastTextToBeVisible(surface.root, 'Attachment added successfully.'); await assertAttachmentVisibleInPopover(surface.root, TEST_ATTACHMENTS.third); await assertAttachmentCount(surface.root, 3); await assertAttachmentsInDatabase({ externalId, surface, expectedAttachments: [TEST_ATTACHMENTS.first, TEST_ATTACHMENTS.second, TEST_ATTACHMENTS.third], }); // Delete first attachment. await getAttachmentDeleteButtons(surface.root).first().click(); await expectToastTextToBeVisible(surface.root, 'Attachment removed successfully.'); await expect(getAttachmentItems(surface.root)).toHaveCount(2); await assertAttachmentNotVisibleInPopover(surface.root, TEST_ATTACHMENTS.first.label); await assertAttachmentVisibleInPopover(surface.root, TEST_ATTACHMENTS.second); await assertAttachmentVisibleInPopover(surface.root, TEST_ATTACHMENTS.third); await assertAttachmentCount(surface.root, 2); await assertAttachmentsInDatabase({ externalId, surface, expectedAttachments: [TEST_ATTACHMENTS.second, TEST_ATTACHMENTS.third], deletedLabel: TEST_ATTACHMENTS.first.label, }); return { externalId, expectedAttachments: [TEST_ATTACHMENTS.second, TEST_ATTACHMENTS.third], deletedAttachment: TEST_ATTACHMENTS.first, }; }; const runEmbeddedAttachmentFlow = async (surface: TEnvelopeEditorSurface): Promise => { const externalId = `e2e-attachments-${nanoid()}`; await updateExternalId(surface, externalId); await openAttachmentsPopover(surface.root); // Create first attachment. await addAttachment(surface.root, TEST_ATTACHMENTS.first); await expectToastTextToBeVisible(surface.root, 'Attachment added successfully.'); await assertAttachmentVisibleInPopover(surface.root, TEST_ATTACHMENTS.first); await assertAttachmentCount(surface.root, 1); // Create second attachment. await addAttachment(surface.root, TEST_ATTACHMENTS.second); await expectToastTextToBeVisible(surface.root, 'Attachment added successfully.'); await assertAttachmentVisibleInPopover(surface.root, TEST_ATTACHMENTS.second); await assertAttachmentCount(surface.root, 2); // Create third attachment. await addAttachment(surface.root, TEST_ATTACHMENTS.third); await expectToastTextToBeVisible(surface.root, 'Attachment added successfully.'); await assertAttachmentVisibleInPopover(surface.root, TEST_ATTACHMENTS.third); await assertAttachmentCount(surface.root, 3); // Delete first attachment. await getAttachmentDeleteButtons(surface.root).first().click(); await expectToastTextToBeVisible(surface.root, 'Attachment removed successfully.'); await expect(getAttachmentItems(surface.root)).toHaveCount(2); await assertAttachmentNotVisibleInPopover(surface.root, TEST_ATTACHMENTS.first.label); await assertAttachmentVisibleInPopover(surface.root, TEST_ATTACHMENTS.second); await assertAttachmentVisibleInPopover(surface.root, TEST_ATTACHMENTS.third); await assertAttachmentCount(surface.root, 2); return { externalId, expectedAttachments: [TEST_ATTACHMENTS.second, TEST_ATTACHMENTS.third], deletedAttachment: TEST_ATTACHMENTS.first, }; }; const assertAttachmentsPersistedInDatabase = async ({ surface, externalId, expectedAttachments, deletedAttachment, }: { surface: TEnvelopeEditorSurface; externalId: string; expectedAttachments: AttachmentFlowResult['expectedAttachments']; deletedAttachment: AttachmentFlowResult['deletedAttachment']; }) => { await assertAttachmentsInDatabase({ externalId, surface, expectedAttachments, deletedLabel: deletedAttachment.label, }); }; test.describe('document editor', () => { test('add, verify and delete attachments', async ({ page }) => { const surface = await openDocumentEnvelopeEditor(page); const result = await runAttachmentFlow(surface); await assertAttachmentsPersistedInDatabase({ surface, ...result, }); }); }); test.describe('template editor', () => { test('add, verify and delete attachments', async ({ page }) => { const surface = await openTemplateEnvelopeEditor(page); const result = await runAttachmentFlow(surface); await assertAttachmentsPersistedInDatabase({ surface, ...result, }); }); }); test.describe('embedded create', () => { test('add, verify and delete attachments', async ({ page }) => { const surface = await openEmbeddedEnvelopeEditor(page, { envelopeType: 'DOCUMENT', tokenNamePrefix: 'e2e-embed-attachments', }); await addEnvelopeItemPdf(surface.root, 'embedded-document-attachments.pdf'); const result = await runEmbeddedAttachmentFlow(surface); await persistEmbeddedEnvelope(surface); await assertAttachmentsPersistedInDatabase({ surface, ...result, }); }); }); test.describe('embedded edit', () => { test('add, verify and delete attachments', async ({ page }) => { const surface = await openEmbeddedEnvelopeEditor(page, { envelopeType: 'TEMPLATE', mode: 'edit', tokenNamePrefix: 'e2e-embed-attachments', }); const result = await runEmbeddedAttachmentFlow(surface); await persistEmbeddedEnvelope(surface); await assertAttachmentsPersistedInDatabase({ surface, ...result, }); }); });