import { expect, test } from '@playwright/test'; import { DocumentSigningOrder, DocumentStatus, FieldType, RecipientRole, SigningStatus, } from '@prisma/client'; import { prisma } from '@documenso/prisma'; import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents'; import { seedUser } from '@documenso/prisma/seed/users'; import { signDirectSignaturePad, signSignaturePad } from '../fixtures/signature'; test('[NEXT_RECIPIENT_DICTATION]: should allow updating next recipient when dictation is enabled', async ({ page, }) => { const user = await seedUser(); const firstSigner = await seedUser(); const secondSigner = await seedUser(); const thirdSigner = await seedUser(); const { recipients, document } = await seedPendingDocumentWithFullFields({ owner: user, recipients: [firstSigner, secondSigner, thirdSigner], recipientsCreateOptions: [{ signingOrder: 1 }, { signingOrder: 2 }, { signingOrder: 3 }], updateDocumentOptions: { documentMeta: { upsert: { create: { allowDictateNextSigner: true, signingOrder: DocumentSigningOrder.SEQUENTIAL, }, update: { allowDictateNextSigner: true, signingOrder: DocumentSigningOrder.SEQUENTIAL, }, }, }, }, }); const firstRecipient = recipients[0]; const { token, fields } = firstRecipient; const signUrl = `/sign/${token}`; await page.goto(signUrl); await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible(); await signSignaturePad(page); // Fill in all fields for (const field of fields) { await page.locator(`#field-${field.id}`).getByRole('button').click(); if (field.type === FieldType.TEXT) { await page.locator('#custom-text').fill('TEXT'); await page.getByRole('button', { name: 'Save' }).click(); } await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true'); } // Complete signing and update next recipient await page.getByRole('button', { name: 'Complete' }).click(); // Verify next recipient info is shown await expect(page.getByRole('dialog')).toBeVisible(); await expect(page.getByText('The next recipient to sign this document will be')).toBeVisible(); // Update next recipient await page.locator('button').filter({ hasText: 'Update Recipient' }).click(); await page.waitForTimeout(1000); // Use dialog context to ensure we're targeting the correct form fields const dialog = page.getByRole('dialog'); await dialog.getByLabel('Name').fill('New Recipient'); await dialog.getByLabel('Email').fill('new.recipient@example.com'); // Submit and verify completion await page.getByRole('button', { name: 'Sign' }).click(); await page.waitForURL(`${signUrl}/complete`); // Verify document and recipient states const updatedDocument = await prisma.document.findUniqueOrThrow({ where: { id: document.id }, include: { recipients: { orderBy: { signingOrder: 'asc' }, }, }, }); // Document should still be pending as there are more recipients expect(updatedDocument.status).toBe(DocumentStatus.PENDING); // First recipient should be completed const updatedFirstRecipient = updatedDocument.recipients[0]; expect(updatedFirstRecipient.signingStatus).toBe(SigningStatus.SIGNED); // Second recipient should be the new recipient const updatedSecondRecipient = updatedDocument.recipients[1]; expect(updatedSecondRecipient.name).toBe('New Recipient'); expect(updatedSecondRecipient.email).toBe('new.recipient@example.com'); expect(updatedSecondRecipient.signingOrder).toBe(2); expect(updatedSecondRecipient.signingStatus).toBe(SigningStatus.NOT_SIGNED); }); test('[NEXT_RECIPIENT_DICTATION]: should not show dictation UI when disabled', async ({ page }) => { const user = await seedUser(); const firstSigner = await seedUser(); const secondSigner = await seedUser(); const { recipients, document } = await seedPendingDocumentWithFullFields({ owner: user, recipients: [firstSigner, secondSigner], recipientsCreateOptions: [{ signingOrder: 1 }, { signingOrder: 2 }], updateDocumentOptions: { documentMeta: { upsert: { create: { allowDictateNextSigner: false, signingOrder: DocumentSigningOrder.SEQUENTIAL, }, update: { allowDictateNextSigner: false, signingOrder: DocumentSigningOrder.SEQUENTIAL, }, }, }, }, }); const firstRecipient = recipients[0]; const { token, fields } = firstRecipient; const signUrl = `/sign/${token}`; await page.goto(signUrl); await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible(); await signSignaturePad(page); // Fill in all fields for (const field of fields) { await page.locator(`#field-${field.id}`).getByRole('button').click(); if (field.type === FieldType.TEXT) { await page.locator('#custom-text').fill('TEXT'); await page.getByRole('button', { name: 'Save' }).click(); } await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true'); } // Complete signing await page.getByRole('button', { name: 'Complete' }).click(); // Verify next recipient UI is not shown await expect( page.getByText('The next recipient to sign this document will be'), ).not.toBeVisible(); await expect(page.getByRole('button', { name: 'Update Recipient' })).not.toBeVisible(); // Submit and verify completion await page.getByRole('button', { name: 'Sign' }).click(); await page.waitForURL(`${signUrl}/complete`); // Verify document and recipient states const updatedDocument = await prisma.document.findUniqueOrThrow({ where: { id: document.id }, include: { recipients: { orderBy: { signingOrder: 'asc' }, }, }, }); // Document should still be pending as there are more recipients expect(updatedDocument.status).toBe(DocumentStatus.PENDING); // First recipient should be completed const updatedFirstRecipient = updatedDocument.recipients[0]; expect(updatedFirstRecipient.signingStatus).toBe(SigningStatus.SIGNED); // Second recipient should remain unchanged const updatedSecondRecipient = updatedDocument.recipients[1]; expect(updatedSecondRecipient.email).toBe(secondSigner.email); expect(updatedSecondRecipient.signingOrder).toBe(2); expect(updatedSecondRecipient.signingStatus).toBe(SigningStatus.NOT_SIGNED); }); test('[NEXT_RECIPIENT_DICTATION]: should work with parallel signing flow', async ({ page }) => { const user = await seedUser(); const firstSigner = await seedUser(); const secondSigner = await seedUser(); const { recipients, document } = await seedPendingDocumentWithFullFields({ owner: user, recipients: [firstSigner, secondSigner], recipientsCreateOptions: [{ signingOrder: 1 }, { signingOrder: 2 }], updateDocumentOptions: { documentMeta: { upsert: { create: { allowDictateNextSigner: false, signingOrder: DocumentSigningOrder.PARALLEL, }, update: { allowDictateNextSigner: false, signingOrder: DocumentSigningOrder.PARALLEL, }, }, }, }, }); // Test both recipients can sign in parallel for (const recipient of recipients) { const { token, fields } = recipient; const signUrl = `/sign/${token}`; await page.goto(signUrl); await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible(); await signSignaturePad(page); // Fill in all fields for (const field of fields) { await page.locator(`#field-${field.id}`).getByRole('button').click(); if (field.type === FieldType.TEXT) { await page.locator('#custom-text').fill('TEXT'); await page.getByRole('button', { name: 'Save' }).click(); } await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true'); } // Complete signing await page.getByRole('button', { name: 'Complete' }).click(); // Verify next recipient UI is not shown in parallel flow await expect( page.getByText('The next recipient to sign this document will be'), ).not.toBeVisible(); await expect(page.getByRole('button', { name: 'Update Recipient' })).not.toBeVisible(); // Submit and verify completion await page.getByRole('button', { name: 'Sign' }).click(); await page.waitForURL(`${signUrl}/complete`); } // Verify final document and recipient states await expect(async () => { const updatedDocument = await prisma.document.findUniqueOrThrow({ where: { id: document.id }, include: { recipients: { orderBy: { signingOrder: 'asc' }, }, }, }); // Document should be completed since all recipients have signed expect(updatedDocument.status).toBe(DocumentStatus.COMPLETED); // All recipients should be completed for (const recipient of updatedDocument.recipients) { expect(recipient.signingStatus).toBe(SigningStatus.SIGNED); } }).toPass(); }); test('[NEXT_RECIPIENT_DICTATION]: should allow assistant to dictate next signer', async ({ page, }) => { const user = await seedUser(); const assistant = await seedUser(); const signer = await seedUser(); const thirdSigner = await seedUser(); const { recipients, document } = await seedPendingDocumentWithFullFields({ owner: user, recipients: [assistant, signer, thirdSigner], recipientsCreateOptions: [ { signingOrder: 1, role: RecipientRole.ASSISTANT }, { signingOrder: 2, role: RecipientRole.SIGNER }, { signingOrder: 3, role: RecipientRole.SIGNER }, ], updateDocumentOptions: { documentMeta: { upsert: { create: { allowDictateNextSigner: true, signingOrder: DocumentSigningOrder.SEQUENTIAL, }, update: { allowDictateNextSigner: true, signingOrder: DocumentSigningOrder.SEQUENTIAL, }, }, }, }, }); const assistantRecipient = recipients[0]; const { token, fields } = assistantRecipient; const signUrl = `/sign/${token}`; await page.goto(signUrl); await expect(page.getByRole('heading', { name: 'Assist Document' })).toBeVisible(); await page.getByRole('radio', { name: assistantRecipient.name }).click(); // Fill in all fields for (const field of fields) { await page.locator(`#field-${field.id}`).getByRole('button').click(); if (field.type === FieldType.SIGNATURE) { await signDirectSignaturePad(page); await page.getByRole('button', { name: 'Sign', exact: true }).click(); } if (field.type === FieldType.TEXT) { await page.locator('#custom-text').fill('TEXT'); await page.getByRole('button', { name: 'Save' }).click(); } await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true'); } // Complete assisting and update next recipient await page.getByRole('button', { name: 'Continue' }).click(); // Verify next recipient info is shown await expect(page.getByRole('dialog')).toBeVisible(); await expect(page.getByText('The next recipient to sign this document will be')).toBeVisible(); // Use dialog context to ensure we're targeting the correct form fields const dialog = page.getByRole('dialog'); await dialog.getByLabel('Name').fill('New Signer'); await dialog.getByLabel('Email').fill('new.signer@example.com'); // Submit and verify completion await page.getByRole('button', { name: /Continue|Proceed/i }).click(); await page.waitForURL(`${signUrl}/complete`); // Verify document and recipient states await expect(async () => { const updatedDocument = await prisma.document.findUniqueOrThrow({ where: { id: document.id }, include: { recipients: { orderBy: { signingOrder: 'asc' }, }, }, }); // Document should still be pending as there are more recipients expect(updatedDocument.status).toBe(DocumentStatus.PENDING); // Assistant should be completed const updatedAssistant = updatedDocument.recipients[0]; expect(updatedAssistant.signingStatus).toBe(SigningStatus.SIGNED); expect(updatedAssistant.role).toBe(RecipientRole.ASSISTANT); // Second recipient should be the new signer const updatedSigner = updatedDocument.recipients[1]; expect(updatedSigner.name).toBe('New Signer'); expect(updatedSigner.email).toBe('new.signer@example.com'); expect(updatedSigner.signingOrder).toBe(2); expect(updatedSigner.signingStatus).toBe(SigningStatus.NOT_SIGNED); expect(updatedSigner.role).toBe(RecipientRole.SIGNER); // Third recipient should remain unchanged const thirdRecipient = updatedDocument.recipients[2]; expect(thirdRecipient.email).toBe(thirdSigner.email); expect(thirdRecipient.signingOrder).toBe(3); expect(thirdRecipient.signingStatus).toBe(SigningStatus.NOT_SIGNED); expect(thirdRecipient.role).toBe(RecipientRole.SIGNER); }).toPass(); });