Merge branch 'main' into feat/add-pdf-image-renderer

This commit is contained in:
David Nguyen
2026-02-24 18:51:04 +11:00
179 changed files with 14753 additions and 3181 deletions
@@ -171,6 +171,7 @@ test.describe('API V2 Envelopes', () => {
positionY: 0,
width: 0,
height: 0,
fieldMeta: { type: 'signature' },
},
{
type: FieldType.SIGNATURE,
@@ -180,6 +181,7 @@ test.describe('API V2 Envelopes', () => {
positionY: 0,
width: 0,
height: 0,
fieldMeta: { type: 'signature' },
},
],
},
@@ -205,6 +207,7 @@ test.describe('API V2 Envelopes', () => {
documentPending: false,
documentCompleted: false,
documentDeleted: false,
ownerRecipientExpired: true,
ownerDocumentCompleted: true,
},
},
@@ -107,10 +107,12 @@ test.describe('AutoSave Subject Step', () => {
const { user, document, team } = await setupDocumentAndNavigateToSubjectStep(page);
// Toggle some email settings checkboxes (randomly - some checked, some unchecked)
await page.getByText('Send recipient signed email').click();
await page.getByText('Send recipient removed email').click();
await page.getByText('Send document completed email', { exact: true }).click();
await page.getByText('Send document deleted email').click();
await page.getByText('Email the owner when a recipient signs').click();
await page.getByText("Email recipients when they're removed from a pending document").click();
await page
.getByText('Email recipients when the document is completed', { exact: true })
.click();
await page.getByText('Email recipients when a pending document is deleted').click();
await triggerAutosave(page);
@@ -127,26 +129,34 @@ test.describe('AutoSave Subject Step', () => {
const emailSettings = retrievedDocumentData.documentMeta?.emailSettings;
await expect(page.getByText('Send recipient signed email')).toBeChecked({
await expect(page.getByText('Email the owner when a recipient signs')).toBeChecked({
checked: emailSettings?.recipientSigned,
});
await expect(page.getByText('Send recipient removed email')).toBeChecked({
await expect(
page.getByText("Email recipients when they're removed from a pending document"),
).toBeChecked({
checked: emailSettings?.recipientRemoved,
});
await expect(page.getByText('Send document completed email', { exact: true })).toBeChecked({
await expect(
page.getByText('Email recipients when the document is completed', { exact: true }),
).toBeChecked({
checked: emailSettings?.documentCompleted,
});
await expect(page.getByText('Send document deleted email')).toBeChecked({
await expect(
page.getByText('Email recipients when a pending document is deleted'),
).toBeChecked({
checked: emailSettings?.documentDeleted,
});
await expect(page.getByText('Send recipient signing request email')).toBeChecked({
await expect(page.getByText('Email recipients with a signing request')).toBeChecked({
checked: emailSettings?.recipientSigningRequest,
});
await expect(page.getByText('Send document pending email')).toBeChecked({
checked: emailSettings?.documentPending,
});
await expect(page.getByText('Send document completed email to the owner')).toBeChecked({
await expect(page.getByText('Email the signer if the document is still pending')).toBeChecked(
{
checked: emailSettings?.documentPending,
},
);
await expect(page.getByText('Email the owner when the document is completed')).toBeChecked({
checked: emailSettings?.ownerDocumentCompleted,
});
}).toPass();
@@ -162,10 +172,12 @@ test.describe('AutoSave Subject Step', () => {
await page.getByRole('textbox', { name: 'Subject (Optional)' }).fill(subject);
await page.getByRole('textbox', { name: 'Message (Optional)' }).fill(message);
await page.getByText('Send recipient signed email').click();
await page.getByText('Send recipient removed email').click();
await page.getByText('Send document completed email', { exact: true }).click();
await page.getByText('Send document deleted email').click();
await page.getByText('Email the owner when a recipient signs').click();
await page.getByText("Email recipients when they're removed from a pending document").click();
await page
.getByText('Email recipients when the document is completed', { exact: true })
.click();
await page.getByText('Email recipients when a pending document is deleted').click();
await triggerAutosave(page);
@@ -191,26 +203,34 @@ test.describe('AutoSave Subject Step', () => {
retrievedDocumentData.documentMeta?.message ?? '',
);
await expect(page.getByText('Send recipient signed email')).toBeChecked({
await expect(page.getByText('Email the owner when a recipient signs')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientSigned,
});
await expect(page.getByText('Send recipient removed email')).toBeChecked({
await expect(
page.getByText("Email recipients when they're removed from a pending document"),
).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientRemoved,
});
await expect(page.getByText('Send document completed email', { exact: true })).toBeChecked({
await expect(
page.getByText('Email recipients when the document is completed', { exact: true }),
).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentCompleted,
});
await expect(page.getByText('Send document deleted email')).toBeChecked({
await expect(
page.getByText('Email recipients when a pending document is deleted'),
).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentDeleted,
});
await expect(page.getByText('Send recipient signing request email')).toBeChecked({
await expect(page.getByText('Email recipients with a signing request')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientSigningRequest,
});
await expect(page.getByText('Send document pending email')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentPending,
});
await expect(page.getByText('Send document completed email to the owner')).toBeChecked({
await expect(page.getByText('Email the signer if the document is still pending')).toBeChecked(
{
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentPending,
},
);
await expect(page.getByText('Email the owner when the document is completed')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.ownerDocumentCompleted,
});
}).toPass();
@@ -0,0 +1,218 @@
import { type APIRequestContext, type Page, expect, test } from '@playwright/test';
import { DocumentStatus, EnvelopeType, FieldType } from '@prisma/client';
import fs from 'node:fs';
import path from 'node:path';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
import { prisma } from '@documenso/prisma';
import { seedUser } from '@documenso/prisma/seed/users';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../lib/constants/app';
import { createApiToken } from '../../../lib/server-only/public-api/create-api-token';
import { RecipientRole } from '../../../prisma/generated/types';
import type {
TCreateEnvelopePayload,
TCreateEnvelopeResponse,
} from '../../../trpc/server/envelope-router/create-envelope.types';
import type { TDistributeEnvelopeRequest } from '../../../trpc/server/envelope-router/distribute-envelope.types';
import { apiSignin } from '../fixtures/authentication';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
const baseUrl = `${WEBAPP_BASE_URL}/api/v2`;
test.describe.configure({ mode: 'parallel', timeout: 60000 });
const signAndVerifyPageDimensions = async ({
page,
request,
pdfFile,
identifier,
title,
expectedWidth,
expectedHeight,
}: {
page: Page;
request: APIRequestContext;
pdfFile: string;
identifier: string;
title: string;
expectedWidth: number;
expectedHeight: number;
}) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const pdfBuffer = fs.readFileSync(path.join(__dirname, `../../../../assets/${pdfFile}`));
const formData = new FormData();
const createEnvelopePayload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title,
recipients: [
{
email: user.email,
name: user.name || '',
role: RecipientRole.SIGNER,
fields: [
{
identifier,
type: FieldType.SIGNATURE,
fieldMeta: { type: 'signature' },
page: 1,
positionX: 10,
positionY: 10,
width: 40,
height: 10,
},
],
},
],
};
formData.append('payload', JSON.stringify(createEnvelopePayload));
formData.append('files', new File([pdfBuffer], identifier, { type: 'application/pdf' }));
const createResponse = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(createResponse.ok()).toBeTruthy();
const { id: envelopeId }: TCreateEnvelopeResponse = await createResponse.json();
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: envelopeId },
include: { recipients: true },
});
const distributeResponse = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${token}` },
data: { envelopeId: envelope.id } satisfies TDistributeEnvelopeRequest,
});
expect(distributeResponse.ok()).toBeTruthy();
// Pre-insert all fields via Prisma so we can skip the UI field interaction.
const fields = await prisma.field.findMany({
where: { envelopeId: envelope.id, inserted: false },
});
for (const field of fields) {
await prisma.field.update({
where: { id: field.id },
data: {
inserted: true,
signature: {
create: {
recipientId: envelope.recipients[0].id,
typedSignature: 'Test Signature',
},
},
},
});
}
const recipientToken = envelope.recipients[0].token;
const signUrl = `/sign/${recipientToken}`;
await apiSignin({
page,
email: user.email,
redirectPath: signUrl,
});
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(`${signUrl}/complete`);
await expect(async () => {
const { status } = await prisma.envelope.findFirstOrThrow({
where: { id: envelope.id },
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass({ timeout: 10000 });
const completedEnvelope = await prisma.envelope.findFirstOrThrow({
where: { id: envelope.id },
include: {
envelopeItems: {
orderBy: { order: 'asc' },
include: { documentData: true },
},
},
});
for (const item of completedEnvelope.envelopeItems) {
const documentUrl = getEnvelopeItemPdfUrl({
type: 'download',
envelopeItem: item,
token: recipientToken,
version: 'signed',
});
const pdfData = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
const loadingTask = pdfjsLib.getDocument({ data: new Uint8Array(pdfData) });
const pdf = await loadingTask.promise;
expect(pdf.numPages).toBeGreaterThan(1);
for (let i = 1; i <= pdf.numPages; i++) {
const pdfPage = await pdf.getPage(i);
const viewport = pdfPage.getViewport({ scale: 1 });
expect(Math.round(viewport.width)).toBe(expectedWidth);
expect(Math.round(viewport.height)).toBe(expectedHeight);
}
}
};
test('cert and audit log pages match letter page dimensions', async ({ page, request }) => {
await signAndVerifyPageDimensions({
page,
request,
pdfFile: 'letter-size.pdf',
identifier: 'letter-doc',
title: 'Letter Size Dimension Test',
expectedWidth: 612,
expectedHeight: 792,
});
});
test('cert and audit log pages match A4 page dimensions', async ({ page, request }) => {
await signAndVerifyPageDimensions({
page,
request,
pdfFile: 'a4-size.pdf',
identifier: 'a4-doc',
title: 'A4 Size Dimension Test',
expectedWidth: 595,
expectedHeight: 842,
});
});
test('cert and audit log pages match tabloid landscape page dimensions', async ({
page,
request,
}) => {
await signAndVerifyPageDimensions({
page,
request,
pdfFile: 'tabloid-landscape.pdf',
identifier: 'tabloid-doc',
title: 'Tabloid Landscape Dimension Test',
expectedWidth: 1224,
expectedHeight: 792,
});
});
@@ -297,18 +297,34 @@ test('field placement visual regression', async ({ page, request }, testInfo) =>
*
* DON'T COMMIT THIS WITHOUT THE "SKIP" COMMAND.
*/
test.skip('download envelope images', async ({ page }) => {
test.skip('download envelope images', async ({ page, request }) => {
const { user, team } = await seedUser();
const { token: apiToken } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const envelope = await seedAlignmentTestDocument({
userId: user.id,
teamId: team.id,
recipientName: user.name || '',
recipientEmail: user.email,
insertFields: true,
status: DocumentStatus.PENDING,
status: DocumentStatus.DRAFT,
});
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${apiToken}` },
data: {
envelopeId: envelope.id,
} satisfies TDistributeEnvelopeRequest,
});
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
const token = envelope.recipients[0].token;
const signUrl = `/sign/${token}`;
@@ -0,0 +1,291 @@
import { expect, test } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { prisma } from '@documenso/prisma';
import { EnvelopeType, RecipientRole } from '@documenso/prisma/client';
import { seedPendingDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import type {
TCreateEnvelopePayload,
TCreateEnvelopeResponse,
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
import type { TDistributeEnvelopeRequest } from '@documenso/trpc/server/envelope-router/distribute-envelope.types';
import { apiSignin } from '../fixtures/authentication';
import { openDropdownMenu } from '../fixtures/generic';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
const baseUrl = `${WEBAPP_BASE_URL}/api/v2-beta`;
const examplePdf = fs.readFileSync(path.join(__dirname, '../../../../assets/example.pdf'));
test.describe.configure({ mode: 'parallel' });
test('[ENVELOPE_EXPIRATION]: sending document sets expiresAt on recipients', async ({
request,
}) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test-expiration-send',
expiresIn: null,
});
const createPayload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: '[TEST] Expiration Send Test',
recipients: [
{
email: 'signer-expiry@test.documenso.com',
name: 'Signer Expiry',
role: RecipientRole.SIGNER,
fields: [
{
type: 'SIGNATURE',
page: 1,
positionX: 10,
positionY: 10,
width: 10,
height: 5,
fieldMeta: { type: 'signature' },
},
],
},
],
};
const formData = new FormData();
formData.append('payload', JSON.stringify(createPayload));
formData.append('files', new File([examplePdf], 'example.pdf', { type: 'application/pdf' }));
const createRes = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(createRes.ok()).toBeTruthy();
const { id: envelopeId }: TCreateEnvelopeResponse = await createRes.json();
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${token}` },
data: { envelopeId } satisfies TDistributeEnvelopeRequest,
});
expect(distributeRes.ok()).toBeTruthy();
// Check that recipients now have expiresAt set.
const recipients = await prisma.recipient.findMany({
where: { envelopeId },
});
expect(recipients.length).toBe(1);
expect(recipients[0].expiresAt).not.toBeNull();
// The default expiration period is 3 months. Verify it's roughly correct.
const expiresAt = recipients[0].expiresAt!;
const now = new Date();
const diffMs = expiresAt.getTime() - now.getTime();
const diffDays = diffMs / (1000 * 60 * 60 * 24);
// 3 months is roughly 89-92 days. Allow a generous range.
expect(diffDays).toBeGreaterThan(80);
expect(diffDays).toBeLessThan(100);
});
test('[ENVELOPE_EXPIRATION]: sending document with custom org expiration period', async ({
request,
}) => {
const { user, organisation, team } = await seedUser();
// Set org expiration to 7 days.
await prisma.organisationGlobalSettings.update({
where: { id: organisation.organisationGlobalSettingsId },
data: { envelopeExpirationPeriod: { unit: 'day', amount: 7 } },
});
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test-expiration-custom',
expiresIn: null,
});
const createPayload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: '[TEST] Custom Expiration Send Test',
recipients: [
{
email: 'signer-custom@test.documenso.com',
name: 'Signer Custom',
role: RecipientRole.SIGNER,
fields: [
{
type: 'SIGNATURE',
page: 1,
positionX: 10,
positionY: 10,
width: 10,
height: 5,
fieldMeta: { type: 'signature' },
},
],
},
],
};
const formData = new FormData();
formData.append('payload', JSON.stringify(createPayload));
formData.append('files', new File([examplePdf], 'example.pdf', { type: 'application/pdf' }));
const createRes = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(createRes.ok()).toBeTruthy();
const { id: envelopeId }: TCreateEnvelopeResponse = await createRes.json();
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${token}` },
data: { envelopeId } satisfies TDistributeEnvelopeRequest,
});
expect(distributeRes.ok()).toBeTruthy();
const recipients = await prisma.recipient.findMany({
where: { envelopeId },
});
expect(recipients.length).toBe(1);
expect(recipients[0].expiresAt).not.toBeNull();
// 7 days expiration.
const expiresAt = recipients[0].expiresAt!;
const now = new Date();
const diffMs = expiresAt.getTime() - now.getTime();
const diffDays = diffMs / (1000 * 60 * 60 * 24);
expect(diffDays).toBeGreaterThan(6);
expect(diffDays).toBeLessThan(8);
});
test('[ENVELOPE_EXPIRATION]: sending document with expiration disabled', async ({ request }) => {
const { user, organisation, team } = await seedUser();
// Disable expiration at org level.
await prisma.organisationGlobalSettings.update({
where: { id: organisation.organisationGlobalSettingsId },
data: { envelopeExpirationPeriod: { disabled: true } },
});
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test-expiration-disabled',
expiresIn: null,
});
const createPayload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: '[TEST] Disabled Expiration Send Test',
recipients: [
{
email: 'signer-disabled@test.documenso.com',
name: 'Signer Disabled',
role: RecipientRole.SIGNER,
fields: [
{
type: 'SIGNATURE',
page: 1,
positionX: 10,
positionY: 10,
width: 10,
height: 5,
fieldMeta: { type: 'signature' },
},
],
},
],
};
const formData = new FormData();
formData.append('payload', JSON.stringify(createPayload));
formData.append('files', new File([examplePdf], 'example.pdf', { type: 'application/pdf' }));
const createRes = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(createRes.ok()).toBeTruthy();
const { id: envelopeId }: TCreateEnvelopeResponse = await createRes.json();
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${token}` },
data: { envelopeId } satisfies TDistributeEnvelopeRequest,
});
expect(distributeRes.ok()).toBeTruthy();
const recipients = await prisma.recipient.findMany({
where: { envelopeId },
});
expect(recipients.length).toBe(1);
expect(recipients[0].expiresAt).toBeNull();
});
test('[ENVELOPE_EXPIRATION]: resending refreshes expiresAt', async ({ page }) => {
const { user, team } = await seedUser();
const document = await seedPendingDocument(user, team.id, ['resend-target@test.documenso.com']);
const recipient = document.recipients[0];
// Set an initial expiresAt that's 1 day from now.
const initialExpiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
await prisma.recipient.update({
where: { id: recipient.id },
data: { expiresAt: initialExpiresAt },
});
await apiSignin({
page,
email: user.email,
redirectPath: `/t/${team.url}/documents?status=PENDING`,
});
// Open the document action menu and click Resend.
const actionBtn = page.getByTestId('document-table-action-btn').first();
await expect(actionBtn).toBeAttached();
await openDropdownMenu(page, actionBtn);
await expect(page.getByRole('menuitem', { name: 'Resend' })).toBeVisible();
await page.getByRole('menuitem', { name: 'Resend' }).click();
// Select the recipient and send.
await page.getByLabel('test.documenso.com').first().click();
await page.getByRole('button', { name: 'Send reminder' }).click();
await expect(page.getByText('Document re-sent', { exact: true })).toBeVisible({
timeout: 10_000,
});
// Verify expiresAt was refreshed.
await expect(async () => {
const updatedRecipient = await prisma.recipient.findUniqueOrThrow({
where: { id: recipient.id },
});
expect(updatedRecipient.expiresAt).not.toBeNull();
expect(updatedRecipient.expiresAt!.getTime()).toBeGreaterThan(initialExpiresAt.getTime());
}).toPass({ timeout: 10_000 });
});
@@ -0,0 +1,150 @@
import { expect, test } from '@playwright/test';
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
import { prisma } from '@documenso/prisma';
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../fixtures/authentication';
test.describe.configure({ mode: 'parallel' });
test('[ENVELOPE_EXPIRATION]: set custom expiration period at organisation level', async ({
page,
}) => {
const { user, organisation } = await seedUser({
isPersonalOrganisation: false,
});
await apiSignin({
page,
email: user.email,
redirectPath: `/o/${organisation.url}/settings/document`,
});
// Wait for the form to load.
await expect(page.getByRole('button', { name: 'Update' }).first()).toBeVisible();
// Change the amount to 2.
const amountInput = page.getByRole('spinbutton');
await amountInput.clear();
await amountInput.fill('2');
// Find all triggers, the unit picker is the one showing Months/Days/etc.
// In the duration mode, there's a mode select and a unit select.
// The unit select is inside the duration row, after the number input.
// Let's find the select trigger that contains the unit text.
const unitTrigger = page
.locator('button[role="combobox"]')
.filter({ hasText: /Months|Days|Weeks|Years/ });
await unitTrigger.click();
await page.getByRole('option', { name: 'Weeks' }).click();
await page.getByRole('button', { name: 'Update' }).first().click();
await expect(page.getByText('Your document preferences have been updated').first()).toBeVisible();
// Verify via database.
const orgSettings = await prisma.organisationGlobalSettings.findUniqueOrThrow({
where: { id: organisation.organisationGlobalSettingsId },
});
expect(orgSettings.envelopeExpirationPeriod).toEqual({ unit: 'week', amount: 2 });
});
test('[ENVELOPE_EXPIRATION]: disable expiration at organisation level', async ({ page }) => {
const { user, organisation } = await seedUser({
isPersonalOrganisation: false,
});
await apiSignin({
page,
email: user.email,
redirectPath: `/o/${organisation.url}/settings/document`,
});
await expect(page.getByRole('button', { name: 'Update' }).first()).toBeVisible();
// Find the mode select (shows "Custom duration") and change to "Never expires".
const modeTrigger = page
.locator('button[role="combobox"]')
.filter({ hasText: 'Custom duration' });
await modeTrigger.click();
await page.getByRole('option', { name: 'Never expires' }).click();
await page.getByRole('button', { name: 'Update' }).first().click();
await expect(page.getByText('Your document preferences have been updated').first()).toBeVisible();
// Verify via database.
const orgSettings = await prisma.organisationGlobalSettings.findUniqueOrThrow({
where: { id: organisation.organisationGlobalSettingsId },
});
expect(orgSettings.envelopeExpirationPeriod).toEqual({ disabled: true });
});
test('[ENVELOPE_EXPIRATION]: team inherits expiration from organisation', async () => {
const { organisation, team } = await seedUser({
isPersonalOrganisation: false,
});
// Set org expiration to 2 weeks directly.
await prisma.organisationGlobalSettings.update({
where: { id: organisation.organisationGlobalSettingsId },
data: { envelopeExpirationPeriod: { unit: 'week', amount: 2 } },
});
// Verify team settings inherit the org setting.
const teamSettings = await getTeamSettings({ teamId: team.id });
expect(teamSettings.envelopeExpirationPeriod).toEqual({ unit: 'week', amount: 2 });
});
test('[ENVELOPE_EXPIRATION]: team overrides organisation expiration', async ({ page }) => {
const { user, organisation, team } = await seedUser({
isPersonalOrganisation: false,
});
// Set org expiration to 2 weeks.
await prisma.organisationGlobalSettings.update({
where: { id: organisation.organisationGlobalSettingsId },
data: { envelopeExpirationPeriod: { unit: 'week', amount: 2 } },
});
await apiSignin({
page,
email: user.email,
redirectPath: `/t/${team.url}/settings/document`,
});
await expect(page.getByRole('button', { name: 'Update' }).first()).toBeVisible();
// Scope to the "Default Envelope Expiration" form field section.
const expirationSection = page.getByText('Default Envelope Expiration').locator('..');
// The expiration picker mode select should show "Inherit from organisation" by default.
const modeTrigger = expirationSection.locator('button[role="combobox"]').first();
await expect(modeTrigger).toBeVisible();
// Switch to custom duration.
await modeTrigger.click();
await page.getByRole('option', { name: 'Custom duration' }).click();
// Set to 5 days.
const amountInput = expirationSection.getByRole('spinbutton');
await amountInput.clear();
await amountInput.fill('5');
const unitTrigger = expirationSection
.locator('button[role="combobox"]')
.filter({ hasText: /Months|Days|Weeks|Years/ });
await unitTrigger.click();
await page.getByRole('option', { name: 'Days' }).click();
await page.getByRole('button', { name: 'Update' }).first().click();
await expect(page.getByText('Your document preferences have been updated').first()).toBeVisible();
// Verify team setting is overridden.
const teamSettings = await getTeamSettings({ teamId: team.id });
expect(teamSettings.envelopeExpirationPeriod).toEqual({ unit: 'day', amount: 5 });
});
@@ -0,0 +1,131 @@
import { expect, test } from '@playwright/test';
import { prisma } from '@documenso/prisma';
import { FieldType } from '@documenso/prisma/client';
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../fixtures/authentication';
import { signSignaturePad } from '../fixtures/signature';
test.describe.configure({ mode: 'parallel' });
test('[ENVELOPE_EXPIRATION]: expired recipient is redirected to expired page', async ({ page }) => {
const { user, team } = await seedUser();
const { recipients } = await seedPendingDocumentWithFullFields({
owner: user,
recipients: ['expired-recipient@test.documenso.com'],
teamId: team.id,
});
const recipient = recipients[0];
// Set expiresAt to the past so the recipient is expired.
await prisma.recipient.update({
where: { id: recipient.id },
data: { expiresAt: new Date(Date.now() - 60_000) },
});
await page.goto(`/sign/${recipient.token}`);
await page.waitForURL(`/sign/${recipient.token}/expired`);
await expect(page.getByText('Signing Deadline Expired')).toBeVisible();
await expect(page.getByText('The signing deadline for this document has passed')).toBeVisible();
});
test('[ENVELOPE_EXPIRATION]: non-expired recipient can access signing page', async ({ page }) => {
const { user, team } = await seedUser();
const { recipients } = await seedPendingDocumentWithFullFields({
owner: user,
recipients: ['active-recipient@test.documenso.com'],
teamId: team.id,
});
const recipient = recipients[0];
// Set expiresAt to 1 hour in the future.
await prisma.recipient.update({
where: { id: recipient.id },
data: { expiresAt: new Date(Date.now() + 60 * 60 * 1000) },
});
await page.goto(`/sign/${recipient.token}`);
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
});
test('[ENVELOPE_EXPIRATION]: recipient with null expiresAt can sign normally', async ({ page }) => {
const { user, team } = await seedUser();
const { recipients } = await seedPendingDocumentWithFullFields({
owner: user,
recipients: ['null-expiry@test.documenso.com'],
teamId: team.id,
});
const recipient = recipients[0];
// Verify expiresAt is null (default from seed).
const dbRecipient = await prisma.recipient.findUniqueOrThrow({
where: { id: recipient.id },
});
expect(dbRecipient.expiresAt).toBeNull();
await page.goto(`/sign/${recipient.token}`);
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
});
test('[ENVELOPE_EXPIRATION]: expired recipient cannot complete signing', async ({ page }) => {
const { user, team } = await seedUser();
// Use only a SIGNATURE field to simplify the signing flow.
const { recipients } = await seedPendingDocumentWithFullFields({
owner: user,
recipients: [user],
teamId: team.id,
fields: [FieldType.SIGNATURE],
});
const recipient = recipients[0];
await apiSignin({
page,
email: user.email,
redirectPath: `/sign/${recipient.token}`,
});
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
// Now expire the recipient while they're on the signing page.
await prisma.recipient.update({
where: { id: recipient.id },
data: { expiresAt: new Date(Date.now() - 1_000) },
});
// Set up signature.
await signSignaturePad(page);
// Click the signature field to attempt to insert it.
// The server will reject because the recipient is now expired.
const signatureField = recipient.fields.find((f) => f.type === FieldType.SIGNATURE);
if (signatureField) {
await page.locator(`#field-${signatureField.id}`).getByRole('button').click();
}
// The server should reject the signing attempt because the recipient has expired.
// Verify the field was NOT inserted (stays data-inserted="false").
if (signatureField) {
await expect(async () => {
const field = await prisma.field.findUniqueOrThrow({
where: { id: signatureField.id },
});
expect(field.inserted).toBe(false);
}).toPass({ timeout: 10_000 });
}
});
@@ -205,9 +205,13 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
await page.getByRole('textbox', { name: 'Reply to email' }).fill('organisation@documenso.com');
// Update email document settings by enabling/disabling some checkboxes
await page.getByRole('checkbox', { name: 'Send recipient signed email' }).uncheck();
await page.getByRole('checkbox', { name: 'Send document pending email' }).uncheck();
await page.getByRole('checkbox', { name: 'Send document deleted email' }).uncheck();
await page.getByRole('checkbox', { name: 'Email the owner when a recipient signs' }).uncheck();
await page
.getByRole('checkbox', { name: 'Email the signer if the document is still pending' })
.uncheck();
await page
.getByRole('checkbox', { name: 'Email recipients when a pending document is deleted' })
.uncheck();
await page.getByRole('button', { name: 'Update' }).first().click();
await expect(page.getByText('Your email preferences have been updated').first()).toBeVisible();
@@ -225,6 +229,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
documentPending: false, // unchecked
documentCompleted: true,
documentDeleted: false, // unchecked
ownerRecipientExpired: true,
ownerDocumentCompleted: true,
});
@@ -240,12 +245,12 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
await page.getByRole('option', { name: 'Override organisation settings' }).click();
// Update some email settings
await page.getByRole('checkbox', { name: 'Send recipient signing request email' }).uncheck();
await page.getByRole('checkbox', { name: 'Email recipients with a signing request' }).uncheck();
await page
.getByRole('checkbox', { name: 'Send document completed email', exact: true })
.getByRole('checkbox', { name: 'Email recipients when the document is completed', exact: true })
.uncheck();
await page
.getByRole('checkbox', { name: 'Send document completed email to the owner' })
.getByRole('checkbox', { name: 'Email the owner when the document is completed' })
.uncheck();
await page.getByRole('button', { name: 'Update' }).first().click();
@@ -264,6 +269,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
documentPending: true,
documentCompleted: false,
documentDeleted: true,
ownerRecipientExpired: true,
ownerDocumentCompleted: false,
});
@@ -284,6 +290,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
documentPending: true,
documentCompleted: false,
documentDeleted: true,
ownerRecipientExpired: true,
ownerDocumentCompleted: false,
});
@@ -309,6 +316,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
documentPending: false,
documentCompleted: true,
documentDeleted: false,
ownerRecipientExpired: true,
ownerDocumentCompleted: true,
});
@@ -329,6 +337,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
documentPending: false,
documentCompleted: true,
documentDeleted: false,
ownerRecipientExpired: true,
ownerDocumentCompleted: true,
});
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 246 KiB