mirror of
https://github.com/documenso/documenso.git
synced 2026-07-24 08:54:20 +10:00
feat: autoplace fields from placeholders (#2111)
This PR introduces automatic detection and placement of fields and
recipients based on PDF placeholders.
The placeholders have the following structure:
- `{{fieldType,recipientPosition,fieldMeta}}`
- `{{text,r1,required=true,textAlign=right,fontSize=50}}`
When the user uploads a PDF document containing such placeholders, they
get converted automatically to Documenso fields and assigned to
recipients.
This commit is contained in:
@@ -0,0 +1,453 @@
|
||||
import { PDF, StandardFonts } from '@libpdf/core';
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import type { Team, User } from '@prisma/client';
|
||||
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, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
import type {
|
||||
TCreateEnvelopePayload,
|
||||
TCreateEnvelopeResponse,
|
||||
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
|
||||
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
||||
|
||||
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||
const baseUrl = `${WEBAPP_BASE_URL}/api/v2-beta`;
|
||||
|
||||
const FIXTURES_DIR = path.join(__dirname, '../../../../assets/fixtures/auto-placement');
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test.describe('Placeholder-based field creation', () => {
|
||||
let user: User, team: Team, token: string;
|
||||
|
||||
test.beforeEach(async () => {
|
||||
({ user, team } = await seedUser());
|
||||
({ token } = await createApiToken({
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
}));
|
||||
});
|
||||
|
||||
const createEnvelopeWithPdf = async (
|
||||
request: APIRequestContext,
|
||||
pdfFilename: string,
|
||||
): Promise<TCreateEnvelopeResponse> => {
|
||||
const pdfPath = path.join(FIXTURES_DIR, pdfFilename);
|
||||
const pdfData = fs.readFileSync(pdfPath);
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append(
|
||||
'payload',
|
||||
JSON.stringify({
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Placeholder Fields Test',
|
||||
} satisfies TCreateEnvelopePayload),
|
||||
);
|
||||
|
||||
formData.append('files', new File([pdfData], pdfFilename, { type: 'application/pdf' }));
|
||||
|
||||
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: formData,
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const createEnvelopeItemsWithPdf = async (
|
||||
request: APIRequestContext,
|
||||
envelopeId: string,
|
||||
pdfFilename: string,
|
||||
) => {
|
||||
const pdfPath = path.join(FIXTURES_DIR, pdfFilename);
|
||||
const pdfData = fs.readFileSync(pdfPath);
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('payload', JSON.stringify({ envelopeId }));
|
||||
formData.append('files', new File([pdfData], pdfFilename, { type: 'application/pdf' }));
|
||||
|
||||
const res = await request.post(`${baseUrl}/envelope/item/create-many`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: formData,
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const addRecipient = async (request: APIRequestContext, envelopeId: string) => {
|
||||
const payload: TCreateEnvelopeRecipientsRequest = {
|
||||
envelopeId,
|
||||
data: [
|
||||
{
|
||||
email: user.email,
|
||||
name: user.name || '',
|
||||
role: RecipientRole.SIGNER,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const res = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: payload,
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
};
|
||||
|
||||
const addRecipients = async (
|
||||
request: APIRequestContext,
|
||||
envelopeId: string,
|
||||
recipients: TCreateEnvelopeRecipientsRequest['data'],
|
||||
) => {
|
||||
const payload: TCreateEnvelopeRecipientsRequest = {
|
||||
envelopeId,
|
||||
data: recipients,
|
||||
};
|
||||
|
||||
const res = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: payload,
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
};
|
||||
|
||||
const getEnvelope = async (
|
||||
request: APIRequestContext,
|
||||
envelopeId: string,
|
||||
): Promise<TGetEnvelopeResponse> => {
|
||||
const res = await request.get(`${baseUrl}/envelope/${envelopeId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
|
||||
return res.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a PDF with the same placeholder appearing multiple times at different locations.
|
||||
*/
|
||||
const createPdfWithDuplicatePlaceholders = async (): Promise<Buffer> => {
|
||||
const pdf = PDF.create();
|
||||
const page = pdf.addPage({ size: 'letter' });
|
||||
|
||||
// Draw the same placeholder text at three different Y positions.
|
||||
page.drawText('{{initials}}', { x: 50, y: 700, font: StandardFonts.Helvetica, size: 12 });
|
||||
page.drawText('{{initials}}', { x: 50, y: 500, font: StandardFonts.Helvetica, size: 12 });
|
||||
page.drawText('{{initials}}', { x: 50, y: 300, font: StandardFonts.Helvetica, size: 12 });
|
||||
|
||||
const bytes = await pdf.save();
|
||||
|
||||
return Buffer.from(bytes);
|
||||
};
|
||||
|
||||
const createEnvelopeWithPdfBuffer = async (
|
||||
request: APIRequestContext,
|
||||
pdfBuffer: Buffer,
|
||||
filename: string,
|
||||
): Promise<TCreateEnvelopeResponse> => {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append(
|
||||
'payload',
|
||||
JSON.stringify({
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Placeholder Fields Test',
|
||||
} satisfies TCreateEnvelopePayload),
|
||||
);
|
||||
|
||||
formData.append('files', new File([pdfBuffer], filename, { type: 'application/pdf' }));
|
||||
|
||||
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: formData,
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
|
||||
return res.json();
|
||||
};
|
||||
|
||||
test('should create a field at a placeholder location', async ({ request }) => {
|
||||
const envelope = await createEnvelopeWithPdf(request, 'no-recipient-placeholders.pdf');
|
||||
await addRecipient(request, envelope.id);
|
||||
|
||||
const envelopeData = await getEnvelope(request, envelope.id);
|
||||
const recipientId = envelopeData.recipients[0].id;
|
||||
|
||||
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: {
|
||||
envelopeId: envelope.id,
|
||||
data: [
|
||||
{
|
||||
recipientId,
|
||||
type: FieldType.SIGNATURE,
|
||||
placeholder: '{{signature}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(createFieldsRes.ok()).toBeTruthy();
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: { envelopeId: envelope.id },
|
||||
});
|
||||
|
||||
expect(fields).toHaveLength(1);
|
||||
expect(fields[0].type).toBe(FieldType.SIGNATURE);
|
||||
|
||||
// Verify the field has non-zero position/dimensions resolved from the placeholder.
|
||||
expect(fields[0].positionX.toNumber()).toBeGreaterThan(0);
|
||||
expect(fields[0].positionY.toNumber()).toBeGreaterThan(0);
|
||||
expect(fields[0].width.toNumber()).toBeGreaterThan(0);
|
||||
expect(fields[0].height.toNumber()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should override width and height when provided', async ({ request }) => {
|
||||
const envelope = await createEnvelopeWithPdf(request, 'no-recipient-placeholders.pdf');
|
||||
await addRecipient(request, envelope.id);
|
||||
|
||||
const envelopeData = await getEnvelope(request, envelope.id);
|
||||
const recipientId = envelopeData.recipients[0].id;
|
||||
|
||||
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: {
|
||||
envelopeId: envelope.id,
|
||||
data: [
|
||||
{
|
||||
recipientId,
|
||||
type: FieldType.NAME,
|
||||
placeholder: '{{name}}',
|
||||
width: 30,
|
||||
height: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(createFieldsRes.ok()).toBeTruthy();
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: { envelopeId: envelope.id },
|
||||
});
|
||||
|
||||
expect(fields).toHaveLength(1);
|
||||
expect(fields[0].width.toNumber()).toBeCloseTo(30, 1);
|
||||
expect(fields[0].height.toNumber()).toBeCloseTo(5, 1);
|
||||
});
|
||||
|
||||
test('should fail when placeholder text is not found in the PDF', async ({ request }) => {
|
||||
const envelope = await createEnvelopeWithPdf(request, 'no-recipient-placeholders.pdf');
|
||||
await addRecipient(request, envelope.id);
|
||||
|
||||
const envelopeData = await getEnvelope(request, envelope.id);
|
||||
const recipientId = envelopeData.recipients[0].id;
|
||||
|
||||
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: {
|
||||
envelopeId: envelope.id,
|
||||
data: [
|
||||
{
|
||||
recipientId,
|
||||
type: FieldType.TEXT,
|
||||
placeholder: '{{nonexistent}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(createFieldsRes.ok()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should create fields using a mix of coordinate and placeholder positioning', async ({
|
||||
request,
|
||||
}) => {
|
||||
const envelope = await createEnvelopeWithPdf(request, 'no-recipient-placeholders.pdf');
|
||||
await addRecipient(request, envelope.id);
|
||||
|
||||
const envelopeData = await getEnvelope(request, envelope.id);
|
||||
const recipientId = envelopeData.recipients[0].id;
|
||||
|
||||
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: {
|
||||
envelopeId: envelope.id,
|
||||
data: [
|
||||
{
|
||||
recipientId,
|
||||
type: FieldType.SIGNATURE,
|
||||
placeholder: '{{signature}}',
|
||||
},
|
||||
{
|
||||
recipientId,
|
||||
type: FieldType.DATE,
|
||||
page: 1,
|
||||
positionX: 10,
|
||||
positionY: 20,
|
||||
width: 15,
|
||||
height: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(createFieldsRes.ok()).toBeTruthy();
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: { envelopeId: envelope.id },
|
||||
orderBy: { type: 'asc' },
|
||||
});
|
||||
|
||||
expect(fields).toHaveLength(2);
|
||||
|
||||
const dateField = fields.find((f) => f.type === FieldType.DATE);
|
||||
const signatureField = fields.find((f) => f.type === FieldType.SIGNATURE);
|
||||
|
||||
expect(dateField).toBeDefined();
|
||||
expect(dateField!.positionX.toNumber()).toBeCloseTo(10, 1);
|
||||
expect(dateField!.positionY.toNumber()).toBeCloseTo(20, 1);
|
||||
|
||||
expect(signatureField).toBeDefined();
|
||||
expect(signatureField!.positionX.toNumber()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should create a field only at first occurrence by default', async ({ request }) => {
|
||||
const pdfBuffer = await createPdfWithDuplicatePlaceholders();
|
||||
const envelope = await createEnvelopeWithPdfBuffer(request, pdfBuffer, 'duplicates.pdf');
|
||||
await addRecipient(request, envelope.id);
|
||||
|
||||
const envelopeData = await getEnvelope(request, envelope.id);
|
||||
const recipientId = envelopeData.recipients[0].id;
|
||||
|
||||
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: {
|
||||
envelopeId: envelope.id,
|
||||
data: [
|
||||
{
|
||||
recipientId,
|
||||
type: FieldType.INITIALS,
|
||||
placeholder: '{{initials}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(createFieldsRes.ok()).toBeTruthy();
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: { envelopeId: envelope.id },
|
||||
});
|
||||
|
||||
// Should only create one field (first occurrence).
|
||||
expect(fields).toHaveLength(1);
|
||||
expect(fields[0].type).toBe(FieldType.INITIALS);
|
||||
});
|
||||
|
||||
test('should create fields at all occurrences when matchAll is true', async ({ request }) => {
|
||||
const pdfBuffer = await createPdfWithDuplicatePlaceholders();
|
||||
const envelope = await createEnvelopeWithPdfBuffer(request, pdfBuffer, 'duplicates.pdf');
|
||||
await addRecipient(request, envelope.id);
|
||||
|
||||
const envelopeData = await getEnvelope(request, envelope.id);
|
||||
const recipientId = envelopeData.recipients[0].id;
|
||||
|
||||
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: {
|
||||
envelopeId: envelope.id,
|
||||
data: [
|
||||
{
|
||||
recipientId,
|
||||
type: FieldType.INITIALS,
|
||||
placeholder: '{{initials}}',
|
||||
matchAll: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(createFieldsRes.ok()).toBeTruthy();
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: { envelopeId: envelope.id },
|
||||
orderBy: { positionY: 'asc' },
|
||||
});
|
||||
|
||||
// Should create three fields (one for each occurrence).
|
||||
expect(fields).toHaveLength(3);
|
||||
|
||||
// All should be INITIALS type.
|
||||
expect(fields.every((f) => f.type === FieldType.INITIALS)).toBe(true);
|
||||
|
||||
// Verify they're at different Y positions.
|
||||
const yPositions = fields.map((f) => f.positionY.toNumber());
|
||||
const uniqueYPositions = new Set(yPositions);
|
||||
|
||||
expect(uniqueYPositions.size).toBe(3);
|
||||
});
|
||||
|
||||
test('should map placeholder recipients by signing order when adding items', async ({
|
||||
request,
|
||||
}) => {
|
||||
const envelope = await createEnvelopeWithPdf(request, 'no-recipient-placeholders.pdf');
|
||||
|
||||
await addRecipients(request, envelope.id, [
|
||||
{
|
||||
email: 'second.recipient@documenso.com',
|
||||
name: 'Second Recipient',
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: 2,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
{
|
||||
email: 'first.recipient@documenso.com',
|
||||
name: 'First Recipient',
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: 1,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
]);
|
||||
|
||||
await createEnvelopeItemsWithPdf(request, envelope.id, 'project-proposal-single-recipient.pdf');
|
||||
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: { envelopeId: envelope.id },
|
||||
});
|
||||
|
||||
const firstRecipient = recipients.find((recipient) => recipient.signingOrder === 1);
|
||||
|
||||
expect(firstRecipient).toBeDefined();
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: { envelopeId: envelope.id },
|
||||
});
|
||||
|
||||
expect(fields.length).toBeGreaterThan(0);
|
||||
expect(fields.every((field) => field.recipientId === firstRecipient!.id)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,307 @@
|
||||
import { type Page, expect, test } from '@playwright/test';
|
||||
import path from 'path';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
const FIXTURES_DIR = path.join(__dirname, '../../../assets/fixtures/auto-placement');
|
||||
|
||||
const SINGLE_PLACEHOLDER_PDF_PATH = path.join(
|
||||
FIXTURES_DIR,
|
||||
'project-proposal-single-recipient.pdf',
|
||||
);
|
||||
|
||||
const MULTIPLE_PLACEHOLDER_PDF_PATH = path.join(
|
||||
FIXTURES_DIR,
|
||||
'project-proposal-multiple-fields-and-recipients.pdf',
|
||||
);
|
||||
|
||||
const NO_RECIPIENT_PDF_PATH = path.join(FIXTURES_DIR, 'no-recipient-placeholders.pdf');
|
||||
|
||||
const INVALID_FIELD_TYPE_PDF_PATH = path.join(FIXTURES_DIR, 'invalid-field-type.pdf');
|
||||
|
||||
const FIELD_TYPE_ONLY_PDF_PATH = path.join(FIXTURES_DIR, 'field-type-only.pdf');
|
||||
|
||||
const setTeamDefaultRecipients = async (
|
||||
teamId: number,
|
||||
defaultRecipients: Array<{ email: string; name: string; role: RecipientRole }>,
|
||||
) => {
|
||||
const teamSettings = await prisma.teamGlobalSettings.findFirstOrThrow({
|
||||
where: {
|
||||
team: {
|
||||
id: teamId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.teamGlobalSettings.update({
|
||||
where: {
|
||||
id: teamSettings.id,
|
||||
},
|
||||
data: {
|
||||
defaultRecipients,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const setupUserAndSignIn = async (page: Page) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
});
|
||||
|
||||
return { user, team };
|
||||
};
|
||||
|
||||
const uploadPdf = async (page: Page, team: { url: string }, pdfPath: string) => {
|
||||
const [fileChooser] = await Promise.all([
|
||||
page.waitForEvent('filechooser'),
|
||||
page
|
||||
.locator('input[type=file]')
|
||||
.nth(1)
|
||||
.evaluate((e) => {
|
||||
if (e instanceof HTMLInputElement) {
|
||||
e.click();
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
await fileChooser.setFiles(pdfPath);
|
||||
|
||||
// Wait for redirect to v2 envelope editor.
|
||||
await page.waitForURL(new RegExp(`/t/${team.url}/documents/envelope_.*`));
|
||||
|
||||
// Extract envelope ID from URL.
|
||||
const urlParts = page.url().split('/');
|
||||
const envelopeId = urlParts.find((part) => part.startsWith('envelope_'));
|
||||
|
||||
if (!envelopeId) {
|
||||
throw new Error('Could not extract envelope ID from URL');
|
||||
}
|
||||
|
||||
return envelopeId;
|
||||
};
|
||||
|
||||
test.describe('PDF Placeholders with single recipient', () => {
|
||||
test('[AUTO_PLACING_FIELDS]: should create placeholder recipients even with default recipients', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
await setTeamDefaultRecipients(team.id, [
|
||||
{
|
||||
email: user.email,
|
||||
name: user.name || user.email,
|
||||
role: RecipientRole.CC,
|
||||
},
|
||||
]);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
});
|
||||
|
||||
const envelopeId = await uploadPdf(page, team, SINGLE_PLACEHOLDER_PDF_PATH);
|
||||
|
||||
await expect(async () => {
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: { envelopeId },
|
||||
});
|
||||
|
||||
const placeholderRecipient = recipients.find(
|
||||
(recipient) => recipient.email === 'recipient.1@documenso.com',
|
||||
);
|
||||
|
||||
const defaultRecipient = recipients.find((recipient) => recipient.email === user.email);
|
||||
|
||||
expect(placeholderRecipient).toBeDefined();
|
||||
expect(defaultRecipient).toBeDefined();
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: { envelopeId },
|
||||
});
|
||||
|
||||
expect(fields.length).toBeGreaterThan(0);
|
||||
expect(fields.every((field) => field.recipientId === placeholderRecipient!.id)).toBe(true);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically create recipients from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { team } = await setupUserAndSignIn(page);
|
||||
await uploadPdf(page, team, SINGLE_PLACEHOLDER_PDF_PATH);
|
||||
|
||||
// V2 editor shows recipients on the upload page under "Recipients" heading.
|
||||
await expect(page.getByRole('heading', { name: 'Recipients' })).toBeVisible();
|
||||
await expect(page.getByTestId('signer-email-input').first()).toHaveValue(
|
||||
'recipient.1@documenso.com',
|
||||
);
|
||||
await expect(page.getByLabel('Name').first()).toHaveValue('Recipient 1');
|
||||
});
|
||||
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically place fields from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { team } = await setupUserAndSignIn(page);
|
||||
const envelopeId = await uploadPdf(page, team, SINGLE_PLACEHOLDER_PDF_PATH);
|
||||
|
||||
// V2 editor renders fields on a Konva canvas, so we verify via the database.
|
||||
await expect(async () => {
|
||||
const fields = await prisma.field.findMany({
|
||||
where: { envelopeId },
|
||||
});
|
||||
|
||||
const fieldTypes = fields.map((f) => f.type).sort();
|
||||
expect(fieldTypes).toEqual(['EMAIL', 'NAME', 'SIGNATURE', 'TEXT'].sort());
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically configure fields from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { team } = await setupUserAndSignIn(page);
|
||||
const envelopeId = await uploadPdf(page, team, SINGLE_PLACEHOLDER_PDF_PATH);
|
||||
|
||||
// Verify field metadata was correctly parsed from the placeholder.
|
||||
await expect(async () => {
|
||||
const textField = await prisma.field.findFirst({
|
||||
where: { envelopeId, type: 'TEXT' },
|
||||
});
|
||||
|
||||
expect(textField).toBeDefined();
|
||||
expect(textField!.fieldMeta).toBeDefined();
|
||||
|
||||
const meta = textField!.fieldMeta as Record<string, unknown>;
|
||||
expect(meta.required).toBe(true);
|
||||
expect(meta.textAlign).toBe('right');
|
||||
}).toPass();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('PDF Placeholders with multiple recipients', () => {
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically create recipients from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { team } = await setupUserAndSignIn(page);
|
||||
const envelopeId = await uploadPdf(page, team, MULTIPLE_PLACEHOLDER_PDF_PATH);
|
||||
|
||||
// V2 editor shows recipients on the upload page.
|
||||
await expect(page.getByRole('heading', { name: 'Recipients' })).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId('signer-email-input').first()).toHaveValue(
|
||||
'recipient.1@documenso.com',
|
||||
);
|
||||
|
||||
await expect(page.getByTestId('signer-email-input').nth(1)).toHaveValue(
|
||||
'recipient.2@documenso.com',
|
||||
);
|
||||
|
||||
await expect(page.getByTestId('signer-email-input').nth(2)).toHaveValue(
|
||||
'recipient.3@documenso.com',
|
||||
);
|
||||
|
||||
// Verify recipients via the database for name validation since the v2 editor
|
||||
// only shows the "Name" label on the first recipient row.
|
||||
await expect(async () => {
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: { envelopeId },
|
||||
orderBy: { signingOrder: 'asc' },
|
||||
});
|
||||
|
||||
expect(recipients).toHaveLength(3);
|
||||
expect(recipients[0].name).toBe('Recipient 1');
|
||||
expect(recipients[1].name).toBe('Recipient 2');
|
||||
expect(recipients[2].name).toBe('Recipient 3');
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically create fields from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { team } = await setupUserAndSignIn(page);
|
||||
const envelopeId = await uploadPdf(page, team, MULTIPLE_PLACEHOLDER_PDF_PATH);
|
||||
|
||||
// V2 editor renders fields on a Konva canvas, so we verify via the database.
|
||||
await expect(async () => {
|
||||
const fields = await prisma.field.findMany({
|
||||
where: { envelopeId },
|
||||
});
|
||||
|
||||
const fieldTypes = fields.map((f) => f.type).sort();
|
||||
expect(fieldTypes).toEqual(
|
||||
['SIGNATURE', 'SIGNATURE', 'SIGNATURE', 'EMAIL', 'EMAIL', 'NAME', 'TEXT', 'NUMBER'].sort(),
|
||||
);
|
||||
}).toPass();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('PDF Placeholders without recipient identifier', () => {
|
||||
test('[AUTO_PLACING_FIELDS]: should skip placeholders without a recipient identifier', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { team } = await setupUserAndSignIn(page);
|
||||
const envelopeId = await uploadPdf(page, team, NO_RECIPIENT_PDF_PATH);
|
||||
|
||||
// Placeholders like {{signature}}, {{name}}, {{email}} have no recipient
|
||||
// identifier and should be skipped entirely. No fields or auto-created
|
||||
// recipients should exist.
|
||||
await expect(async () => {
|
||||
const fields = await prisma.field.findMany({
|
||||
where: { envelopeId },
|
||||
});
|
||||
|
||||
expect(fields).toHaveLength(0);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('[AUTO_PLACING_FIELDS]: should skip a bare field type placeholder', async ({ page }) => {
|
||||
const { team } = await setupUserAndSignIn(page);
|
||||
const envelopeId = await uploadPdf(page, team, FIELD_TYPE_ONLY_PDF_PATH);
|
||||
|
||||
await expect(async () => {
|
||||
const fields = await prisma.field.findMany({
|
||||
where: { envelopeId },
|
||||
});
|
||||
|
||||
expect(fields).toHaveLength(0);
|
||||
}).toPass();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('PDF Placeholders with invalid field types', () => {
|
||||
test('[AUTO_PLACING_FIELDS]: should skip invalid field types and process valid ones', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { team } = await setupUserAndSignIn(page);
|
||||
const envelopeId = await uploadPdf(page, team, INVALID_FIELD_TYPE_PDF_PATH);
|
||||
|
||||
// Only the valid placeholders (signature,r1 and email,r2) should create fields.
|
||||
// The invalid ones (bogus,r1 and foobar,r2) should be skipped.
|
||||
await expect(async () => {
|
||||
const fields = await prisma.field.findMany({
|
||||
where: { envelopeId },
|
||||
});
|
||||
|
||||
const fieldTypes = fields.map((f) => f.type).sort();
|
||||
expect(fieldTypes).toEqual(['EMAIL', 'SIGNATURE'].sort());
|
||||
}).toPass();
|
||||
|
||||
// Both valid recipients should still be created.
|
||||
await expect(async () => {
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: { envelopeId },
|
||||
orderBy: { signingOrder: 'asc' },
|
||||
});
|
||||
|
||||
expect(recipients).toHaveLength(2);
|
||||
}).toPass();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user