Files
documenso/packages/app-tests/e2e/api/v2/placeholder-fields-api.spec.ts
T
2026-05-08 16:04:22 +10:00

442 lines
14 KiB
TypeScript

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';
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';
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);
});
});