This commit is contained in:
David Nguyen
2026-08-26 12:10:29 +10:00
parent 9dc83bdb06
commit 6db71b13d4
70 changed files with 5964 additions and 892 deletions
@@ -0,0 +1,150 @@
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
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, RecipientRole, SendStatus, SigningStatus } from '@prisma/client';
/**
* A viewer's completion dialog activates the next-signer validator from
* `allowDictateNextSigner` alone, while the name/email inputs only render
* when a dictatable next recipient exists. When dictation is enabled but the
* next step is not dictatable (a group of two or more, or the viewer is
* last), submission must still work — historically it failed Zod validation
* on the hidden inputs and "Mark as Viewed" silently did nothing.
*/
const seedViewerDictationDocument = async (
recipientsCreateOptions: {
signingOrder: number;
role?: RecipientRole;
sendStatus?: SendStatus;
}[],
) => {
const { user, team } = await seedUser();
const signers = await Promise.all(recipientsCreateOptions.map(async () => (await seedUser()).user));
const { recipients, document } = await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: signers,
recipientsCreateOptions,
// No fields, so completion is not blocked by unsigned required fields.
fields: [],
updateDocumentOptions: {
documentMeta: {
upsert: {
create: {
signingOrder: DocumentSigningOrder.SEQUENTIAL,
allowDictateNextSigner: true,
},
update: {
signingOrder: DocumentSigningOrder.SEQUENTIAL,
allowDictateNextSigner: true,
},
},
},
},
});
return { document, recipients };
};
test('[NEXT_RECIPIENT_DICTATION]: viewer can mark as viewed when the next step is a group', async ({ page }) => {
const { document, recipients } = await seedViewerDictationDocument([
{ signingOrder: 1, role: RecipientRole.VIEWER },
// The next step is a group of two, so there is no single dictatable next
// recipient — the dialog must not demand one.
{ signingOrder: 2, sendStatus: SendStatus.NOT_SENT },
{ signingOrder: 2, sendStatus: SendStatus.NOT_SENT },
]);
const [viewer, groupA, groupB] = recipients;
const signUrl = `/sign/${viewer.token}`;
await page.goto(signUrl);
await expect(page.getByRole('heading', { name: 'View Document' })).toBeVisible();
const dialog = page.getByRole('dialog');
// Retry the click: it can land before hydration attaches the handler.
await expect(async () => {
await page.getByRole('button', { name: 'Mark as viewed', exact: true }).click();
await expect(dialog).toBeVisible({ timeout: 2_000 });
}).toPass();
// No dictation inputs: a group cannot be dictated over.
await expect(dialog.getByText('Next Recipient Name')).not.toBeVisible();
await dialog.getByRole('button', { name: 'Mark as Viewed', exact: true }).click();
await page.waitForURL(`${signUrl}/complete`);
// The viewer completed and the group's step unlocked.
await expect
.poll(async () => {
const updatedRecipients = await prisma.recipient.findMany({
where: { envelopeId: document.id },
orderBy: { id: 'asc' },
});
return updatedRecipients.map((recipient) => [recipient.signingStatus, recipient.sendStatus]);
})
.toEqual([
[SigningStatus.SIGNED, SendStatus.SENT],
[SigningStatus.NOT_SIGNED, SendStatus.SENT],
[SigningStatus.NOT_SIGNED, SendStatus.SENT],
]);
// Nobody was renamed: no next-signer values existed to apply.
const groupAAfter = await prisma.recipient.findUniqueOrThrow({ where: { id: groupA.id } });
const groupBAfter = await prisma.recipient.findUniqueOrThrow({ where: { id: groupB.id } });
expect(groupAAfter.email).toBe(groupA.email);
expect(groupBAfter.email).toBe(groupB.email);
});
test('[NEXT_RECIPIENT_DICTATION]: viewer can mark as viewed when they are the last recipient', async ({ page }) => {
const { recipients } = await seedViewerDictationDocument([
{ signingOrder: 1 },
{ signingOrder: 2, role: RecipientRole.VIEWER, sendStatus: SendStatus.NOT_SENT },
]);
const [signer, viewer] = recipients;
// Advance the flow to the viewer's turn.
await completeDocumentWithToken({
token: signer.token,
id: { type: 'envelopeId', id: signer.envelopeId },
});
const signUrl = `/sign/${viewer.token}`;
await page.goto(signUrl);
await expect(page.getByRole('heading', { name: 'View Document' })).toBeVisible();
const dialog = page.getByRole('dialog');
// Retry the click: it can land before hydration attaches the handler.
await expect(async () => {
await page.getByRole('button', { name: 'Mark as viewed', exact: true }).click();
await expect(dialog).toBeVisible({ timeout: 2_000 });
}).toPass();
// No dictation inputs: there is nobody after the viewer.
await expect(dialog.getByText('Next Recipient Name')).not.toBeVisible();
await dialog.getByRole('button', { name: 'Mark as Viewed', exact: true }).click();
await page.waitForURL(`${signUrl}/complete`);
await expect
.poll(async () => {
const viewerAfter = await prisma.recipient.findUniqueOrThrow({ where: { id: viewer.id } });
return viewerAfter.signingStatus;
})
.toBe(SigningStatus.SIGNED);
});