Files
documenso/packages/app-tests/e2e/envelopes/file-conversion.spec.ts
T
ephraimduncan d4bee4aed1 fix: tests
2026-01-01 22:46:35 +00:00

1167 lines
37 KiB
TypeScript

import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { DocumentStatus, EnvelopeType, FieldType } from '@prisma/client';
import { nanoid } from 'nanoid';
import fs from 'node:fs';
import path from 'node:path';
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 });
/**
* Helper to verify originalData is populated for converted files.
* PNG/JPEG/DOCX should have originalData, PDF should not.
*/
async function verifyOriginalData(envelopeId: string, expectOriginalData: boolean) {
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: envelopeId },
include: {
envelopeItems: {
include: { documentData: true },
},
},
});
if (expectOriginalData) {
expect(envelope.envelopeItems[0].documentData?.originalData).not.toBeNull();
expect(envelope.envelopeItems[0].documentData?.originalMimeType).not.toBeNull();
} else {
expect(envelope.envelopeItems[0].documentData?.originalData).toBeNull();
}
}
/**
* Helper to mark signature fields as inserted via Prisma.
* This bypasses Konva canvas interaction which is hard to test.
* Based on the pattern used in envelope-alignment.spec.ts.
*/
async function markSignatureFieldsAsInserted(envelopeId: string, recipientId: number) {
const signatureFields = await prisma.field.findMany({
where: {
envelopeId,
recipientId,
type: FieldType.SIGNATURE,
inserted: false,
},
});
for (const field of signatureFields) {
await prisma.field.update({
where: { id: field.id },
data: {
inserted: true,
customText: '',
signature: {
create: {
recipientId,
typedSignature: 'Test Signature',
signatureImageAsBase64: null,
},
},
},
});
}
}
/**
* Helper to complete the signing flow for a recipient.
* Expects fields to already be marked as inserted via Prisma.
*/
async function completeSigningFlow(page: Page) {
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
// Wait for Complete button to be visible (fields should already be inserted)
await expect(page.getByRole('button', { name: 'Complete' })).toBeVisible({ timeout: 10000 });
// Click Complete button
await page.getByRole('button', { name: 'Complete' }).click();
// Wait for Sign button to be visible in the confirmation dialog
const signButton = page.getByRole('button', { name: 'Sign' });
await expect(signButton).toBeVisible({ timeout: 10000 });
// Use force click to handle transient DOM changes
await signButton.click({ force: true });
// Wait for "Document Signed" heading to appear (confirms completion)
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible({
timeout: 30000,
});
}
test.describe('File Conversion', () => {
test.describe('API Tests', () => {
test('[FILE_CONVERSION_API]: should upload, convert, and sign a PNG image via API', async ({
page,
request,
}) => {
const { user: uploader, team } = await seedUser();
const { user: recipient } = await seedUser();
const { token } = await createApiToken({
userId: uploader.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const pngFile = fs.readFileSync(path.join(__dirname, '../../../../assets/test-image.png'));
const formData = new FormData();
const createEnvelopePayload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: 'PNG Conversion Test',
recipients: [
{
email: recipient.email,
name: recipient.name || '',
role: RecipientRole.SIGNER,
fields: [
{
type: FieldType.SIGNATURE,
fieldMeta: { type: 'signature' },
page: 1,
positionX: 10,
positionY: 10,
width: 20,
height: 5,
},
],
},
],
};
formData.append('payload', JSON.stringify(createEnvelopePayload));
formData.append('files', new File([pngFile], 'test-image.png', { type: 'image/png' }));
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
if (!createEnvelopeRequest.ok()) {
const errorBody = await createEnvelopeRequest.json();
const errorCode =
errorBody.code === 'INTERNAL_SERVER_ERROR'
? errorBody.data?.code || errorBody.data?.appError?.code
: errorBody.code;
if (errorCode === 'CONVERSION_SERVICE_UNAVAILABLE' || errorCode === 'CONVERSION_FAILED') {
test.skip(true, 'Gotenberg conversion service is not available');
return;
}
throw new Error(`Failed to create envelope: ${JSON.stringify(errorBody)}`);
}
expect(createEnvelopeRequest.status()).toBe(200);
const { id: createdEnvelopeId }: TCreateEnvelopeResponse = await createEnvelopeRequest.json();
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: createdEnvelopeId },
include: {
recipients: true,
envelopeItems: true,
},
});
expect(envelope.envelopeItems.length).toBe(1);
expect(envelope.recipients.length).toBe(1);
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${token}` },
data: {
envelopeId: envelope.id,
} satisfies TDistributeEnvelopeRequest,
});
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
const recipientToken = envelope.recipients[0].token;
const signUrl = `/sign/${recipientToken}`;
// Mark signature fields as inserted via Prisma BEFORE loading page
// (bypasses Konva canvas interaction)
await markSignatureFieldsAsInserted(envelope.id, envelope.recipients[0].id);
await apiSignin({
page,
email: recipient.email,
redirectPath: signUrl,
});
await completeSigningFlow(page);
await expect(async () => {
const { status } = await prisma.envelope.findFirstOrThrow({
where: { id: envelope.id },
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass({ timeout: 10000 });
await verifyOriginalData(envelope.id, true);
});
test('[FILE_CONVERSION_API]: should upload, convert, and sign a JPEG image via API', async ({
page,
request,
}) => {
const { user: uploader, team } = await seedUser();
const { user: recipient } = await seedUser();
const { token } = await createApiToken({
userId: uploader.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const jpegFile = fs.readFileSync(path.join(__dirname, '../../../../assets/test-image.jpg'));
const formData = new FormData();
const createEnvelopePayload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: 'JPEG Conversion Test',
recipients: [
{
email: recipient.email,
name: recipient.name || '',
role: RecipientRole.SIGNER,
fields: [
{
type: FieldType.SIGNATURE,
fieldMeta: { type: 'signature' },
page: 1,
positionX: 10,
positionY: 10,
width: 20,
height: 5,
},
],
},
],
};
formData.append('payload', JSON.stringify(createEnvelopePayload));
formData.append('files', new File([jpegFile], 'test-image.jpg', { type: 'image/jpeg' }));
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
if (!createEnvelopeRequest.ok()) {
const errorBody = await createEnvelopeRequest.json();
const errorCode =
errorBody.code === 'INTERNAL_SERVER_ERROR'
? errorBody.data?.code || errorBody.data?.appError?.code
: errorBody.code;
if (errorCode === 'CONVERSION_SERVICE_UNAVAILABLE' || errorCode === 'CONVERSION_FAILED') {
test.skip(true, 'Gotenberg conversion service is not available');
return;
}
throw new Error(`Failed to create envelope: ${JSON.stringify(errorBody)}`);
}
expect(createEnvelopeRequest.status()).toBe(200);
const { id: createdEnvelopeId }: TCreateEnvelopeResponse = await createEnvelopeRequest.json();
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: createdEnvelopeId },
include: {
recipients: true,
envelopeItems: true,
},
});
expect(envelope.envelopeItems.length).toBe(1);
expect(envelope.recipients.length).toBe(1);
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${token}` },
data: {
envelopeId: envelope.id,
} satisfies TDistributeEnvelopeRequest,
});
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
const recipientToken = envelope.recipients[0].token;
const signUrl = `/sign/${recipientToken}`;
// Mark signature fields as inserted via Prisma BEFORE loading page
// (bypasses Konva canvas interaction)
await markSignatureFieldsAsInserted(envelope.id, envelope.recipients[0].id);
await apiSignin({
page,
email: recipient.email,
redirectPath: signUrl,
});
await completeSigningFlow(page);
await expect(async () => {
const { status } = await prisma.envelope.findFirstOrThrow({
where: { id: envelope.id },
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass({ timeout: 10000 });
await verifyOriginalData(envelope.id, true);
});
test('[FILE_CONVERSION_API]: should upload, convert, and sign a DOCX document via API', async ({
page,
request,
}) => {
const { user: uploader, team } = await seedUser();
const { user: recipient } = await seedUser();
const { token } = await createApiToken({
userId: uploader.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const docxFile = fs.readFileSync(
path.join(__dirname, '../../../../assets/test-document.docx'),
);
const formData = new FormData();
const createEnvelopePayload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: 'DOCX Conversion Test',
recipients: [
{
email: recipient.email,
name: recipient.name || '',
role: RecipientRole.SIGNER,
fields: [
{
type: FieldType.SIGNATURE,
fieldMeta: { type: 'signature' },
page: 1,
positionX: 10,
positionY: 10,
width: 20,
height: 5,
},
],
},
],
};
formData.append('payload', JSON.stringify(createEnvelopePayload));
formData.append(
'files',
new File([docxFile], 'test-document.docx', {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
}),
);
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
if (!createEnvelopeRequest.ok()) {
const errorBody = await createEnvelopeRequest.json();
const errorCode =
errorBody.code === 'INTERNAL_SERVER_ERROR'
? errorBody.data?.code || errorBody.data?.appError?.code
: errorBody.code;
if (errorCode === 'CONVERSION_SERVICE_UNAVAILABLE' || errorCode === 'CONVERSION_FAILED') {
test.skip(true, 'Gotenberg conversion service is not available');
return;
}
throw new Error(`Failed to create envelope: ${JSON.stringify(errorBody)}`);
}
expect(createEnvelopeRequest.status()).toBe(200);
const { id: createdEnvelopeId }: TCreateEnvelopeResponse = await createEnvelopeRequest.json();
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: createdEnvelopeId },
include: {
recipients: true,
envelopeItems: true,
},
});
expect(envelope.envelopeItems.length).toBe(1);
expect(envelope.recipients.length).toBe(1);
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${token}` },
data: {
envelopeId: envelope.id,
} satisfies TDistributeEnvelopeRequest,
});
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
const recipientToken = envelope.recipients[0].token;
const signUrl = `/sign/${recipientToken}`;
// Mark signature fields as inserted via Prisma BEFORE loading page
// (bypasses Konva canvas interaction)
await markSignatureFieldsAsInserted(envelope.id, envelope.recipients[0].id);
await apiSignin({
page,
email: recipient.email,
redirectPath: signUrl,
});
await completeSigningFlow(page);
await expect(async () => {
const { status } = await prisma.envelope.findFirstOrThrow({
where: { id: envelope.id },
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass({ timeout: 10000 });
await verifyOriginalData(envelope.id, true);
});
test('[FILE_CONVERSION_API]: should upload and sign a PDF document via API (no conversion)', async ({
page,
request,
}) => {
const { user: uploader, team } = await seedUser();
const { user: recipient } = await seedUser();
const { token } = await createApiToken({
userId: uploader.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const pdfFile = fs.readFileSync(path.join(__dirname, '../../../../assets/example.pdf'));
const formData = new FormData();
const createEnvelopePayload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: 'PDF Upload Test',
recipients: [
{
email: recipient.email,
name: recipient.name || '',
role: RecipientRole.SIGNER,
fields: [
{
type: FieldType.SIGNATURE,
fieldMeta: { type: 'signature' },
page: 1,
positionX: 10,
positionY: 10,
width: 20,
height: 5,
},
],
},
],
};
formData.append('payload', JSON.stringify(createEnvelopePayload));
formData.append('files', new File([pdfFile], 'example.pdf', { type: 'application/pdf' }));
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(createEnvelopeRequest.ok()).toBeTruthy();
expect(createEnvelopeRequest.status()).toBe(200);
const { id: createdEnvelopeId }: TCreateEnvelopeResponse = await createEnvelopeRequest.json();
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: createdEnvelopeId },
include: {
recipients: true,
envelopeItems: true,
},
});
expect(envelope.envelopeItems.length).toBe(1);
expect(envelope.recipients.length).toBe(1);
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${token}` },
data: {
envelopeId: envelope.id,
} satisfies TDistributeEnvelopeRequest,
});
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
const recipientToken = envelope.recipients[0].token;
const signUrl = `/sign/${recipientToken}`;
// Mark signature fields as inserted via Prisma BEFORE loading page
// (bypasses Konva canvas interaction)
await markSignatureFieldsAsInserted(envelope.id, envelope.recipients[0].id);
await apiSignin({
page,
email: recipient.email,
redirectPath: signUrl,
});
await completeSigningFlow(page);
await expect(async () => {
const { status } = await prisma.envelope.findFirstOrThrow({
where: { id: envelope.id },
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass({ timeout: 10000 });
// PDF should NOT have originalData (no conversion needed)
await verifyOriginalData(envelope.id, false);
});
test('[FILE_CONVERSION_API]: should reject unsupported file types', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const jsonFile = fs.readFileSync(path.join(__dirname, '../../../../package.json'));
const formData = new FormData();
const createEnvelopePayload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: 'Unsupported File Test',
recipients: [
{
email: user.email,
name: user.name || '',
role: RecipientRole.SIGNER,
fields: [
{
type: FieldType.SIGNATURE,
fieldMeta: { type: 'signature' },
page: 1,
positionX: 10,
positionY: 10,
width: 20,
height: 5,
},
],
},
],
};
formData.append('payload', JSON.stringify(createEnvelopePayload));
formData.append('files', new File([jsonFile], 'package.json', { type: 'application/json' }));
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(createEnvelopeRequest.ok()).toBeFalsy();
const errorBody = await createEnvelopeRequest.json();
// Accept either UNSUPPORTED_FILE_TYPE or INTERNAL_SERVER_ERROR as valid rejection
expect(
errorBody.code === 'UNSUPPORTED_FILE_TYPE' || errorBody.code === 'INTERNAL_SERVER_ERROR',
).toBeTruthy();
});
test('[FILE_CONVERSION_API]: should return error when Gotenberg is unavailable', async ({
request,
}) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const pngFile = fs.readFileSync(path.join(__dirname, '../../../../assets/test-image.png'));
const formData = new FormData();
const createEnvelopePayload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: 'Gotenberg Error Test',
recipients: [
{
email: user.email,
name: user.name || '',
role: RecipientRole.SIGNER,
fields: [
{
type: FieldType.SIGNATURE,
fieldMeta: { type: 'signature' },
page: 1,
positionX: 10,
positionY: 10,
width: 20,
height: 5,
},
],
},
],
};
formData.append('payload', JSON.stringify(createEnvelopePayload));
formData.append('files', new File([pngFile], 'test-image.png', { type: 'image/png' }));
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
// This test only validates the error flow if Gotenberg is actually unavailable
if (createEnvelopeRequest.ok()) {
test.skip(true, 'Gotenberg is available - skipping error scenario test');
return;
}
const errorBody = await createEnvelopeRequest.json();
const errorCode =
errorBody.code === 'INTERNAL_SERVER_ERROR'
? errorBody.data?.code || errorBody.data?.appError?.code
: errorBody.code;
expect(
errorCode === 'CONVERSION_SERVICE_UNAVAILABLE' || errorCode === 'CONVERSION_FAILED',
).toBeTruthy();
});
});
test.describe('UI Tests', () => {
test('[FILE_CONVERSION_UI]: should upload, convert, and sign a PNG image', async ({
page,
request,
}) => {
const { user: uploader, team } = await seedUser();
const { user: recipient } = await seedUser();
const { token } = await createApiToken({
userId: uploader.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
await apiSignin({
page,
email: uploader.email,
redirectPath: `/t/${team.url}/documents`,
});
// Wait for documents page to load
await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();
// Upload PNG via file input
// Use .first() because there are two upload inputs (dropzone wrapper + upload button)
const fileInput = page.getByTestId('document-upload-input').first();
await fileInput.setInputFiles(path.join(__dirname, '../../../../assets/test-image.png'));
// Wait for redirect to editor or error toast
try {
await page.waitForURL(/\/documents\/.*\/edit/, { timeout: 15000 });
} catch {
// Check if error toast appeared (Gotenberg unavailable)
const errorToast = page.locator('li.destructive');
if (await errorToast.isVisible()) {
test.skip(true, 'Gotenberg conversion service is not available');
return;
}
throw new Error('Upload failed without expected error toast');
}
// Get envelope ID from URL
const urlMatch = page.url().match(/documents\/(.+?)\/edit/);
const envelopeId = urlMatch?.[1];
expect(envelopeId).toBeDefined();
// Add recipient via API
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: envelopeId },
include: { envelopeItems: true },
});
const recipientRecord = await prisma.recipient.create({
data: {
email: recipient.email,
name: recipient.name || '',
role: RecipientRole.SIGNER,
envelopeId: envelope.id,
token: nanoid(),
},
});
// Add field via Prisma
await prisma.field.create({
data: {
envelopeId: envelope.id,
recipientId: recipientRecord.id,
type: FieldType.SIGNATURE,
page: 1,
positionX: 10,
positionY: 10,
width: 20,
height: 5,
envelopeItemId: envelope.envelopeItems[0].id,
customText: '',
inserted: false,
fieldMeta: { type: 'signature' },
},
});
// Distribute envelope via API
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${token}` },
data: {
envelopeId: envelope.id,
} satisfies TDistributeEnvelopeRequest,
});
if (!distributeEnvelopeRequest.ok()) {
const errorBody = await distributeEnvelopeRequest.json();
throw new Error(`Failed to distribute envelope: ${JSON.stringify(errorBody)}`);
}
// Get updated recipient with token
const updatedRecipient = await prisma.recipient.findUniqueOrThrow({
where: { id: recipientRecord.id },
});
const signUrl = `/sign/${updatedRecipient.token}`;
// Mark signature fields as inserted via Prisma BEFORE loading page
// (bypasses Konva canvas interaction)
await markSignatureFieldsAsInserted(envelope.id, recipientRecord.id);
// Sign as recipient
await apiSignin({
page,
email: recipient.email,
redirectPath: signUrl,
});
await completeSigningFlow(page);
// Verify completion
await expect(async () => {
const { status } = await prisma.envelope.findFirstOrThrow({
where: { id: envelope.id },
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass({ timeout: 10000 });
// PNG should have originalData
await verifyOriginalData(envelope.id, true);
});
test('[FILE_CONVERSION_UI]: should upload, convert, and sign a JPEG image', async ({
page,
request,
}) => {
const { user: uploader, team } = await seedUser();
const { user: recipient } = await seedUser();
const { token } = await createApiToken({
userId: uploader.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
await apiSignin({
page,
email: uploader.email,
redirectPath: `/t/${team.url}/documents`,
});
await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();
// Use .first() because there are two upload inputs (dropzone wrapper + upload button)
const fileInput = page.getByTestId('document-upload-input').first();
await fileInput.setInputFiles(path.join(__dirname, '../../../../assets/test-image.jpg'));
try {
await page.waitForURL(/\/documents\/.*\/edit/, { timeout: 15000 });
} catch {
const errorToast = page.locator('li.destructive');
if (await errorToast.isVisible()) {
test.skip(true, 'Gotenberg conversion service is not available');
return;
}
throw new Error('Upload failed without expected error toast');
}
const urlMatch = page.url().match(/documents\/(.+?)\/edit/);
const envelopeId = urlMatch?.[1];
expect(envelopeId).toBeDefined();
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: envelopeId },
include: { envelopeItems: true },
});
const recipientRecord = await prisma.recipient.create({
data: {
email: recipient.email,
name: recipient.name || '',
role: RecipientRole.SIGNER,
envelopeId: envelope.id,
token: nanoid(),
},
});
await prisma.field.create({
data: {
envelopeId: envelope.id,
recipientId: recipientRecord.id,
type: FieldType.SIGNATURE,
page: 1,
positionX: 10,
positionY: 10,
width: 20,
height: 5,
envelopeItemId: envelope.envelopeItems[0].id,
customText: '',
inserted: false,
fieldMeta: { type: 'signature' },
},
});
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${token}` },
data: {
envelopeId: envelope.id,
} satisfies TDistributeEnvelopeRequest,
});
if (!distributeEnvelopeRequest.ok()) {
const errorBody = await distributeEnvelopeRequest.json();
throw new Error(`Failed to distribute envelope: ${JSON.stringify(errorBody)}`);
}
const updatedRecipient = await prisma.recipient.findUniqueOrThrow({
where: { id: recipientRecord.id },
});
const signUrl = `/sign/${updatedRecipient.token}`;
// Mark signature fields as inserted via Prisma BEFORE loading page
// (bypasses Konva canvas interaction)
await markSignatureFieldsAsInserted(envelope.id, recipientRecord.id);
await apiSignin({
page,
email: recipient.email,
redirectPath: signUrl,
});
await completeSigningFlow(page);
await expect(async () => {
const { status } = await prisma.envelope.findFirstOrThrow({
where: { id: envelope.id },
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass({ timeout: 10000 });
await verifyOriginalData(envelope.id, true);
});
test('[FILE_CONVERSION_UI]: should upload, convert, and sign a DOCX document', async ({
page,
request,
}) => {
const { user: uploader, team } = await seedUser();
const { user: recipient } = await seedUser();
const { token } = await createApiToken({
userId: uploader.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
await apiSignin({
page,
email: uploader.email,
redirectPath: `/t/${team.url}/documents`,
});
await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();
// Use .first() because there are two upload inputs (dropzone wrapper + upload button)
const fileInput = page.getByTestId('document-upload-input').first();
await fileInput.setInputFiles(path.join(__dirname, '../../../../assets/test-document.docx'));
try {
await page.waitForURL(/\/documents\/.*\/edit/, { timeout: 15000 });
} catch {
const errorToast = page.locator('li.destructive');
if (await errorToast.isVisible()) {
test.skip(true, 'Gotenberg conversion service is not available');
return;
}
throw new Error('Upload failed without expected error toast');
}
const urlMatch = page.url().match(/documents\/(.+?)\/edit/);
const envelopeId = urlMatch?.[1];
expect(envelopeId).toBeDefined();
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: envelopeId },
include: { envelopeItems: true },
});
const recipientRecord = await prisma.recipient.create({
data: {
email: recipient.email,
name: recipient.name || '',
role: RecipientRole.SIGNER,
envelopeId: envelope.id,
token: nanoid(),
},
});
await prisma.field.create({
data: {
envelopeId: envelope.id,
recipientId: recipientRecord.id,
type: FieldType.SIGNATURE,
page: 1,
positionX: 10,
positionY: 10,
width: 20,
height: 5,
envelopeItemId: envelope.envelopeItems[0].id,
customText: '',
inserted: false,
fieldMeta: { type: 'signature' },
},
});
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${token}` },
data: {
envelopeId: envelope.id,
} satisfies TDistributeEnvelopeRequest,
});
if (!distributeEnvelopeRequest.ok()) {
const errorBody = await distributeEnvelopeRequest.json();
throw new Error(`Failed to distribute envelope: ${JSON.stringify(errorBody)}`);
}
const updatedRecipient = await prisma.recipient.findUniqueOrThrow({
where: { id: recipientRecord.id },
});
const signUrl = `/sign/${updatedRecipient.token}`;
// Mark signature fields as inserted via Prisma BEFORE loading page
// (bypasses Konva canvas interaction)
await markSignatureFieldsAsInserted(envelope.id, recipientRecord.id);
await apiSignin({
page,
email: recipient.email,
redirectPath: signUrl,
});
await completeSigningFlow(page);
await expect(async () => {
const { status } = await prisma.envelope.findFirstOrThrow({
where: { id: envelope.id },
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass({ timeout: 10000 });
await verifyOriginalData(envelope.id, true);
});
test('[FILE_CONVERSION_UI]: should upload and sign a PDF document (no conversion)', async ({
page,
request,
}) => {
const { user: uploader, team } = await seedUser();
const { user: recipient } = await seedUser();
const { token } = await createApiToken({
userId: uploader.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
await apiSignin({
page,
email: uploader.email,
redirectPath: `/t/${team.url}/documents`,
});
await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();
// Use .first() because there are two upload inputs (dropzone wrapper + upload button)
const fileInput = page.getByTestId('document-upload-input').first();
await fileInput.setInputFiles(path.join(__dirname, '../../../../assets/example.pdf'));
await page.waitForURL(/\/documents\/.*\/edit/, { timeout: 15000 });
const urlMatch = page.url().match(/documents\/(.+?)\/edit/);
const envelopeId = urlMatch?.[1];
expect(envelopeId).toBeDefined();
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: envelopeId },
include: { envelopeItems: true },
});
const recipientRecord = await prisma.recipient.create({
data: {
email: recipient.email,
name: recipient.name || '',
role: RecipientRole.SIGNER,
envelopeId: envelope.id,
token: nanoid(),
},
});
await prisma.field.create({
data: {
envelopeId: envelope.id,
recipientId: recipientRecord.id,
type: FieldType.SIGNATURE,
page: 1,
positionX: 10,
positionY: 10,
width: 20,
height: 5,
envelopeItemId: envelope.envelopeItems[0].id,
customText: '',
inserted: false,
fieldMeta: { type: 'signature' },
},
});
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${token}` },
data: {
envelopeId: envelope.id,
} satisfies TDistributeEnvelopeRequest,
});
if (!distributeEnvelopeRequest.ok()) {
const errorBody = await distributeEnvelopeRequest.json();
throw new Error(`Failed to distribute envelope: ${JSON.stringify(errorBody)}`);
}
const updatedRecipient = await prisma.recipient.findUniqueOrThrow({
where: { id: recipientRecord.id },
});
const signUrl = `/sign/${updatedRecipient.token}`;
// Mark signature fields as inserted via Prisma BEFORE loading page
// (bypasses Konva canvas interaction)
await markSignatureFieldsAsInserted(envelope.id, recipientRecord.id);
await apiSignin({
page,
email: recipient.email,
redirectPath: signUrl,
});
await completeSigningFlow(page);
await expect(async () => {
const { status } = await prisma.envelope.findFirstOrThrow({
where: { id: envelope.id },
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass({ timeout: 10000 });
// PDF should NOT have originalData
await verifyOriginalData(envelope.id, false);
});
test('[FILE_CONVERSION_UI]: should show error when Gotenberg is unavailable', async ({
page,
}) => {
const { user, team } = await seedUser();
await apiSignin({
page,
email: user.email,
redirectPath: `/t/${team.url}/documents`,
});
await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();
// Use .first() because there are two upload inputs (dropzone wrapper + upload button)
const fileInput = page.getByTestId('document-upload-input').first();
await fileInput.setInputFiles(path.join(__dirname, '../../../../assets/test-image.png'));
// Wait for either navigation (success) or error toast (Gotenberg down)
const navigationPromise = page.waitForURL(/\/documents\/.*\/edit/, { timeout: 15000 });
const toastPromise = page.waitForSelector('li.destructive', {
timeout: 15000,
});
const result = await Promise.race([
navigationPromise.then(() => 'navigation'),
toastPromise.then(() => 'toast'),
]).catch(() => 'timeout');
if (result === 'navigation') {
test.skip(true, 'Gotenberg is available - skipping error scenario test');
return;
}
if (result === 'toast') {
// Error toast appeared - Gotenberg is unavailable
const errorToast = page.locator('li.destructive');
await expect(errorToast).toBeVisible();
} else {
throw new Error('Neither navigation nor error toast occurred');
}
});
});
});