mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 17:35:05 +10:00
fix: sort CC recipients last (#2930)
This commit is contained in:
@@ -2,7 +2,14 @@ import { prisma } from '@documenso/prisma';
|
||||
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { DocumentSigningOrder, DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentStatus,
|
||||
FieldType,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { signDirectSignaturePad, signSignaturePad } from '../fixtures/signature';
|
||||
|
||||
@@ -370,3 +377,221 @@ test('[NEXT_RECIPIENT_DICTATION]: should allow assistant to dictate next signer'
|
||||
expect(thirdRecipient.role).toBe(RecipientRole.SIGNER);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should skip CC recipient when dictating next signer', async ({ page }) => {
|
||||
const { user, team } = await seedUser();
|
||||
const { user: firstSigner } = await seedUser();
|
||||
const { user: ccUser } = await seedUser();
|
||||
const { user: secondSigner } = await seedUser();
|
||||
|
||||
const { recipients, document } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
teamId: team.id,
|
||||
recipients: [firstSigner, ccUser, secondSigner],
|
||||
recipientsCreateOptions: [
|
||||
{ signingOrder: 1 },
|
||||
{
|
||||
// CC recipients are created pre-signed, mirroring production behaviour.
|
||||
signingOrder: 2,
|
||||
role: RecipientRole.CC,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
},
|
||||
{ signingOrder: 3 },
|
||||
],
|
||||
updateDocumentOptions: {
|
||||
documentMeta: {
|
||||
upsert: {
|
||||
create: {
|
||||
allowDictateNextSigner: true,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
update: {
|
||||
allowDictateNextSigner: true,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const firstRecipient = recipients.find((r) => r.email === firstSigner.email);
|
||||
const ccRecipient = recipients.find((r) => r.email === ccUser.email);
|
||||
|
||||
if (!firstRecipient || !ccRecipient) {
|
||||
throw new Error('Recipients not found');
|
||||
}
|
||||
|
||||
// CC recipients cannot have fields.
|
||||
await prisma.field.deleteMany({
|
||||
where: {
|
||||
recipientId: ccRecipient.id,
|
||||
},
|
||||
});
|
||||
|
||||
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 verify the offered next recipient
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
await expect(page.getByText('Next Recipient Name')).toBeVisible();
|
||||
|
||||
// The dictation dialog must offer the second signer, not the CC recipient.
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog.getByLabel('Name')).toHaveValue(secondSigner.name ?? '');
|
||||
await expect(dialog.getByLabel('Email')).toHaveValue(secondSigner.email);
|
||||
|
||||
// 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.envelope.findUniqueOrThrow({
|
||||
where: { id: document.id },
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: { signingOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Document should still be pending as the second signer has not signed
|
||||
expect(updatedDocument.status).toBe(DocumentStatus.PENDING);
|
||||
|
||||
// The CC recipient must remain untouched
|
||||
const updatedCcRecipient = updatedDocument.recipients[1];
|
||||
expect(updatedCcRecipient.email).toBe(ccUser.email);
|
||||
expect(updatedCcRecipient.role).toBe(RecipientRole.CC);
|
||||
expect(updatedCcRecipient.signingStatus).toBe(SigningStatus.SIGNED);
|
||||
|
||||
// The second signer must remain the next pending recipient
|
||||
const updatedSecondRecipient = updatedDocument.recipients[2];
|
||||
expect(updatedSecondRecipient.email).toBe(secondSigner.email);
|
||||
expect(updatedSecondRecipient.signingOrder).toBe(3);
|
||||
expect(updatedSecondRecipient.signingStatus).toBe(SigningStatus.NOT_SIGNED);
|
||||
});
|
||||
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should not offer dictation when CC recipient is last', async ({ page }) => {
|
||||
const { user, team } = await seedUser();
|
||||
const { user: firstSigner } = await seedUser();
|
||||
const { user: secondSigner } = await seedUser();
|
||||
const { user: ccUser } = await seedUser();
|
||||
|
||||
const { recipients, document } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
teamId: team.id,
|
||||
recipients: [firstSigner, secondSigner, ccUser],
|
||||
recipientsCreateOptions: [
|
||||
{ signingOrder: 1 },
|
||||
{ signingOrder: 2 },
|
||||
{
|
||||
// CC recipients are created pre-signed, mirroring production behaviour.
|
||||
signingOrder: 3,
|
||||
role: RecipientRole.CC,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
},
|
||||
],
|
||||
updateDocumentOptions: {
|
||||
documentMeta: {
|
||||
upsert: {
|
||||
create: {
|
||||
allowDictateNextSigner: true,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
update: {
|
||||
allowDictateNextSigner: true,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const firstRecipient = recipients.find((r) => r.email === firstSigner.email);
|
||||
const secondRecipient = recipients.find((r) => r.email === secondSigner.email);
|
||||
const ccRecipient = recipients.find((r) => r.email === ccUser.email);
|
||||
|
||||
if (!firstRecipient || !secondRecipient || !ccRecipient) {
|
||||
throw new Error('Recipients not found');
|
||||
}
|
||||
|
||||
// CC recipients cannot have fields.
|
||||
await prisma.field.deleteMany({
|
||||
where: {
|
||||
recipientId: ccRecipient.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Sign as both signers in order.
|
||||
for (const recipient of [firstRecipient, secondRecipient]) {
|
||||
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();
|
||||
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
|
||||
if (recipient.id === secondRecipient.id) {
|
||||
// The last actionable signer must not be offered the CC recipient.
|
||||
await expect(page.getByText('Next Recipient Name')).not.toBeVisible();
|
||||
}
|
||||
|
||||
// Submit and verify completion
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
}
|
||||
|
||||
// The document completes without any action from the CC recipient.
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const finalDocument = await prisma.envelope.findUniqueOrThrow({
|
||||
where: { id: document.id },
|
||||
});
|
||||
|
||||
return finalDocument.status;
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toBe(DocumentStatus.COMPLETED);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user