mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
Fix webhooks being sent twice due to duplicate frontend calls Updated the assistant confirmation dialog so the next signer is always visible (if dictate is enabled). Because if the form is invalid (due to no name) there is no visual queue that the form is invalid (since it's hidden) ## Notes Didn't bother to remove the weird assistants form since it currently works for now  ## Tests - Currently running locally - Tested webhooks via network tab and via webhook.site
388 lines
13 KiB
TypeScript
388 lines
13 KiB
TypeScript
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();
|
|
});
|