import { nanoid } from '@documenso/lib/universal/id'; import { prisma } from '@documenso/prisma'; import { expect, type Page, test } from '@playwright/test'; import { DocumentDistributionMethod, DocumentVisibility } from '@prisma/client'; import { getEnvelopeEditorSettingsTrigger, openDocumentEnvelopeEditor, openEmbeddedEnvelopeEditor, openTemplateEnvelopeEditor, persistEmbeddedEnvelope, type TEnvelopeEditorSurface, } from '../fixtures/envelope-editor'; import { expectToastTextToBeVisible } from '../fixtures/generic'; type SettingsFlowData = { externalId: string; isEmbedded: boolean; }; const TEST_SETTINGS_VALUES = { replyTo: 'e2e-settings@example.com', redirectUrl: 'https://example.com/e2e-settings-complete', subject: 'E2E settings subject', message: 'E2E settings message', language: 'French', dateFormat: 'DD/MM/YYYY', timezone: 'Europe/London', distributionMethod: 'None', expirationMode: 'Custom duration', expirationAmount: 5, expirationUnit: 'Weeks', reminderMode: 'Enabled', reminderSendAfterAmount: 3, reminderSendAfterUnit: 'Days', reminderRepeatMode: 'Custom interval', reminderRepeatAmount: 7, reminderRepeatUnit: 'Days', accessAuth: 'Require account', actionAuth: 'Require password', visibility: 'Managers and above', }; const DB_EXPECTED_VALUES = { language: 'fr', dateFormat: 'dd/MM/yyyy', timezone: 'Europe/London', distributionMethod: DocumentDistributionMethod.NONE, envelopeExpirationPeriod: { unit: 'week', amount: 5 }, reminderSettings: { sendAfter: { unit: 'day', amount: 3 }, repeatEvery: { unit: 'day', amount: 7 }, }, visibility: DocumentVisibility.MANAGER_AND_ABOVE, globalAccessAuth: ['ACCOUNT'], globalActionAuth: ['PASSWORD'], emailSettings: { recipientSigned: false, recipientSigningRequest: false, recipientRemoved: false, documentPending: false, documentCompleted: false, documentDeleted: false, ownerDocumentCompleted: false, ownerRecipientExpired: false, ownerDocumentCreated: false, }, }; const openSettingsDialog = async (root: Page) => { await getEnvelopeEditorSettingsTrigger(root).click(); await expect(root.getByRole('heading', { name: 'Document Settings' })).toBeVisible(); }; const clickSettingsDialogHeader = async (root: Page) => { await root.locator('[data-testid="envelope-editor-settings-dialog-header"]').click(); }; const getComboboxByLabel = (root: Page, label: string) => root.locator(`label:has-text("${label}")`).locator('xpath=..').locator('[role="combobox"]').first(); const selectMultiSelectOption = async ( root: Page, dataTestId: 'documentAccessSelectValue' | 'documentActionSelectValue', optionLabel: string, ) => { const select = root.locator(`[data-testid="${dataTestId}"]`); await select.click(); await root.locator('[cmdk-item]').filter({ hasText: optionLabel }).first().click(); await clickSettingsDialogHeader(root); }; const runSettingsFlow = async ({ root }: TEnvelopeEditorSurface, { externalId, isEmbedded }: SettingsFlowData) => { await openSettingsDialog(root); await getComboboxByLabel(root, 'Language').click(); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.language }).click(); await clickSettingsDialogHeader(root); const signatureTypesCombobox = getComboboxByLabel(root, 'Allowed Signature Types'); await signatureTypesCombobox.click(); await root.getByRole('option', { name: 'Upload' }).click(); await clickSettingsDialogHeader(root); await getComboboxByLabel(root, 'Date Format').click(); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.dateFormat, exact: true }).click(); await clickSettingsDialogHeader(root); await getComboboxByLabel(root, 'Time Zone').click(); await root.locator('[cmdk-input]').last().fill(TEST_SETTINGS_VALUES.timezone); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.timezone }).click(); await clickSettingsDialogHeader(root); await root.locator('input[name="externalId"]').fill(externalId); await root.locator('input[name="meta.redirectUrl"]').fill(TEST_SETTINGS_VALUES.redirectUrl); await root.getByRole('button', { name: 'Notifications' }).click(); // Fill email-content fields and toggle recipient-facing notification checkboxes // while distributionMethod is still EMAIL. After it flips to NONE below, these // controls are disabled because no email is sent to recipients. await root.locator('input[name="meta.subject"]').fill(TEST_SETTINGS_VALUES.subject); await root.locator('textarea[name="meta.message"]').fill(TEST_SETTINGS_VALUES.message); await root.locator('input[name="meta.emailReplyTo"]').fill(TEST_SETTINGS_VALUES.replyTo); await root.locator('#recipientSigned').click(); await root.locator('#recipientSigningRequest').click(); await root.locator('#recipientRemoved').click(); await root.locator('#documentPending').click(); await root.locator('#documentCompleted').click(); await root.locator('#documentDeleted').click(); await root.getByRole('button', { name: 'General' }).click(); await root.locator('[data-testid="documentDistributionMethodSelectValue"]').click(); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.distributionMethod }).click(); await clickSettingsDialogHeader(root); await getComboboxByLabel(root, 'Expiration').click(); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.expirationMode }).click(); await root.getByRole('spinbutton').clear(); await root.getByRole('spinbutton').fill(String(TEST_SETTINGS_VALUES.expirationAmount)); const expirationUnitTrigger = root .locator('button[role="combobox"]') .filter({ hasText: /Months|Days|Weeks|Years/ }) .first(); await expirationUnitTrigger.click(); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.expirationUnit }).click(); await clickSettingsDialogHeader(root); // Configure reminder settings. await root.getByRole('button', { name: 'Reminders' }).click(); await root.locator('[data-testid="reminder-mode-select"]').click(); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.reminderMode }).click(); await clickSettingsDialogHeader(root); await root.locator('[data-testid="reminder-send-after-amount"]').clear(); await root .locator('[data-testid="reminder-send-after-amount"]') .fill(String(TEST_SETTINGS_VALUES.reminderSendAfterAmount)); await root.locator('[data-testid="reminder-send-after-unit"]').click(); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.reminderSendAfterUnit }).click(); await clickSettingsDialogHeader(root); await root.locator('[data-testid="reminder-repeat-mode-select"]').click(); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.reminderRepeatMode }).click(); await clickSettingsDialogHeader(root); await root.locator('[data-testid="reminder-repeat-amount"]').clear(); await root.locator('[data-testid="reminder-repeat-amount"]').fill(String(TEST_SETTINGS_VALUES.reminderRepeatAmount)); await root.locator('[data-testid="reminder-repeat-unit"]').click(); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.reminderRepeatUnit }).click(); await clickSettingsDialogHeader(root); const spinbuttons = root.getByRole('spinbutton'); await spinbuttons.first().clear(); await spinbuttons.first().fill(String(TEST_SETTINGS_VALUES.reminderSendAfterAmount)); const sendAfterUnitTrigger = root .locator('button[role="combobox"]') .filter({ hasText: /Days|Weeks|Months/ }) .first(); await sendAfterUnitTrigger.click(); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.reminderSendAfterUnit }).click(); await clickSettingsDialogHeader(root); const repeatModeSelect = root .locator('button[role="combobox"]') .filter({ hasText: /Custom interval|Don't repeat/ }) .first(); await repeatModeSelect.click(); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.reminderRepeatMode }).click(); await clickSettingsDialogHeader(root); await spinbuttons.last().clear(); await spinbuttons.last().fill(String(TEST_SETTINGS_VALUES.reminderRepeatAmount)); const repeatUnitTrigger = root .locator('button[role="combobox"]') .filter({ hasText: /Days|Weeks|Months/ }) .last(); await repeatUnitTrigger.click(); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.reminderRepeatUnit }).click(); await clickSettingsDialogHeader(root); await root.getByRole('button', { name: 'Notifications' }).click(); // Distribution is NONE: email-content fields stay rendered but disabled, // recipient-facing checkboxes are hidden entirely and replaced by an alert, // owner-facing checkboxes stay editable so we toggle them here. await expect(root.locator('input[name="meta.subject"]')).toBeDisabled(); await expect(root.locator('textarea[name="meta.message"]')).toBeDisabled(); await expect(root.locator('input[name="meta.emailReplyTo"]')).toBeDisabled(); await expect(root.locator('#recipientSigned')).toHaveCount(0); await expect(root.locator('#recipientSigningRequest')).toHaveCount(0); await expect(root.locator('#recipientRemoved')).toHaveCount(0); await expect(root.locator('#documentPending')).toHaveCount(0); await expect(root.locator('#documentCompleted')).toHaveCount(0); await expect(root.locator('#documentDeleted')).toHaveCount(0); await expect(root.getByText(/Email distribution needs to be enabled/)).toBeVisible(); // Email Sender select only renders when the org has the emailDomains feature // flag plus allowConfigureEmailSender, so the assertion is conditional. const emailSenderSelect = getComboboxByLabel(root, 'Email Sender'); const hasEmailSenderSelect = (await emailSenderSelect.count()) > 0; if (hasEmailSenderSelect) { await expect(emailSenderSelect).toBeDisabled(); } await expect(root.locator('#ownerDocumentCompleted')).toBeEnabled(); await root.locator('#ownerDocumentCompleted').click(); await root.locator('#ownerRecipientExpired').click(); await root.locator('#ownerDocumentCreated').click(); await root.getByRole('button', { name: 'Security' }).click(); await selectMultiSelectOption(root, 'documentAccessSelectValue', TEST_SETTINGS_VALUES.accessAuth); const actionAuthSelect = root.locator('[data-testid="documentActionSelectValue"]'); const hasActionAuthSelect = (await actionAuthSelect.count()) > 0; if (hasActionAuthSelect) { await selectMultiSelectOption(root, 'documentActionSelectValue', TEST_SETTINGS_VALUES.actionAuth); } if (isEmbedded) { await expect(root.locator('[data-testid="documentVisibilitySelectValue"]')).toHaveCount(0); } else { await root.locator('[data-testid="documentVisibilitySelectValue"]').click(); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.visibility }).click(); await clickSettingsDialogHeader(root); } await root.getByRole('button', { name: 'Update' }).click(); if (!isEmbedded) { await expectToastTextToBeVisible(root, 'Envelope updated'); } await openSettingsDialog(root); await expect(root.locator('input[name="externalId"]')).toHaveValue(externalId); await expect(root.locator('input[name="meta.redirectUrl"]')).toHaveValue(TEST_SETTINGS_VALUES.redirectUrl); await expect(getComboboxByLabel(root, 'Language')).toContainText(TEST_SETTINGS_VALUES.language); await expect(getComboboxByLabel(root, 'Allowed Signature Types')).not.toContainText('Upload'); await expect(getComboboxByLabel(root, 'Date Format')).toContainText(TEST_SETTINGS_VALUES.dateFormat); await expect(getComboboxByLabel(root, 'Time Zone')).toContainText(TEST_SETTINGS_VALUES.timezone); await expect(root.locator('[data-testid="documentDistributionMethodSelectValue"]')).toContainText( TEST_SETTINGS_VALUES.distributionMethod, ); await expect(getComboboxByLabel(root, 'Expiration')).toContainText(TEST_SETTINGS_VALUES.expirationMode); await expect(root.getByRole('spinbutton')).toHaveValue(String(TEST_SETTINGS_VALUES.expirationAmount)); await expect( root.locator('button[role="combobox"]').filter({ hasText: TEST_SETTINGS_VALUES.expirationUnit }).first(), ).toBeVisible(); // Verify reminder settings persisted in UI. await root.getByRole('button', { name: 'Reminders' }).click(); await expect(root.locator('[data-testid="reminder-mode-select"]')).toContainText(TEST_SETTINGS_VALUES.reminderMode); await expect(root.locator('[data-testid="reminder-send-after-amount"]')).toHaveValue( String(TEST_SETTINGS_VALUES.reminderSendAfterAmount), ); await expect(root.locator('[data-testid="reminder-send-after-unit"]')).toContainText( TEST_SETTINGS_VALUES.reminderSendAfterUnit, ); await expect(root.locator('[data-testid="reminder-repeat-mode-select"]')).toContainText( TEST_SETTINGS_VALUES.reminderRepeatMode, ); await expect(root.locator('[data-testid="reminder-repeat-amount"]')).toHaveValue( String(TEST_SETTINGS_VALUES.reminderRepeatAmount), ); await expect(root.locator('[data-testid="reminder-repeat-unit"]')).toContainText( TEST_SETTINGS_VALUES.reminderRepeatUnit, ); await root.getByRole('button', { name: 'Notifications' }).click(); // Distribution persisted as NONE: recipient-facing checkboxes are hidden, owner-facing // checkboxes remain visible and persist their stored values. Email-content fields are // still rendered (disabled) and persist their stored values. await expect(root.locator('#recipientSigned')).toHaveCount(0); await expect(root.locator('#recipientSigningRequest')).toHaveCount(0); await expect(root.locator('#recipientRemoved')).toHaveCount(0); await expect(root.locator('#documentPending')).toHaveCount(0); await expect(root.locator('#documentCompleted')).toHaveCount(0); await expect(root.locator('#documentDeleted')).toHaveCount(0); await expect(root.getByText(/Email distribution needs to be enabled/)).toBeVisible(); await expect(root.locator('#ownerDocumentCompleted')).toHaveAttribute('aria-checked', 'false'); await expect(root.locator('#ownerRecipientExpired')).toHaveAttribute('aria-checked', 'false'); await expect(root.locator('#ownerDocumentCreated')).toHaveAttribute('aria-checked', 'false'); await expect(root.locator('input[name="meta.emailReplyTo"]')).toHaveValue(TEST_SETTINGS_VALUES.replyTo); await expect(root.locator('input[name="meta.subject"]')).toHaveValue(TEST_SETTINGS_VALUES.subject); await expect(root.locator('textarea[name="meta.message"]')).toHaveValue(TEST_SETTINGS_VALUES.message); await root.getByRole('button', { name: 'Security' }).click(); await expect(root.locator('[data-testid="documentAccessSelectValue"]')).toContainText( TEST_SETTINGS_VALUES.accessAuth, ); if (hasActionAuthSelect) { await expect(root.locator('[data-testid="documentActionSelectValue"]')).toContainText( TEST_SETTINGS_VALUES.actionAuth, ); } if (isEmbedded) { await expect(root.locator('[data-testid="documentVisibilitySelectValue"]')).toHaveCount(0); } else { await expect(root.locator('[data-testid="documentVisibilitySelectValue"]')).toContainText( TEST_SETTINGS_VALUES.visibility, ); } await root.getByRole('button', { name: 'Update' }).click(); if (!isEmbedded) { await expectToastTextToBeVisible(root, 'Envelope updated'); } return { hasActionAuthSelect, }; }; const assertEnvelopeSettingsPersistedInDatabase = async ({ externalId, surface, hasActionAuthSelect, shouldAssertVisibility, }: { externalId: string; surface: TEnvelopeEditorSurface; hasActionAuthSelect: boolean; shouldAssertVisibility: boolean; }) => { const envelope = await prisma.envelope.findFirstOrThrow({ where: { externalId, userId: surface.userId, teamId: surface.teamId, type: surface.envelopeType, }, orderBy: { createdAt: 'desc' }, include: { documentMeta: true, }, }); expect(envelope.externalId).toBe(externalId); if (shouldAssertVisibility) { expect(envelope.visibility).toBe(DB_EXPECTED_VALUES.visibility); } expect(envelope.documentMeta.language).toBe(DB_EXPECTED_VALUES.language); expect(envelope.documentMeta.dateFormat).toBe(DB_EXPECTED_VALUES.dateFormat); expect(envelope.documentMeta.timezone).toBe(DB_EXPECTED_VALUES.timezone); expect(envelope.documentMeta.distributionMethod).toBe(DB_EXPECTED_VALUES.distributionMethod); expect(envelope.documentMeta.envelopeExpirationPeriod).toEqual(DB_EXPECTED_VALUES.envelopeExpirationPeriod); expect(envelope.documentMeta.reminderSettings).toEqual(DB_EXPECTED_VALUES.reminderSettings); expect(envelope.documentMeta.redirectUrl).toBe(TEST_SETTINGS_VALUES.redirectUrl); expect(envelope.documentMeta.emailReplyTo).toBe(TEST_SETTINGS_VALUES.replyTo); expect(envelope.documentMeta.subject).toBe(TEST_SETTINGS_VALUES.subject); expect(envelope.documentMeta.message).toBe(TEST_SETTINGS_VALUES.message); expect(envelope.documentMeta.drawSignatureEnabled).toBe(true); expect(envelope.documentMeta.typedSignatureEnabled).toBe(true); expect(envelope.documentMeta.uploadSignatureEnabled).toBe(false); expect(envelope.documentMeta.emailSettings).toMatchObject(DB_EXPECTED_VALUES.emailSettings); const authOptions = parseAuthOptions(envelope.authOptions); expect(authOptions.globalAccessAuth ?? []).toEqual(DB_EXPECTED_VALUES.globalAccessAuth); if (hasActionAuthSelect) { expect(authOptions.globalActionAuth ?? []).toEqual(DB_EXPECTED_VALUES.globalActionAuth); } }; const parseAuthOptions = (authOptions: unknown): { globalAccessAuth: string[]; globalActionAuth: string[] } => { if (!isRecord(authOptions)) { return { globalAccessAuth: [], globalActionAuth: [], }; } return { globalAccessAuth: Array.isArray(authOptions.globalAccessAuth) ? authOptions.globalAccessAuth.filter((entry): entry is string => typeof entry === 'string') : [], globalActionAuth: Array.isArray(authOptions.globalActionAuth) ? authOptions.globalActionAuth.filter((entry): entry is string => typeof entry === 'string') : [], }; }; const isRecord = (value: unknown): value is Record => typeof value === 'object' && value !== null; test.describe('document editor', () => { test('update and persist settings', async ({ page }) => { const surface = await openDocumentEnvelopeEditor(page); const externalId = `e2e-settings-${nanoid()}`; const { hasActionAuthSelect } = await runSettingsFlow(surface, { externalId, isEmbedded: false, }); await assertEnvelopeSettingsPersistedInDatabase({ externalId, surface, hasActionAuthSelect, shouldAssertVisibility: true, }); }); }); test.describe('template editor', () => { test('update and persist settings', async ({ page }) => { const surface = await openTemplateEnvelopeEditor(page); const externalId = `e2e-settings-${nanoid()}`; const { hasActionAuthSelect } = await runSettingsFlow(surface, { externalId, isEmbedded: false, }); await assertEnvelopeSettingsPersistedInDatabase({ externalId, surface, hasActionAuthSelect, shouldAssertVisibility: true, }); }); }); test.describe('embedded create', () => { test('update and persist settings', async ({ page }) => { const surface = await openEmbeddedEnvelopeEditor(page, { envelopeType: 'DOCUMENT', tokenNamePrefix: 'e2e-embed-settings', }); const externalId = `e2e-settings-${nanoid()}`; const { hasActionAuthSelect } = await runSettingsFlow(surface, { externalId, isEmbedded: true, }); await persistEmbeddedEnvelope(surface); await assertEnvelopeSettingsPersistedInDatabase({ externalId, surface, hasActionAuthSelect, shouldAssertVisibility: false, }); }); }); test.describe('embedded edit', () => { test('update and persist settings', async ({ page }) => { const surface = await openEmbeddedEnvelopeEditor(page, { envelopeType: 'TEMPLATE', mode: 'edit', tokenNamePrefix: 'e2e-embed-settings', }); const externalId = `e2e-settings-${nanoid()}`; const { hasActionAuthSelect } = await runSettingsFlow(surface, { externalId, isEmbedded: true, }); await persistEmbeddedEnvelope(surface); await assertEnvelopeSettingsPersistedInDatabase({ externalId, surface, hasActionAuthSelect, shouldAssertVisibility: false, }); }); });