fix: lint project (#2693)

This commit is contained in:
David Nguyen
2026-05-08 16:04:22 +10:00
committed by GitHub
parent edbf65969b
commit 8671f269e8
1414 changed files with 12867 additions and 24335 deletions
+2 -3
View File
@@ -1,6 +1,3 @@
import { TsRestHttpError, fetchRequestHandler } from '@ts-rest/serverless/fetch';
import { Hono } from 'hono';
import { ApiContractV1 } from '@documenso/api/v1/contract';
import { ApiContractV1Implementation } from '@documenso/api/v1/implementation';
import { OpenAPIV1 } from '@documenso/api/v1/openapi';
@@ -10,6 +7,8 @@ import { subscribeHandler } from '@documenso/lib/server-only/webhooks/zapier/sub
import { unsubscribeHandler } from '@documenso/lib/server-only/webhooks/zapier/unsubscribe';
// This is a bit nasty. Todo: Extract
import type { HonoEnv } from '@documenso/remix/server/router';
import { fetchRequestHandler, TsRestHttpError } from '@ts-rest/serverless/fetch';
import { Hono } from 'hono';
// This is bad, ts-router will be created on each request.
// But don't really have a choice here.
+1 -1
View File
@@ -3,6 +3,6 @@
"include": ["."],
"exclude": ["dist", "build", "node_modules"],
"compilerOptions": {
"strict": true,
"strict": true
}
}
+1 -2
View File
@@ -1,9 +1,8 @@
import { initContract } from '@ts-rest/core';
import {
ZCreateTemplateV2RequestSchema,
ZCreateTemplateV2ResponseSchema,
} from '@documenso/trpc/server/template-router/schema';
import { initContract } from '@ts-rest/core';
import {
ZAuthorizationHeadersSchema,
+195 -223
View File
@@ -1,24 +1,20 @@
import { DocumentDataType, EnvelopeType, SigningStatus } from '@prisma/client';
import { tsr } from '@ts-rest/serverless/fetch';
import { match } from 'ts-pattern';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { DocumentDataType, EnvelopeType, SigningStatus } from '@prisma/client';
import { tsr } from '@ts-rest/serverless/fetch';
import { match } from 'ts-pattern';
import '@documenso/lib/constants/time-zones';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { AppError } from '@documenso/lib/errors/app-error';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { updateDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { updateDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import {
getEnvelopeById,
getEnvelopeWhereInput,
} from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { getEnvelopeById, getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { deleteDocumentField } from '@documenso/lib/server-only/field/delete-document-field';
import { updateEnvelopeFields } from '@documenso/lib/server-only/field/update-envelope-fields';
import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf';
@@ -42,16 +38,10 @@ import {
} from '@documenso/lib/types/field-meta';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import {
getPresignGetUrl,
getPresignPostUrl,
} from '@documenso/lib/universal/upload/server-actions';
import { getPresignGetUrl, getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import {
mapSecondaryIdToDocumentId,
mapSecondaryIdToTemplateId,
} from '@documenso/lib/utils/envelope';
import { mapSecondaryIdToDocumentId, mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { ApiContractV1 } from './contract';
@@ -527,9 +517,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}
const timezone = meta?.timezone
? TIME_ZONES.find((tz) => tz === meta?.timezone)
: DEFAULT_DOCUMENT_TIME_ZONE;
const timezone = meta?.timezone ? TIME_ZONES.find((tz) => tz === meta?.timezone) : DEFAULT_DOCUMENT_TIME_ZONE;
const isTimeZoneValid = meta?.timezone ? TIME_ZONES.includes(String(timezone)) : true;
@@ -728,64 +716,176 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
createDocumentFromTemplate: authenticatedMiddleware(
async (args, user, team, { logger, metadata }) => {
const { body, params } = args;
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { body, params } = args;
logger.info({
input: {
templateId: params.templateId,
logger.info({
input: {
templateId: params.templateId,
},
});
const { remaining } = await getServerLimits({ userId: user.id, teamId: team.id });
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
},
});
};
}
const { remaining } = await getServerLimits({ userId: user.id, teamId: team.id });
const templateId = Number(params.templateId);
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
},
};
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
const template = await getEnvelopeById({
id: {
type: 'templateId',
id: templateId,
},
type: EnvelopeType.TEMPLATE,
userId: user.id,
teamId: team.id,
});
if (template.envelopeItems.length !== 1) {
throw new Error('API V1 does not support templates with multiple documents');
}
// V1 API request schema uses indices for recipients
// So we remap the recipients to attach the IDs
const mappedRecipients = body.recipients.map((recipient, index) => {
const existingRecipient = template.recipients.at(index);
if (!existingRecipient) {
throw new Error('Recipient not found.');
}
const templateId = Number(params.templateId);
return {
id: existingRecipient.id,
name: recipient.name,
email: recipient.email,
signingOrder: recipient.signingOrder,
role: recipient.role, // You probably shouldn't be able to change the role.
};
});
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
const createdEnvelope = await createDocumentFromTemplate({
id: {
type: 'templateId',
id: templateId,
},
externalId: body.externalId || null,
userId: user.id,
teamId: team.id,
recipients: mappedRecipients,
override: {
...body.meta,
title: body.title,
},
attachments: body.attachments,
formValues: body.formValues,
requestMetadata: metadata,
});
const template = await getEnvelopeById({
id: {
type: 'templateId',
id: templateId,
},
type: EnvelopeType.TEMPLATE,
userId: user.id,
teamId: team.id,
const envelopeItems = await prisma.envelopeItem.findMany({
where: {
envelopeId: createdEnvelope.id,
},
include: {
documentData: true,
},
});
const firstEnvelopeItemData = envelopeItems[0].documentData;
if (!firstEnvelopeItemData) {
throw new Error('Document data not found.');
}
if (body.formValues) {
const pdf = await getFileServerSide(firstEnvelopeItemData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
if (template.envelopeItems.length !== 1) {
throw new Error('API V1 does not support templates with multiple documents');
}
const newDocumentData = await putNormalizedPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
// V1 API request schema uses indices for recipients
// So we remap the recipients to attach the IDs
const mappedRecipients = body.recipients.map((recipient, index) => {
const existingRecipient = template.recipients.at(index);
await prisma.envelopeItem.update({
where: {
id: firstEnvelopeItemData.id,
},
data: {
title: body.title || fileName,
documentDataId: newDocumentData.id,
},
});
}
if (!existingRecipient) {
throw new Error('Recipient not found.');
}
if (body.authOptions || body.formValues) {
await prisma.envelope.update({
where: {
id: createdEnvelope.id,
},
data: {
formValues: body.formValues,
authOptions: body.authOptions,
},
});
}
return {
id: existingRecipient.id,
return {
status: 200,
body: {
documentId: mapSecondaryIdToDocumentId(createdEnvelope.secondaryId),
recipients: createdEnvelope.recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
role: recipient.role, // You probably shouldn't be able to change the role.
};
});
const createdEnvelope = await createDocumentFromTemplate({
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
}),
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { body, params } = args;
logger.info({
input: {
templateId: params.templateId,
},
});
const { remaining } = await getServerLimits({ userId: user.id, teamId: team.id });
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
},
};
}
const templateId = Number(params.templateId);
let envelope: Awaited<ReturnType<typeof createDocumentFromTemplate>> | null = null;
try {
envelope = await createDocumentFromTemplate({
id: {
type: 'templateId',
id: templateId,
@@ -793,165 +893,49 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
externalId: body.externalId || null,
userId: user.id,
teamId: team.id,
recipients: mappedRecipients,
recipients: body.recipients,
prefillFields: body.prefillFields,
folderId: body.folderId,
override: {
...body.meta,
title: body.title,
...body.meta,
},
attachments: body.attachments,
formValues: body.formValues,
requestMetadata: metadata,
});
} catch (err) {
return AppError.toRestAPIError(err);
}
const envelopeItems = await prisma.envelopeItem.findMany({
if (body.authOptions) {
await prisma.envelope.update({
where: {
envelopeId: createdEnvelope.id,
id: envelope.id,
},
include: {
documentData: true,
data: {
authOptions: body.authOptions,
},
});
}
const firstEnvelopeItemData = envelopeItems[0].documentData;
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
if (!firstEnvelopeItemData) {
throw new Error('Document data not found.');
}
if (body.formValues) {
const pdf = await getFileServerSide(firstEnvelopeItemData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
const newDocumentData = await putNormalizedPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
await prisma.envelopeItem.update({
where: {
id: firstEnvelopeItemData.id,
},
data: {
title: body.title || fileName,
documentDataId: newDocumentData.id,
},
});
}
if (body.authOptions || body.formValues) {
await prisma.envelope.update({
where: {
id: createdEnvelope.id,
},
data: {
formValues: body.formValues,
authOptions: body.authOptions,
},
});
}
return {
status: 200,
body: {
documentId: mapSecondaryIdToDocumentId(createdEnvelope.secondaryId),
recipients: createdEnvelope.recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
},
),
generateDocumentFromTemplate: authenticatedMiddleware(
async (args, user, team, { logger, metadata }) => {
const { body, params } = args;
logger.info({
input: {
templateId: params.templateId,
},
});
const { remaining } = await getServerLimits({ userId: user.id, teamId: team.id });
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
},
};
}
const templateId = Number(params.templateId);
let envelope: Awaited<ReturnType<typeof createDocumentFromTemplate>> | null = null;
try {
envelope = await createDocumentFromTemplate({
id: {
type: 'templateId',
id: templateId,
},
externalId: body.externalId || null,
userId: user.id,
teamId: team.id,
recipients: body.recipients,
prefillFields: body.prefillFields,
folderId: body.folderId,
override: {
title: body.title,
...body.meta,
},
formValues: body.formValues,
requestMetadata: metadata,
});
} catch (err) {
return AppError.toRestAPIError(err);
}
if (body.authOptions) {
await prisma.envelope.update({
where: {
id: envelope.id,
},
data: {
authOptions: body.authOptions,
},
});
}
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
return {
status: 200,
body: {
documentId: legacyDocumentId,
recipients: envelope.recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
},
),
return {
status: 200,
body: {
documentId: legacyDocumentId,
recipients: envelope.recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
}),
sendDocument: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId } = args.params;
@@ -1374,16 +1358,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
const createdFields = await prisma.$transaction(async (tx) => {
return Promise.all(
fields.map(async (fieldData) => {
const {
recipientId,
type,
pageNumber,
pageWidth,
pageHeight,
pageX,
pageY,
fieldMeta,
} = fieldData;
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } = fieldData;
if (pageNumber <= 0) {
throw new Error('Invalid page number');
@@ -1404,9 +1379,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
throw new Error('Recipient has already signed the document');
}
const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(
type,
);
const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(type);
if (advancedField && !fieldMeta) {
throw new Error(
@@ -1510,8 +1483,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
updateField: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId, fieldId } = args.params;
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } =
args.body;
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } = args.body;
logger.info({
input: {
+3 -4
View File
@@ -1,7 +1,3 @@
import type { Team, User } from '@prisma/client';
import type { TsRestRequest } from '@ts-rest/serverless';
import type { Logger } from 'pino';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
import type { BaseApiLog, RootApiLog } from '@documenso/lib/types/api-logs';
@@ -9,6 +5,9 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { nanoid } from '@documenso/lib/universal/id';
import { logger } from '@documenso/lib/utils/logger';
import type { Team, User } from '@prisma/client';
import type { TsRestRequest } from '@ts-rest/serverless';
import type { Logger } from 'pino';
type B = {
// appRoute: any;
+1 -2
View File
@@ -1,6 +1,5 @@
import { generateOpenApi } from '@ts-rest/open-api';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { generateOpenApi } from '@ts-rest/open-api';
import { ApiContractV1 } from './contract';
+21 -40
View File
@@ -1,17 +1,4 @@
import { extendZodWithOpenApi } from '@anatine/zod-openapi';
import {
DocumentDataType,
DocumentDistributionMethod,
DocumentSigningOrder,
FieldType,
ReadStatus,
RecipientRole,
SendStatus,
SigningStatus,
} from '@prisma/client';
import { TemplateType } from '@prisma/client';
import { z } from 'zod';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
@@ -25,6 +12,18 @@ import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-emai
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import { ZFieldMetaPrefillFieldsSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { zEmail } from '@documenso/lib/utils/zod';
import {
DocumentDataType,
DocumentDistributionMethod,
DocumentSigningOrder,
FieldType,
ReadStatus,
RecipientRole,
SendStatus,
SigningStatus,
TemplateType,
} from '@prisma/client';
import { z } from 'zod';
extendZodWithOpenApi(z);
@@ -36,10 +35,7 @@ export const ZNoBodyMutationSchema = null;
export const ZGetDocumentsQuerySchema = z.object({
page: z.coerce.number().min(1).optional().default(1),
perPage: z.coerce.number().min(1).optional().default(10),
folderId: z
.string()
.describe('Filter documents by folder ID. When omitted, returns root documents.')
.optional(),
folderId: z.string().describe('Filter documents by folder ID. When omitted, returns root documents.').optional(),
});
export type TGetDocumentsQuerySchema = z.infer<typeof ZGetDocumentsQuerySchema>;
@@ -84,9 +80,7 @@ export const ZSuccessfulGetDocumentResponseSchema = ZSuccessfulDocumentResponseS
),
});
export type TSuccessfulGetDocumentResponseSchema = z.infer<
typeof ZSuccessfulGetDocumentResponseSchema
>;
export type TSuccessfulGetDocumentResponseSchema = z.infer<typeof ZSuccessfulGetDocumentResponseSchema>;
export type TSuccessfulDocumentResponseSchema = z.infer<typeof ZSuccessfulDocumentResponseSchema>;
@@ -109,9 +103,7 @@ export const ZResendDocumentForSigningMutationSchema = z.object({
recipients: z.array(z.number()),
});
export type TResendDocumentForSigningMutationSchema = z.infer<
typeof ZResendDocumentForSigningMutationSchema
>;
export type TResendDocumentForSigningMutationSchema = z.infer<typeof ZResendDocumentForSigningMutationSchema>;
export const ZSuccessfulResendDocumentResponseSchema = z.object({
message: z.string(),
@@ -161,16 +153,14 @@ export const ZCreateDocumentMutationSchema = z.object({
subject: z.string(),
message: z.string(),
timezone: z.string().default(DEFAULT_DOCUMENT_TIME_ZONE).openapi({
description:
'The timezone of the date. Must be one of the options listed in the list below.',
description: 'The timezone of the date. Must be one of the options listed in the list below.',
enum: TIME_ZONES,
}),
dateFormat: z
.string()
.default(DEFAULT_DOCUMENT_DATE_FORMAT)
.openapi({
description:
'The format of the date. Must be one of the options listed in the list below.',
description: 'The format of the date. Must be one of the options listed in the list below.',
enum: DATE_FORMATS.map((format) => format.value),
}),
redirectUrl: z.string(),
@@ -235,9 +225,7 @@ export const ZCreateDocumentMutationResponseSchema = z.object({
),
});
export type TCreateDocumentMutationResponseSchema = z.infer<
typeof ZCreateDocumentMutationResponseSchema
>;
export type TCreateDocumentMutationResponseSchema = z.infer<typeof ZCreateDocumentMutationResponseSchema>;
export const ZCreateDocumentFromTemplateMutationSchema = z.object({
title: z.string().min(1),
@@ -289,9 +277,7 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
.optional(),
});
export type TCreateDocumentFromTemplateMutationSchema = z.infer<
typeof ZCreateDocumentFromTemplateMutationSchema
>;
export type TCreateDocumentFromTemplateMutationSchema = z.infer<typeof ZCreateDocumentFromTemplateMutationSchema>;
export const ZCreateDocumentFromTemplateMutationResponseSchema = z.object({
documentId: z.number(),
@@ -376,9 +362,7 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
prefillFields: z.array(ZFieldMetaPrefillFieldsSchema).optional(),
});
export type TGenerateDocumentFromTemplateMutationSchema = z.infer<
typeof ZGenerateDocumentFromTemplateMutationSchema
>;
export type TGenerateDocumentFromTemplateMutationSchema = z.infer<typeof ZGenerateDocumentFromTemplateMutationSchema>;
export const ZGenerateDocumentFromTemplateMutationResponseSchema = z.object({
documentId: z.number(),
@@ -469,10 +453,7 @@ const ZCreateFieldSchema = z.object({
fieldMeta: ZFieldMetaSchema.openapi({}),
});
export const ZCreateFieldMutationSchema = z.union([
ZCreateFieldSchema,
z.array(ZCreateFieldSchema).min(1),
]);
export const ZCreateFieldMutationSchema = z.union([ZCreateFieldSchema, z.array(ZCreateFieldSchema).min(1)]);
export type TCreateFieldMutationSchema = z.infer<typeof ZCreateFieldMutationSchema>;
@@ -1,9 +1,8 @@
import { FieldType } from '@prisma/client';
import fs from 'node:fs';
import path from 'node:path';
import type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
import { toCheckboxCustomText } from '@documenso/lib/utils/fields';
import { FieldType } from '@prisma/client';
export type FieldTestData = TFieldAndMeta & {
page: number;
@@ -25,11 +24,7 @@ const fullColumnWidth = 57.37499999999998;
const rowHeight = 6.7;
const rowPadding = 0;
const calculatePositionPageOne = (
row: number,
column: number,
width: 'full' | 'column' = 'column',
) => {
const calculatePositionPageOne = (row: number, column: number, width: 'full' | 'column' = 'column') => {
const alignmentGridStartX = 31;
const alignmentGridStartY = 19;
@@ -41,11 +36,7 @@ const calculatePositionPageOne = (
};
};
const calculatePositionPageTwo = (
row: number,
column: number,
width: 'full' | 'column' = 'column',
) => {
const calculatePositionPageTwo = (row: number, column: number, width: 'full' | 'column' = 'column') => {
const alignmentGridStartX = 31;
const alignmentGridStartY = 16.35;
@@ -1,10 +1,9 @@
import { FieldType } from '@prisma/client';
import { toCheckboxCustomText } from '@documenso/lib/utils/fields';
import {
CheckboxValidationRules,
numberFormatValues,
} from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
import { FieldType } from '@prisma/client';
import type { FieldTestData } from './field-alignment-pdf';
import { signatureBase64Demo } from './field-alignment-pdf';
@@ -160,8 +160,7 @@ export const OVERFLOW_TEST_FIELDS: OverflowFieldTestData[] = [
seedFieldMeta: { type: 'date', overflow: 'auto' },
page: 1,
...calculateSingleLinePosition(2),
customText:
'Wednesday, April 16, 2026 at 14:30:45.123 Coordinated Universal Time signed in Melbourne, Australia',
customText: 'Wednesday, April 16, 2026 at 14:30:45.123 Coordinated Universal Time signed in Melbourne, Australia',
},
// Multi-line 3×3: Row 0 = TA_LEFT (short / medium / long)
{
@@ -1,8 +1,7 @@
import { expect, test } from '@playwright/test';
import { nanoid } from '@documenso/lib/universal/id';
import { seedOrganisationMembers } from '@documenso/prisma/seed/organisations';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../../fixtures/authentication';
@@ -1,19 +1,14 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { generateDatabaseId } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { DocumentStatus } from '@documenso/prisma/client';
import {
seedCompletedDocument,
seedDocuments,
seedPendingDocument,
} from '@documenso/prisma/seed/documents';
import { seedCompletedDocument, seedDocuments, seedPendingDocument } from '@documenso/prisma/seed/documents';
import { seedOrganisationMembers } from '@documenso/prisma/seed/organisations';
import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
import { apiSignin, apiSignout } from '../../fixtures/authentication';
@@ -1,7 +1,3 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { generateDatabaseId } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
@@ -9,6 +5,9 @@ import { seedOrganisationMembers } from '@documenso/prisma/seed/organisations';
import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
import { apiSignin, apiSignout } from '../../fixtures/authentication';
@@ -171,9 +170,7 @@ test.describe('Template Search - Cross-Team Isolation', () => {
// ─── Recipient Email Search ──────────────────────────────────────────────────
test.describe('Template Search - Recipient Email', () => {
test('should find templates by recipient email within team but not cross-team', async ({
page,
}) => {
test('should find templates by recipient email within team but not cross-team', async ({ page }) => {
const { team: teamA, owner: ownerA } = await seedTeam();
const adminUserA = await seedTeamMember({ teamId: teamA.id, role: TeamMemberRole.ADMIN });
const { team: teamB, owner: ownerB } = await seedTeam();
@@ -1,15 +1,11 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { FieldType, RecipientRole } from '@documenso/prisma/client';
import {
seedBlankDocument,
seedPendingDocumentWithFullFields,
} from '@documenso/prisma/seed/documents';
import { seedBlankDocument, seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
test.describe('Document API', () => {
test('sendDocument: should respect sendCompletionEmails setting', async ({ request }) => {
@@ -193,9 +189,7 @@ test.describe('Document API', () => {
expect(response.status()).toBe(400);
});
test('sendDocument: should fail when signer has only non-signature fields', async ({
request,
}) => {
test('sendDocument: should fail when signer has only non-signature fields', async ({ request }) => {
const { user, team } = await seedUser();
// Create a blank document and get it with envelope items
@@ -1,5 +1,3 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import type { TCheckboxFieldMeta, TRadioFieldMeta } from '@documenso/lib/types/field-meta';
@@ -12,16 +10,14 @@ import { prisma } from '@documenso/prisma';
import { FieldType, RecipientRole } from '@documenso/prisma/client';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../../fixtures/authentication';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
test.describe('Template Field Prefill API v1', () => {
test('should create a document from template with prefilled fields', async ({
page,
request,
}) => {
test('should create a document from template with prefilled fields', async ({ page, request }) => {
// 1. Create a user
const { user, team } = await seedUser();
@@ -196,9 +192,7 @@ test.describe('Template Field Prefill API v1', () => {
});
// 7. Navigate to the template
await page.goto(
`${WEBAPP_BASE_URL}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`,
);
await page.goto(`${WEBAPP_BASE_URL}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`);
// 8. Create a document from the template with prefilled fields
const response = await request.post(
@@ -381,10 +375,7 @@ test.describe('Template Field Prefill API v1', () => {
await expect(page.getByText('Select B')).toBeVisible();
});
test('should create a document from template without prefilled fields', async ({
page,
request,
}) => {
test('should create a document from template without prefilled fields', async ({ page, request }) => {
// 1. Create a user
const { user, team } = await seedUser();
@@ -487,9 +478,7 @@ test.describe('Template Field Prefill API v1', () => {
});
// 7. Navigate to the template
await page.goto(
`${WEBAPP_BASE_URL}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`,
);
await page.goto(`${WEBAPP_BASE_URL}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`);
// 8. Create a document from the template without prefilled fields
const response = await request.post(
@@ -1,11 +1,6 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import {
mapSecondaryIdToDocumentId,
mapSecondaryIdToTemplateId,
} from '@documenso/lib/utils/envelope';
import { mapSecondaryIdToDocumentId, mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { DocumentDataType, FieldType } from '@documenso/prisma/client';
import {
@@ -16,6 +11,7 @@ import {
} from '@documenso/prisma/seed/documents';
import { seedBlankTemplate, seedTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
@@ -25,9 +21,7 @@ test.describe.configure({
test.describe('Document Access API V1', () => {
test.describe('Document GET endpoint', () => {
test('should block unauthorized access to documents not owned by the user', async ({
request,
}) => {
test('should block unauthorized access to documents not owned by the user', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
@@ -319,9 +313,7 @@ test.describe('Document Access API V1', () => {
});
test.describe('Document recipients POST endpoint', () => {
test('should block unauthorized access to document recipients endpoint', async ({
request,
}) => {
test('should block unauthorized access to document recipients endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
@@ -371,9 +363,7 @@ test.describe('Document Access API V1', () => {
});
test.describe('Document recipients PATCH endpoint', () => {
test('should block unauthorized access to PATCH on recipients endpoint', async ({
request,
}) => {
test('should block unauthorized access to PATCH on recipients endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
@@ -459,9 +449,7 @@ test.describe('Document Access API V1', () => {
});
test.describe('Document recipients DELETE endpoint', () => {
test('should block unauthorized access to DELETE on recipients endpoint', async ({
request,
}) => {
test('should block unauthorized access to DELETE on recipients endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
@@ -972,9 +960,7 @@ test.describe('Document Access API V1', () => {
expect(resB.ok()).toBeTruthy();
expect(resB.status()).toBe(200);
expect(reqData.documents.every((doc: { userId: number }) => doc.userId !== userA.id)).toBe(
true,
);
expect(reqData.documents.every((doc: { userId: number }) => doc.userId !== userA.id)).toBe(true);
expect(reqData.documents.length).toBe(0);
expect(reqData.totalPages).toBe(0);
});
@@ -999,9 +985,7 @@ test.describe('Document Access API V1', () => {
expect(resA.ok()).toBeTruthy();
expect(resA.status()).toBe(200);
expect(reqData.documents.length).toBeGreaterThan(0);
expect(reqData.documents.every((doc: { userId: number }) => doc.userId === userA.id)).toBe(
true,
);
expect(reqData.documents.every((doc: { userId: number }) => doc.userId === userA.id)).toBe(true);
});
});
@@ -1027,9 +1011,7 @@ test.describe('Document Access API V1', () => {
expect(resB.ok()).toBeTruthy();
expect(resB.status()).toBe(200);
expect(reqData.templates.every((tpl: { userId: number }) => tpl.userId !== userA.id)).toBe(
true,
);
expect(reqData.templates.every((tpl: { userId: number }) => tpl.userId !== userA.id)).toBe(true);
expect(reqData.templates.length).toBe(0);
expect(reqData.totalPages).toBe(0);
});
@@ -1054,16 +1036,12 @@ test.describe('Document Access API V1', () => {
expect(resA.ok()).toBeTruthy();
expect(resA.status()).toBe(200);
expect(reqData.templates.length).toBeGreaterThan(0);
expect(reqData.templates.every((tpl: { userId: number }) => tpl.userId === userA.id)).toBe(
true,
);
expect(reqData.templates.every((tpl: { userId: number }) => tpl.userId === userA.id)).toBe(true);
});
});
test.describe('Create document from template endpoint', () => {
test('should block unauthorized access to create-document-from-template endpoint', async ({
request,
}) => {
test('should block unauthorized access to create-document-from-template endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
@@ -1104,9 +1082,7 @@ test.describe('Document Access API V1', () => {
expect(resB.status()).toBe(401);
});
test('should allow authorized access to create-document-from-template endpoint', async ({
request,
}) => {
test('should allow authorized access to create-document-from-template endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { token: tokenA } = await createApiToken({
userId: userA.id,
@@ -1,8 +1,5 @@
import { type APIRequestContext, 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 { EnvelopeType, FieldType, RecipientRole } from '@documenso/prisma/client';
@@ -13,6 +10,8 @@ import type {
} 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 { type APIRequestContext, 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`;
@@ -138,9 +137,7 @@ test.describe('Envelope distribute validation', () => {
expect(errorResponse.message).toContain('Signers must have at least one signature field');
});
test('should fail to distribute when signer has non-signature fields only', async ({
request,
}) => {
test('should fail to distribute when signer has non-signature fields only', async ({ request }) => {
const envelope = await createEnvelope(request, token);
const envelopeData = await getEnvelope(request, token, envelope.id);
@@ -321,9 +318,7 @@ test.describe('Envelope distribute validation', () => {
expect(distributeRes.status()).toBe(200);
});
test('should fail when one of multiple signers is missing signature field', async ({
request,
}) => {
test('should fail when one of multiple signers is missing signature field', async ({ request }) => {
const envelope = await createEnvelope(request, token);
const envelopeData = await getEnvelope(request, token, envelope.id);
@@ -1,16 +1,13 @@
import { expect, test } from '@playwright/test';
import type { APIRequestContext } from 'playwright-core';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import type { CreateEmbeddingPresignTokenOptions } from '@documenso/lib/server-only/embedding-presign/create-embedding-presign-token';
import type { VerifyEmbeddingPresignTokenOptions } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import type { APIRequestContext } from 'playwright-core';
test.describe('Embedding Presign API', () => {
test('createEmbeddingPresignToken: should create a token with default expiration', async ({
request,
}) => {
test('createEmbeddingPresignToken: should create a token with default expiration', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
@@ -34,9 +31,7 @@ test.describe('Embedding Presign API', () => {
expect(responseData.expiresIn).toBe(3600); // Default 1 hour in seconds
});
test('createEmbeddingPresignToken: should create a token with custom expiration', async ({
request,
}) => {
test('createEmbeddingPresignToken: should create a token with custom expiration', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
@@ -187,9 +182,7 @@ test.describe('Embedding Presign API', () => {
expect(verifyResponseData.success).toBe(true);
});
test('verifyEmbeddingPresignToken: should reject a scope mismatched token', async ({
request,
}) => {
test('verifyEmbeddingPresignToken: should reject a scope mismatched token', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
@@ -233,19 +226,16 @@ const createPresignToken = async (
apiToken: string,
data?: Partial<CreateEmbeddingPresignTokenOptions>,
) => {
return await request.post(
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/create-presign-token`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
data: {
apiToken,
...data,
},
return await request.post(`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/create-presign-token`, {
headers: {
Authorization: `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
);
data: {
apiToken,
...data,
},
});
};
const verifyPresignToken = async (
@@ -253,14 +243,11 @@ const verifyPresignToken = async (
apiToken: string,
data: VerifyEmbeddingPresignTokenOptions,
) => {
return await request.post(
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/verify-presign-token`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
data,
return await request.post(`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/verify-presign-token`, {
headers: {
Authorization: `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
);
data,
});
};
@@ -1,9 +1,5 @@
import { type APIRequestContext, expect, test } from '@playwright/test';
import type { Team, User } from '@prisma/client';
import fs from 'node:fs';
import path from 'node:path';
import { pick } from 'remeda';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
@@ -19,17 +15,20 @@ import {
RecipientRole,
} from '@documenso/prisma/client';
import { seedUser } from '@documenso/prisma/seed/users';
import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.types';
import type {
TCreateEnvelopePayload,
TCreateEnvelopeResponse,
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.types';
import type { TDistributeEnvelopeRequest } from '@documenso/trpc/server/envelope-router/distribute-envelope.types';
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
import type { TUpdateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/update-envelope-recipients.types';
import type { TFindEnvelopesResponse } from '@documenso/trpc/server/envelope-router/find-envelopes.types';
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
import { type APIRequestContext, expect, test } from '@playwright/test';
import type { Team, User } from '@prisma/client';
import { pick } from 'remeda';
import { ALIGNMENT_TEST_FIELDS } from '../../../constants/field-alignment-pdf';
import { FIELD_META_TEST_FIELDS } from '../../../constants/field-meta-pdf';
@@ -94,9 +93,7 @@ test.describe('API V2 Envelopes', () => {
const files = [
{
name: 'field-font-alignment.pdf',
data: fs.readFileSync(
path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'),
),
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/field-font-alignment.pdf')),
},
];
@@ -232,9 +229,7 @@ test.describe('API V2 Envelopes', () => {
},
{
name: 'field-font-alignment.pdf',
data: fs.readFileSync(
path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'),
),
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/field-font-alignment.pdf')),
},
];
@@ -294,15 +289,11 @@ test.describe('API V2 Envelopes', () => {
expect(envelope.documentMeta.dateFormat).toBe(payload.meta.dateFormat);
expect(envelope.documentMeta.distributionMethod).toBe(payload.meta.distributionMethod);
expect(envelope.documentMeta.signingOrder).toBe(payload.meta.signingOrder);
expect(envelope.documentMeta.allowDictateNextSigner).toBe(
payload.meta.allowDictateNextSigner,
);
expect(envelope.documentMeta.allowDictateNextSigner).toBe(payload.meta.allowDictateNextSigner);
expect(envelope.documentMeta.redirectUrl).toBe(payload.meta.redirectUrl);
expect(envelope.documentMeta.language).toBe(payload.meta.language);
expect(envelope.documentMeta.typedSignatureEnabled).toBe(payload.meta.typedSignatureEnabled);
expect(envelope.documentMeta.uploadSignatureEnabled).toBe(
payload.meta.uploadSignatureEnabled,
);
expect(envelope.documentMeta.uploadSignatureEnabled).toBe(payload.meta.uploadSignatureEnabled);
expect(envelope.documentMeta.drawSignatureEnabled).toBe(payload.meta.drawSignatureEnabled);
expect(envelope.documentMeta.emailReplyTo).toBe(payload.meta.emailReplyTo);
expect(envelope.documentMeta.emailSettings).toEqual(payload.meta.emailSettings);
@@ -324,9 +315,7 @@ test.describe('API V2 Envelopes', () => {
role: recipient.role,
signingOrder: recipient.signingOrder,
accessAuth: recipient.authOptions?.accessAuth,
}).toEqual(
pick(payload.recipients[0], ['email', 'name', 'role', 'signingOrder', 'accessAuth']),
);
}).toEqual(pick(payload.recipients[0], ['email', 'name', 'role', 'signingOrder', 'accessAuth']));
expect({
type: field.type,
@@ -335,16 +324,7 @@ test.describe('API V2 Envelopes', () => {
positionY: field.positionY.toNumber(),
width: field.width.toNumber(),
height: field.height.toNumber(),
}).toEqual(
pick(payload.recipients[0].fields[0], [
'type',
'page',
'positionX',
'positionY',
'width',
'height',
]),
);
}).toEqual(pick(payload.recipients[0].fields[0], ['type', 'page', 'positionX', 'positionY', 'width', 'height']));
// Expect string based ID to work.
expect(field.envelopeItemId).toBe(
@@ -363,13 +343,9 @@ test.describe('API V2 Envelopes', () => {
*/
test('Envelope full test', async ({ request }) => {
// Step 1: Create initial envelope with Prisma (with first envelope item)
const alignmentPdf = fs.readFileSync(
path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'),
);
const alignmentPdf = fs.readFileSync(path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'));
const fieldMetaPdf = fs.readFileSync(
path.join(__dirname, '../../../../../assets/field-meta.pdf'),
);
const fieldMetaPdf = fs.readFileSync(path.join(__dirname, '../../../../../assets/field-meta.pdf'));
const formData = new FormData();
@@ -382,10 +358,7 @@ test.describe('API V2 Envelopes', () => {
);
// Only add one file for now.
formData.append(
'files',
new File([alignmentPdf], 'field-font-alignment.pdf', { type: 'application/pdf' }),
);
formData.append('files', new File([alignmentPdf], 'field-font-alignment.pdf', { type: 'application/pdf' }));
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${tokenA}` },
@@ -418,10 +391,7 @@ test.describe('API V2 Envelopes', () => {
const createEnvelopeItemFormData = new FormData();
createEnvelopeItemFormData.append('payload', JSON.stringify(createEnvelopeItemsPayload));
createEnvelopeItemFormData.append(
'files',
new File([fieldMetaPdf], 'field-meta.pdf', { type: 'application/pdf' }),
);
createEnvelopeItemFormData.append('files', new File([fieldMetaPdf], 'field-meta.pdf', { type: 'application/pdf' }));
const createItemsRes = await request.post(`${baseUrl}/envelope/item/create-many`, {
headers: { Authorization: `Bearer ${tokenA}` },
@@ -481,12 +451,8 @@ test.describe('API V2 Envelopes', () => {
const envelopeResponse = (await getRes.json()) as TGetEnvelopeResponse;
const recipientId = envelopeResponse.recipients[0].id;
const alignmentItem = envelopeResponse.envelopeItems.find(
(item: { order: number }) => item.order === 1,
);
const fieldMetaItem = envelopeResponse.envelopeItems.find(
(item: { order: number }) => item.order === 2,
);
const alignmentItem = envelopeResponse.envelopeItems.find((item: { order: number }) => item.order === 1);
const fieldMetaItem = envelopeResponse.envelopeItems.find((item: { order: number }) => item.order === 2);
expect(recipientId).toBeDefined();
expect(alignmentItem).toBeDefined();
@@ -555,9 +521,7 @@ test.describe('API V2 Envelopes', () => {
// Verify structure
expect(finalEnvelope.envelopeItems.length).toBe(2);
expect(finalEnvelope.recipients.length).toBe(1);
expect(finalEnvelope.fields.length).toBe(
ALIGNMENT_TEST_FIELDS.length + FIELD_META_TEST_FIELDS.length,
);
expect(finalEnvelope.fields.length).toBe(ALIGNMENT_TEST_FIELDS.length + FIELD_META_TEST_FIELDS.length);
expect(finalEnvelope.title).toBe('Envelope Full Field Test');
expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT);
@@ -568,17 +532,11 @@ test.describe('API V2 Envelopes', () => {
});
test.describe('Envelope find endpoint', () => {
const createEnvelope = async (
request: APIRequestContext,
token: string,
payload: TCreateEnvelopePayload,
) => {
const createEnvelope = async (request: APIRequestContext, token: string, payload: TCreateEnvelopePayload) => {
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
const pdfData = fs.readFileSync(
path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'),
);
const pdfData = fs.readFileSync(path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'));
formData.append('files', new File([pdfData], 'test.pdf', { type: 'application/pdf' }));
const res = await request.post(`${baseUrl}/envelope/create`, {
@@ -1199,9 +1157,7 @@ test.describe('API V2 Envelopes', () => {
expect(distributeResponse.recipients[1].signingUrl).toBeTruthy();
});
test('Distribute envelope with empty email recipient and auth requirements fails', async ({
request,
}) => {
test('Distribute envelope with empty email recipient and auth requirements fails', async ({ request }) => {
const payload = {
type: EnvelopeType.DOCUMENT,
title: 'Document with Auth Requirements',
@@ -1,6 +1,3 @@
import { expect, test } from '@playwright/test';
import type { Team, User } from '@prisma/client';
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';
@@ -15,6 +12,8 @@ import {
import { seedTeam, seedTeamEmail, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/find-documents.types';
import { expect, test } from '@playwright/test';
import type { Team, User } from '@prisma/client';
import { apiSignin } from '../../fixtures/authentication';
@@ -78,9 +77,7 @@ test.describe('Find Documents API - Personal Context', () => {
expect(json!.totalPages).toBe(0);
});
test('should return only documents owned by the user and not the other user', async ({
request,
}) => {
test('should return only documents owned by the user and not the other user', async ({ request }) => {
// The v2 API token scopes to a team. A personal team token only returns
// docs belonging to that team — cross-team received docs are NOT included.
await seedDraftDocument(userA, teamA.id, [], {
@@ -287,9 +284,7 @@ test.describe('Find Documents API - Personal Context', () => {
expect(json!.data[0].title).toBe('Quarterly Report 2024');
});
test('should search by recipient email and not return docs with different recipients', async ({
request,
}) => {
test('should search by recipient email and not return docs with different recipients', async ({ request }) => {
const { user: userC } = await seedUser();
await seedPendingDocument(userA, teamA.id, [userB], {
@@ -404,9 +399,7 @@ test.describe('Find Documents API - Personal Context', () => {
}
});
test('should only show root-level documents when no folderId is provided', async ({
request,
}) => {
test('should only show root-level documents when no folderId is provided', async ({ request }) => {
const folder = await prisma.folder.create({
data: {
name: 'Test Folder',
@@ -499,9 +492,7 @@ test.describe('Find Documents API - Personal Context', () => {
expect(doc.recipients[0].email).toBe(userB.email);
});
test('should not return deleted documents but should return non-deleted ones', async ({
request,
}) => {
test('should not return deleted documents but should return non-deleted ones', async ({ request }) => {
const deletedDoc = await seedDraftDocument(userA, teamA.id, [], {
createDocumentOptions: { title: 'Deleted Document' },
});
@@ -562,9 +553,7 @@ test.describe('Find Documents API - Personal Context', () => {
});
test.describe('Find Documents API - Team Context', () => {
test('should return team documents for team members and exclude non-team docs', async ({
request,
}) => {
test('should return team documents for team members and exclude non-team docs', async ({ request }) => {
const { team, owner } = await seedTeam();
const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.ADMIN });
@@ -723,9 +712,7 @@ test.describe('Find Documents API - Team Context', () => {
expect(json!.count).toBe(2);
});
test('should enforce visibility across admin and manager levels with adequate data', async ({
request,
}) => {
test('should enforce visibility across admin and manager levels with adequate data', async ({ request }) => {
// Note: MEMBER role cannot create API tokens (requires MANAGE_TEAM permission).
// MEMBER visibility is tested in the UI test file instead.
const { team, owner } = await seedTeam();
@@ -978,9 +965,7 @@ test.describe('Find Documents API - Team with Team Email', () => {
expect(titles).not.toContain('External Noise Doc');
});
test('team email documents should respect visibility rules with adequate controls', async ({
request,
}) => {
test('team email documents should respect visibility rules with adequate controls', async ({ request }) => {
const { team } = await seedTeam();
const teamEmail = `team-vis-email-${team.id}@test.documenso.com`;
@@ -1033,9 +1018,7 @@ test.describe('Find Documents API - Team with Team Email', () => {
});
test.describe('Find Documents API - Deleted Document Handling', () => {
test('should not show soft-deleted documents for owner but show non-deleted ones', async ({
request,
}) => {
test('should not show soft-deleted documents for owner but show non-deleted ones', async ({ request }) => {
const { user, team } = await seedUser();
const deletedDoc = await seedPendingDocument(user, team.id, [], {
@@ -1068,9 +1051,7 @@ test.describe('Find Documents API - Deleted Document Handling', () => {
expect(titles).not.toContain('Soft Deleted by Owner');
});
test('should not show documents where owner soft-deleted their copy in personal context', async ({
request,
}) => {
test('should not show documents where owner soft-deleted their copy in personal context', async ({ request }) => {
// In personal context, documentDeletedAt on recipient hides the doc for that user.
// Note: the v2 API scopes to a team, so we test this by having the owner
// soft-delete a doc from their own personal team.
@@ -1102,9 +1083,7 @@ test.describe('Find Documents API - Deleted Document Handling', () => {
expect(titles).not.toContain('Owner Soft Deleted');
});
test('should not show deleted team documents for any team member but show non-deleted ones', async ({
request,
}) => {
test('should not show deleted team documents for any team member but show non-deleted ones', async ({ request }) => {
const { team, owner } = await seedTeam();
const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.ADMIN });
@@ -1198,9 +1177,7 @@ test.describe('Find Documents API - Edge Cases', () => {
expect(res.ok()).toBeFalsy();
});
test('personal documents should not appear in team context even with adequate team data', async ({
request,
}) => {
test('personal documents should not appear in team context even with adequate team data', async ({ request }) => {
const { team, owner } = await seedTeam();
const teamMember = await seedTeamMember({
@@ -1276,9 +1253,7 @@ const trpcQuery = async (
};
test.describe('Find Documents API - Adversarial: x-team-id Header Spoofing', () => {
test('should reject request when user spoofs x-team-id to a team they do not belong to', async ({
page,
}) => {
test('should reject request when user spoofs x-team-id to a team they do not belong to', async ({ page }) => {
// Setup: two separate teams with documents
const { team: teamA, owner: ownerA } = await seedTeam();
const { team: teamB, owner: ownerB } = await seedTeam();
@@ -1365,9 +1340,7 @@ test.describe('Find Documents API - Adversarial: x-team-id Header Spoofing', ()
});
test.describe('Find Documents API - Adversarial: Cross-Team folderId', () => {
test('should NOT return documents from another team when folderId belongs to that team', async ({
request,
}) => {
test('should NOT return documents from another team when folderId belongs to that team', async ({ request }) => {
// Setup: two teams each with a folder and documents
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
@@ -1452,9 +1425,7 @@ test.describe('Find Documents API - Adversarial: Cross-Team folderId', () => {
});
test.describe('Find Documents API - Adversarial: Cross-Team senderIds', () => {
test('should NOT return documents when senderIds contains users from another team', async ({
page,
}) => {
test('should NOT return documents when senderIds contains users from another team', async ({ page }) => {
const { team: teamA, owner: ownerA } = await seedTeam();
const { team: teamB, owner: ownerB } = await seedTeam();
@@ -1495,9 +1466,7 @@ test.describe('Find Documents API - Adversarial: Cross-Team senderIds', () => {
expect(docs2[0].title).toBe('TeamA Doc by OwnerA');
});
test('senderIds with mix of valid and cross-team userIds should only return matching team docs', async ({
page,
}) => {
test('senderIds with mix of valid and cross-team userIds should only return matching team docs', async ({ page }) => {
const { team, owner } = await seedTeam();
const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.ADMIN });
const { user: outsider } = await seedUser();
@@ -1542,9 +1511,7 @@ test.describe('Find Documents API - Adversarial: Cross-Team senderIds', () => {
});
test.describe('Find Documents API - Adversarial: Cross-Team templateId', () => {
test('should NOT return documents from another team when filtering by their templateId', async ({
request,
}) => {
test('should NOT return documents from another team when filtering by their templateId', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
@@ -1,15 +1,7 @@
import { expect, test } from '@playwright/test';
import type { Team, User } from '@prisma/client';
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 {
DocumentStatus,
DocumentVisibility,
EnvelopeType,
TeamMemberRole,
} from '@documenso/prisma/client';
import { DocumentStatus, DocumentVisibility, EnvelopeType, TeamMemberRole } from '@documenso/prisma/client';
import {
seedBlankDocument,
seedCompletedDocument,
@@ -19,6 +11,8 @@ import {
import { seedTeam, seedTeamEmail, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
import type { TFindEnvelopesResponse } from '@documenso/trpc/server/envelope-router/find-envelopes.types';
import { expect, test } from '@playwright/test';
import type { Team, User } from '@prisma/client';
import { apiSignin } from '../../fixtures/authentication';
@@ -81,9 +75,7 @@ test.describe('Find Envelopes API - Basic', () => {
expect(json!.totalPages).toBe(0);
});
test('should return only envelopes owned by the user and not the other user', async ({
request,
}) => {
test('should return only envelopes owned by the user and not the other user', async ({ request }) => {
await seedDraftDocument(userA, teamA.id, [], {
createDocumentOptions: { title: 'UserA Doc' },
});
@@ -445,9 +437,7 @@ test.describe('Find Envelopes API - Token Masking', () => {
test.describe('Find Envelopes API - Team Context', () => {
// Regression test: findEnvelopes previously had `{ userId }` as a top-level OR branch with no
// teamId constraint, so personal docs leaked into team context. Fixed in the Kysely refactor.
test('should return team envelopes for team members and exclude personal docs', async ({
request,
}) => {
test('should return team envelopes for team members and exclude personal docs', async ({ request }) => {
const { team, owner } = await seedTeam();
const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.ADMIN });
@@ -672,9 +662,7 @@ test.describe('Find Envelopes API - Team Context', () => {
// ─── Team with Team Email ────────────────────────────────────────────────────
test.describe('Find Envelopes API - Team Email', () => {
test('should include envelopes received by team email from external senders', async ({
request,
}) => {
test('should include envelopes received by team email from external senders', async ({ request }) => {
const { team, owner } = await seedTeam();
const teamEmailAddr = `team-find-env-${team.id}@test.documenso.com`;
await seedTeamEmail({ email: teamEmailAddr, teamId: team.id });
@@ -711,9 +699,7 @@ test.describe('Find Envelopes API - Team Email', () => {
expect(titles).not.toContain('External Noise Doc');
});
test('should NOT include external noise from other teams when team has team email', async ({
request,
}) => {
test('should NOT include external noise from other teams when team has team email', async ({ request }) => {
const { team: teamA, owner: ownerA } = await seedTeam();
const teamEmailAddr = `team-noise-${teamA.id}@test.documenso.com`;
await seedTeamEmail({ email: teamEmailAddr, teamId: teamA.id });
@@ -809,9 +795,7 @@ const trpcQuery = async (
};
test.describe('Find Envelopes API - Adversarial: x-team-id Header Spoofing', () => {
test('should reject request when user spoofs x-team-id to a team they do not belong to', async ({
page,
}) => {
test('should reject request when user spoofs x-team-id to a team they do not belong to', async ({ page }) => {
const { team: teamA, owner: ownerA } = await seedTeam();
const { team: teamB, owner: ownerB } = await seedTeam();
@@ -883,9 +867,7 @@ test.describe('Find Envelopes API - Adversarial: x-team-id Header Spoofing', ()
});
test.describe('Find Envelopes API - Adversarial: Cross-Team folderId', () => {
test('should NOT return envelopes from another team when folderId belongs to that team', async ({
request,
}) => {
test('should NOT return envelopes from another team when folderId belongs to that team', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
@@ -965,9 +947,7 @@ test.describe('Find Envelopes API - Adversarial: Cross-Team folderId', () => {
});
test.describe('Find Envelopes API - Adversarial: Cross-Team templateId', () => {
test('should NOT return envelopes from another team when filtering by their templateId', async ({
request,
}) => {
test('should NOT return envelopes from another team when filtering by their templateId', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
@@ -1007,9 +987,7 @@ test.describe('Find Envelopes API - Adversarial: Cross-Team templateId', () => {
// ─── Personal vs Team Isolation ──────────────────────────────────────────────
test.describe('Find Envelopes API - Cross-User Isolation', () => {
test('other users personal envelopes should never appear regardless of context', async ({
request,
}) => {
test('other users personal envelopes should never appear regardless of context', async ({ request }) => {
const { team } = await seedTeam();
const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.ADMIN });
const { user: outsider, team: outsiderTeam } = await seedUser();
@@ -1,23 +1,18 @@
import { PDF } from '@libpdf/core';
import type { APIRequestContext } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { DocumentStatus, FieldType, SigningStatus } from '@prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { PDF } from '@libpdf/core';
import type { APIRequestContext } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { DocumentStatus, FieldType, SigningStatus } from '@prisma/client';
import { apiSeedDraftDocument, apiSeedPendingDocument } from '../../fixtures/api-seeds';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
const API_BASE_URL = `${WEBAPP_BASE_URL}/api/v2-beta`;
const trpcMutation = async (
request: APIRequestContext,
procedure: string,
input: Record<string, unknown>,
) => {
const trpcMutation = async (request: APIRequestContext, procedure: string, input: Record<string, unknown>) => {
const res = await request.post(`${WEBAPP_BASE_URL}/api/trpc/${procedure}`, {
headers: {
'content-type': 'application/json',
@@ -63,9 +58,7 @@ const signAndCompleteRecipient = async ({
};
test.describe('API V2 partial signed PDF downloads', () => {
test('returns a PDF with inserted fields, supports ETag, and rejects after completion', async ({
request,
}) => {
test('returns a PDF with inserted fields, supports ETag, and rejects after completion', async ({ request }) => {
const { envelope, token, distributeResult } = await apiSeedPendingDocument(request, {
recipients: [
{ email: 'partial-signer-1@test.documenso.com', name: 'Partial Signer 1' },
@@ -118,9 +111,9 @@ test.describe('API V2 partial signed PDF downloads', () => {
});
expect(dbEnvelope.status).toBe(DocumentStatus.PENDING);
expect(
dbEnvelope.recipients.find((recipient) => recipient.id === recipientOne.id)?.signingStatus,
).toBe(SigningStatus.SIGNED);
expect(dbEnvelope.recipients.find((recipient) => recipient.id === recipientOne.id)?.signingStatus).toBe(
SigningStatus.SIGNED,
);
}).toPass();
const downloadUrl = `${API_BASE_URL}/envelope/item/${envelopeItem.id}/download?version=pending`;
@@ -1,10 +1,5 @@
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';
@@ -16,6 +11,10 @@ import type {
} 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`;
@@ -66,11 +65,7 @@ test.describe('Placeholder-based field creation', () => {
return res.json();
};
const createEnvelopeItemsWithPdf = async (
request: APIRequestContext,
envelopeId: string,
pdfFilename: string,
) => {
const createEnvelopeItemsWithPdf = async (request: APIRequestContext, envelopeId: string, pdfFilename: string) => {
const pdfPath = path.join(FIXTURES_DIR, pdfFilename);
const pdfData = fs.readFileSync(pdfPath);
@@ -129,10 +124,7 @@ test.describe('Placeholder-based field creation', () => {
expect(res.ok()).toBeTruthy();
};
const getEnvelope = async (
request: APIRequestContext,
envelopeId: string,
): Promise<TGetEnvelopeResponse> => {
const getEnvelope = async (request: APIRequestContext, envelopeId: string): Promise<TGetEnvelopeResponse> => {
const res = await request.get(`${baseUrl}/envelope/${envelopeId}`, {
headers: { Authorization: `Bearer ${token}` },
});
@@ -281,9 +273,7 @@ test.describe('Placeholder-based field creation', () => {
expect(createFieldsRes.ok()).toBeFalsy();
});
test('should create fields using a mix of coordinate and placeholder positioning', async ({
request,
}) => {
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);
@@ -409,9 +399,7 @@ test.describe('Placeholder-based field creation', () => {
expect(uniqueYPositions.size).toBe(3);
});
test('should map placeholder recipients by signing order when adding items', async ({
request,
}) => {
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, [
@@ -1,5 +1,3 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import type { TCheckboxFieldMeta, TRadioFieldMeta } from '@documenso/lib/types/field-meta';
@@ -12,16 +10,14 @@ import { prisma } from '@documenso/prisma';
import { FieldType, RecipientRole } from '@documenso/prisma/client';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../../fixtures/authentication';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
test.describe('Template Field Prefill API v2', () => {
test('should create a document from template with prefilled fields', async ({
page,
request,
}) => {
test('should create a document from template with prefilled fields', async ({ page, request }) => {
// 1. Create a user
const { user, team } = await seedUser();
@@ -196,9 +192,7 @@ test.describe('Template Field Prefill API v2', () => {
});
// 7. Navigate to the template
await page.goto(
`${WEBAPP_BASE_URL}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`,
);
await page.goto(`${WEBAPP_BASE_URL}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`);
// 8. Create a document from the template with prefilled fields using v2 API
const response = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
@@ -378,10 +372,7 @@ test.describe('Template Field Prefill API v2', () => {
await expect(page.getByText('Select B')).toBeVisible();
});
test('should create a document from template without prefilled fields', async ({
page,
request,
}) => {
test('should create a document from template without prefilled fields', async ({ page, request }) => {
// 1. Create a user
const { user, team } = await seedUser();
@@ -484,9 +475,7 @@ test.describe('Template Field Prefill API v2', () => {
});
// 7. Navigate to the template
await page.goto(
`${WEBAPP_BASE_URL}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`,
);
await page.goto(`${WEBAPP_BASE_URL}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`);
// 8. Create a document from the template without prefilled fields using v2 API
const response = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,10 @@
import { expect, test } from '@playwright/test';
import type { Team, User } from '@prisma/client';
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 { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import type { Team, User } from '@prisma/client';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
@@ -35,33 +34,23 @@ test.describe('Envelope Attachments API V2', () => {
});
test.describe('Envelope attachment find endpoint', () => {
test('should block unauthorized access to envelope attachment find endpoint', async ({
request,
}) => {
test('should block unauthorized access to envelope attachment find endpoint', async ({ request }) => {
const doc = await seedBlankDocument(userA, teamA.id);
const res = await request.get(
`${WEBAPP_BASE_URL}/api/v2-beta/envelope/attachment?envelopeId=${doc.id}`,
{
headers: { Authorization: `Bearer ${tokenB}` },
},
);
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/attachment?envelopeId=${doc.id}`, {
headers: { Authorization: `Bearer ${tokenB}` },
});
expect(res.ok()).toBeFalsy();
expect(res.status()).toBe(404);
});
test('should allow authorized access to envelope attachment find endpoint', async ({
request,
}) => {
test('should allow authorized access to envelope attachment find endpoint', async ({ request }) => {
const doc = await seedBlankDocument(userA, teamA.id);
const res = await request.get(
`${WEBAPP_BASE_URL}/api/v2-beta/envelope/attachment?envelopeId=${doc.id}`,
{
headers: { Authorization: `Bearer ${tokenA}` },
},
);
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/attachment?envelopeId=${doc.id}`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
@@ -69,9 +58,7 @@ test.describe('Envelope Attachments API V2', () => {
});
test.describe('Envelope attachment create endpoint', () => {
test('should block unauthorized access to envelope attachment create endpoint', async ({
request,
}) => {
test('should block unauthorized access to envelope attachment create endpoint', async ({ request }) => {
const doc = await seedBlankDocument(userA, teamA.id);
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/attachment/create`, {
@@ -89,9 +76,7 @@ test.describe('Envelope Attachments API V2', () => {
expect(res.status()).toBe(404);
});
test('should allow authorized access to envelope attachment create endpoint', async ({
request,
}) => {
test('should allow authorized access to envelope attachment create endpoint', async ({ request }) => {
const doc = await seedBlankDocument(userA, teamA.id);
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/attachment/create`, {
@@ -111,9 +96,7 @@ test.describe('Envelope Attachments API V2', () => {
});
test.describe('Envelope attachment update endpoint', () => {
test('should block unauthorized access to envelope attachment update endpoint', async ({
request,
}) => {
test('should block unauthorized access to envelope attachment update endpoint', async ({ request }) => {
const doc = await seedBlankDocument(userA, teamA.id);
const attachment = await prisma.envelopeAttachment.create({
@@ -140,9 +123,7 @@ test.describe('Envelope Attachments API V2', () => {
expect(res.status()).toBe(404);
});
test('should allow authorized access to envelope attachment update endpoint', async ({
request,
}) => {
test('should allow authorized access to envelope attachment update endpoint', async ({ request }) => {
const doc = await seedBlankDocument(userA, teamA.id);
const attachment = await prisma.envelopeAttachment.create({
@@ -171,9 +152,7 @@ test.describe('Envelope Attachments API V2', () => {
});
test.describe('Envelope attachment delete endpoint', () => {
test('should block unauthorized access to envelope attachment delete endpoint', async ({
request,
}) => {
test('should block unauthorized access to envelope attachment delete endpoint', async ({ request }) => {
const doc = await seedBlankDocument(userA, teamA.id);
const attachment = await prisma.envelopeAttachment.create({
@@ -194,9 +173,7 @@ test.describe('Envelope Attachments API V2', () => {
expect(res.status()).toBe(404);
});
test('should allow authorized access to envelope attachment delete endpoint', async ({
request,
}) => {
test('should allow authorized access to envelope attachment delete endpoint', async ({ request }) => {
const doc = await seedBlankDocument(userA, teamA.id);
const attachment = await prisma.envelopeAttachment.create({
@@ -1,13 +1,12 @@
import { expect, test } from '@playwright/test';
import type { Team, User } from '@prisma/client';
import { EnvelopeType, FolderType } from '@prisma/client';
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 { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedBlankFolder } from '@documenso/prisma/seed/folders';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import type { Team, User } from '@prisma/client';
import { EnvelopeType, FolderType } from '@prisma/client';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
@@ -16,375 +15,366 @@ test.describe.configure({
});
// Todo: Remove skip once the API endpoints are released.
test.describe.skip('Envelope Bulk API V2', () => {
let userA: User, teamA: Team, userB: User, teamB: Team, tokenA: string, tokenB: string;
test.describe
.skip('Envelope Bulk API V2', () => {
let userA: User, teamA: Team, userB: User, teamB: Team, tokenA: string, tokenB: string;
test.beforeEach(async () => {
({ user: userA, team: teamA } = await seedUser());
({ token: tokenA } = await createApiToken({
userId: userA.id,
teamId: teamA.id,
tokenName: 'userA',
expiresIn: null,
}));
test.beforeEach(async () => {
({ user: userA, team: teamA } = await seedUser());
({ token: tokenA } = await createApiToken({
userId: userA.id,
teamId: teamA.id,
tokenName: 'userA',
expiresIn: null,
}));
({ user: userB, team: teamB } = await seedUser());
({ token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
}));
});
test.describe('Envelope bulk move endpoint', () => {
test('should block unauthorized access to envelope bulk move endpoint', async ({ request }) => {
// Create a document owned by userA
const doc = await seedBlankDocument(userA, teamA.id);
// UserB tries to move userA's document
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/move`, {
headers: { Authorization: `Bearer ${tokenB}` },
data: {
envelopeIds: [doc.id],
envelopeType: EnvelopeType.DOCUMENT,
folderId: null,
},
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.movedCount).toBe(0);
// Verify in database that the document was not modified
const docInDb = await prisma.envelope.findFirst({
where: { id: doc.id },
});
expect(docInDb).not.toBeNull();
expect(docInDb?.folderId).toBeNull();
({ user: userB, team: teamB } = await seedUser());
({ token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
}));
});
test('should block moving envelopes to unauthorized folder', async ({ request }) => {
// Create a document owned by userB
const doc = await seedBlankDocument(userB, teamB.id);
test.describe('Envelope bulk move endpoint', () => {
test('should block unauthorized access to envelope bulk move endpoint', async ({ request }) => {
// Create a document owned by userA
const doc = await seedBlankDocument(userA, teamA.id);
// Create a folder owned by userA
const folderA = await seedBlankFolder(userA, teamA.id, {
createFolderOptions: {
name: 'UserA Folder',
type: FolderType.DOCUMENT,
},
// UserB tries to move userA's document
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/move`, {
headers: { Authorization: `Bearer ${tokenB}` },
data: {
envelopeIds: [doc.id],
envelopeType: EnvelopeType.DOCUMENT,
folderId: null,
},
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.movedCount).toBe(0);
// Verify in database that the document was not modified
const docInDb = await prisma.envelope.findFirst({
where: { id: doc.id },
});
expect(docInDb).not.toBeNull();
expect(docInDb?.folderId).toBeNull();
});
// UserB tries to move their document to userA's folder
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/move`, {
headers: { Authorization: `Bearer ${tokenB}` },
data: {
envelopeIds: [doc.id],
envelopeType: EnvelopeType.DOCUMENT,
folderId: folderA.id,
},
test('should block moving envelopes to unauthorized folder', async ({ request }) => {
// Create a document owned by userB
const doc = await seedBlankDocument(userB, teamB.id);
// Create a folder owned by userA
const folderA = await seedBlankFolder(userA, teamA.id, {
createFolderOptions: {
name: 'UserA Folder',
type: FolderType.DOCUMENT,
},
});
// UserB tries to move their document to userA's folder
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/move`, {
headers: { Authorization: `Bearer ${tokenB}` },
data: {
envelopeIds: [doc.id],
envelopeType: EnvelopeType.DOCUMENT,
folderId: folderA.id,
},
});
expect(res.ok()).toBeFalsy();
expect(res.status()).toBe(404);
// Verify in database that the document was not modified
const docInDb = await prisma.envelope.findFirst({
where: { id: doc.id },
});
expect(docInDb).not.toBeNull();
expect(docInDb?.folderId).toBeNull();
});
expect(res.ok()).toBeFalsy();
expect(res.status()).toBe(404);
test('should allow authorized access to envelope bulk move endpoint', async ({ request }) => {
// Create a document owned by userA
const doc = await seedBlankDocument(userA, teamA.id);
// Verify in database that the document was not modified
const docInDb = await prisma.envelope.findFirst({
where: { id: doc.id },
// Create a folder owned by userA
const folderA = await seedBlankFolder(userA, teamA.id, {
createFolderOptions: {
name: 'UserA Folder',
type: FolderType.DOCUMENT,
},
});
// UserA moves their own document to their own folder
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/move`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: {
envelopeIds: [doc.id],
envelopeType: EnvelopeType.DOCUMENT,
folderId: folderA.id,
},
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.movedCount).toBe(1);
// Verify in database that the document was moved to the folder
const docInDb = await prisma.envelope.findFirst({
where: { id: doc.id },
});
expect(docInDb).not.toBeNull();
expect(docInDb?.folderId).toBe(folderA.id);
});
expect(docInDb).not.toBeNull();
expect(docInDb?.folderId).toBeNull();
test('should only move authorized envelopes when given mixed array of envelope IDs', async ({ request }) => {
// Create documents owned by userA
const docA1 = await seedBlankDocument(userA, teamA.id);
const docA2 = await seedBlankDocument(userA, teamA.id);
// Create a document owned by userB
const docB = await seedBlankDocument(userB, teamB.id);
// Create a folder owned by userA
const folderA = await seedBlankFolder(userA, teamA.id, {
createFolderOptions: {
name: 'UserA Folder',
type: FolderType.DOCUMENT,
},
});
// UserA tries to move a mix of their own documents and userB's document
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/move`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: {
envelopeIds: [docA1.id, docB.id, docA2.id],
envelopeType: EnvelopeType.DOCUMENT,
folderId: folderA.id,
},
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const body = await res.json();
// Only userA's documents should be moved
expect(body.movedCount).toBe(2);
// Verify userA's documents were moved
const docA1InDb = await prisma.envelope.findFirst({
where: { id: docA1.id },
});
expect(docA1InDb).not.toBeNull();
expect(docA1InDb?.folderId).toBe(folderA.id);
const docA2InDb = await prisma.envelope.findFirst({
where: { id: docA2.id },
});
expect(docA2InDb).not.toBeNull();
expect(docA2InDb?.folderId).toBe(folderA.id);
// Verify userB's document was NOT moved
const docBInDb = await prisma.envelope.findFirst({
where: { id: docB.id },
});
expect(docBInDb).not.toBeNull();
expect(docBInDb?.folderId).toBeNull();
});
test('should move zero envelopes when all envelope IDs in array are unauthorized', async ({ request }) => {
// Create documents owned by userB
const docB1 = await seedBlankDocument(userB, teamB.id);
const docB2 = await seedBlankDocument(userB, teamB.id);
// Create a folder owned by userA
const folderA = await seedBlankFolder(userA, teamA.id, {
createFolderOptions: {
name: 'UserA Folder',
type: FolderType.DOCUMENT,
},
});
// UserA tries to move userB's documents
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/move`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: {
envelopeIds: [docB1.id, docB2.id],
envelopeType: EnvelopeType.DOCUMENT,
folderId: folderA.id,
},
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.movedCount).toBe(0);
// Verify userB's documents were NOT moved
const docB1InDb = await prisma.envelope.findFirst({
where: { id: docB1.id },
});
expect(docB1InDb).not.toBeNull();
expect(docB1InDb?.folderId).toBeNull();
const docB2InDb = await prisma.envelope.findFirst({
where: { id: docB2.id },
});
expect(docB2InDb).not.toBeNull();
expect(docB2InDb?.folderId).toBeNull();
});
});
test('should allow authorized access to envelope bulk move endpoint', async ({ request }) => {
// Create a document owned by userA
const doc = await seedBlankDocument(userA, teamA.id);
test.describe('Envelope bulk delete endpoint', () => {
test('should block unauthorized access to envelope bulk delete endpoint', async ({ request }) => {
// Create a document owned by userA
const doc = await seedBlankDocument(userA, teamA.id);
// Create a folder owned by userA
const folderA = await seedBlankFolder(userA, teamA.id, {
createFolderOptions: {
name: 'UserA Folder',
type: FolderType.DOCUMENT,
},
// UserB tries to delete userA's document
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/delete`, {
headers: { Authorization: `Bearer ${tokenB}` },
data: {
envelopeIds: [doc.id],
},
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.deletedCount).toBe(0);
// Unauthorized envelope ID should be in failedIds
expect(body.failedIds).toEqual([doc.id]);
// Verify in database that the document still exists
const docInDb = await prisma.envelope.findFirst({
where: { id: doc.id },
});
expect(docInDb).not.toBeNull();
expect(docInDb?.id).toBe(doc.id);
expect(docInDb?.deletedAt).toBeNull();
});
// UserA moves their own document to their own folder
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/move`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: {
envelopeIds: [doc.id],
envelopeType: EnvelopeType.DOCUMENT,
folderId: folderA.id,
},
test('should allow authorized access to envelope bulk delete endpoint', async ({ request }) => {
// Create a document owned by userA
const doc = await seedBlankDocument(userA, teamA.id);
// UserA deletes their own document
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/delete`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: {
envelopeIds: [doc.id],
},
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.deletedCount).toBe(1);
expect(body.failedIds).toEqual([]);
// Verify in database that the document no longer exists
const docInDb = await prisma.envelope.findFirst({
where: { id: doc.id },
});
expect(docInDb).toBeNull();
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
test('should only delete authorized envelopes when given mixed array of envelope IDs', async ({ request }) => {
// Create documents owned by userA
const docA1 = await seedBlankDocument(userA, teamA.id);
const docA2 = await seedBlankDocument(userA, teamA.id);
const body = await res.json();
expect(body.movedCount).toBe(1);
// Create a document owned by userB
const docB = await seedBlankDocument(userB, teamB.id);
// Verify in database that the document was moved to the folder
const docInDb = await prisma.envelope.findFirst({
where: { id: doc.id },
// UserA tries to delete a mix of their own documents and userB's document
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/delete`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: {
envelopeIds: [docA1.id, docB.id, docA2.id],
},
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const body = await res.json();
// Only userA's documents should be deleted
expect(body.deletedCount).toBe(2);
// Unauthorized envelope ID (docB) should be in failedIds
expect(body.failedIds).toEqual([docB.id]);
// Verify userA's documents were deleted
const docA1InDb = await prisma.envelope.findFirst({
where: { id: docA1.id },
});
expect(docA1InDb).toBeNull();
const docA2InDb = await prisma.envelope.findFirst({
where: { id: docA2.id },
});
expect(docA2InDb).toBeNull();
// Verify userB's document was NOT deleted
const docBInDb = await prisma.envelope.findFirst({
where: { id: docB.id },
});
expect(docBInDb).not.toBeNull();
expect(docBInDb?.id).toBe(docB.id);
expect(docBInDb?.deletedAt).toBeNull();
});
expect(docInDb).not.toBeNull();
expect(docInDb?.folderId).toBe(folderA.id);
});
test('should delete zero envelopes when all envelope IDs in array are unauthorized', async ({ request }) => {
// Create documents owned by userB
const docB1 = await seedBlankDocument(userB, teamB.id);
const docB2 = await seedBlankDocument(userB, teamB.id);
test('should only move authorized envelopes when given mixed array of envelope IDs', async ({
request,
}) => {
// Create documents owned by userA
const docA1 = await seedBlankDocument(userA, teamA.id);
const docA2 = await seedBlankDocument(userA, teamA.id);
// UserA tries to delete userB's documents
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/delete`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: {
envelopeIds: [docB1.id, docB2.id],
},
});
// Create a document owned by userB
const docB = await seedBlankDocument(userB, teamB.id);
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
// Create a folder owned by userA
const folderA = await seedBlankFolder(userA, teamA.id, {
createFolderOptions: {
name: 'UserA Folder',
type: FolderType.DOCUMENT,
},
const body = await res.json();
expect(body.deletedCount).toBe(0);
// All unauthorized envelope IDs should be in failedIds
expect(body.failedIds).toEqual(expect.arrayContaining([docB1.id, docB2.id]));
expect(body.failedIds).toHaveLength(2);
// Verify userB's documents were NOT deleted
const docB1InDb = await prisma.envelope.findFirst({
where: { id: docB1.id },
});
expect(docB1InDb).not.toBeNull();
expect(docB1InDb?.id).toBe(docB1.id);
expect(docB1InDb?.deletedAt).toBeNull();
const docB2InDb = await prisma.envelope.findFirst({
where: { id: docB2.id },
});
expect(docB2InDb).not.toBeNull();
expect(docB2InDb?.id).toBe(docB2.id);
expect(docB2InDb?.deletedAt).toBeNull();
});
// UserA tries to move a mix of their own documents and userB's document
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/move`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: {
envelopeIds: [docA1.id, docB.id, docA2.id],
envelopeType: EnvelopeType.DOCUMENT,
folderId: folderA.id,
},
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const body = await res.json();
// Only userA's documents should be moved
expect(body.movedCount).toBe(2);
// Verify userA's documents were moved
const docA1InDb = await prisma.envelope.findFirst({
where: { id: docA1.id },
});
expect(docA1InDb).not.toBeNull();
expect(docA1InDb?.folderId).toBe(folderA.id);
const docA2InDb = await prisma.envelope.findFirst({
where: { id: docA2.id },
});
expect(docA2InDb).not.toBeNull();
expect(docA2InDb?.folderId).toBe(folderA.id);
// Verify userB's document was NOT moved
const docBInDb = await prisma.envelope.findFirst({
where: { id: docB.id },
});
expect(docBInDb).not.toBeNull();
expect(docBInDb?.folderId).toBeNull();
});
test('should move zero envelopes when all envelope IDs in array are unauthorized', async ({
request,
}) => {
// Create documents owned by userB
const docB1 = await seedBlankDocument(userB, teamB.id);
const docB2 = await seedBlankDocument(userB, teamB.id);
// Create a folder owned by userA
const folderA = await seedBlankFolder(userA, teamA.id, {
createFolderOptions: {
name: 'UserA Folder',
type: FolderType.DOCUMENT,
},
});
// UserA tries to move userB's documents
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/move`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: {
envelopeIds: [docB1.id, docB2.id],
envelopeType: EnvelopeType.DOCUMENT,
folderId: folderA.id,
},
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.movedCount).toBe(0);
// Verify userB's documents were NOT moved
const docB1InDb = await prisma.envelope.findFirst({
where: { id: docB1.id },
});
expect(docB1InDb).not.toBeNull();
expect(docB1InDb?.folderId).toBeNull();
const docB2InDb = await prisma.envelope.findFirst({
where: { id: docB2.id },
});
expect(docB2InDb).not.toBeNull();
expect(docB2InDb?.folderId).toBeNull();
});
});
test.describe('Envelope bulk delete endpoint', () => {
test('should block unauthorized access to envelope bulk delete endpoint', async ({
request,
}) => {
// Create a document owned by userA
const doc = await seedBlankDocument(userA, teamA.id);
// UserB tries to delete userA's document
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/delete`, {
headers: { Authorization: `Bearer ${tokenB}` },
data: {
envelopeIds: [doc.id],
},
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.deletedCount).toBe(0);
// Unauthorized envelope ID should be in failedIds
expect(body.failedIds).toEqual([doc.id]);
// Verify in database that the document still exists
const docInDb = await prisma.envelope.findFirst({
where: { id: doc.id },
});
expect(docInDb).not.toBeNull();
expect(docInDb?.id).toBe(doc.id);
expect(docInDb?.deletedAt).toBeNull();
});
test('should allow authorized access to envelope bulk delete endpoint', async ({ request }) => {
// Create a document owned by userA
const doc = await seedBlankDocument(userA, teamA.id);
// UserA deletes their own document
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/delete`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: {
envelopeIds: [doc.id],
},
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.deletedCount).toBe(1);
expect(body.failedIds).toEqual([]);
// Verify in database that the document no longer exists
const docInDb = await prisma.envelope.findFirst({
where: { id: doc.id },
});
expect(docInDb).toBeNull();
});
test('should only delete authorized envelopes when given mixed array of envelope IDs', async ({
request,
}) => {
// Create documents owned by userA
const docA1 = await seedBlankDocument(userA, teamA.id);
const docA2 = await seedBlankDocument(userA, teamA.id);
// Create a document owned by userB
const docB = await seedBlankDocument(userB, teamB.id);
// UserA tries to delete a mix of their own documents and userB's document
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/delete`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: {
envelopeIds: [docA1.id, docB.id, docA2.id],
},
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const body = await res.json();
// Only userA's documents should be deleted
expect(body.deletedCount).toBe(2);
// Unauthorized envelope ID (docB) should be in failedIds
expect(body.failedIds).toEqual([docB.id]);
// Verify userA's documents were deleted
const docA1InDb = await prisma.envelope.findFirst({
where: { id: docA1.id },
});
expect(docA1InDb).toBeNull();
const docA2InDb = await prisma.envelope.findFirst({
where: { id: docA2.id },
});
expect(docA2InDb).toBeNull();
// Verify userB's document was NOT deleted
const docBInDb = await prisma.envelope.findFirst({
where: { id: docB.id },
});
expect(docBInDb).not.toBeNull();
expect(docBInDb?.id).toBe(docB.id);
expect(docBInDb?.deletedAt).toBeNull();
});
test('should delete zero envelopes when all envelope IDs in array are unauthorized', async ({
request,
}) => {
// Create documents owned by userB
const docB1 = await seedBlankDocument(userB, teamB.id);
const docB2 = await seedBlankDocument(userB, teamB.id);
// UserA tries to delete userB's documents
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/bulk/delete`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: {
envelopeIds: [docB1.id, docB2.id],
},
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.deletedCount).toBe(0);
// All unauthorized envelope IDs should be in failedIds
expect(body.failedIds).toEqual(expect.arrayContaining([docB1.id, docB2.id]));
expect(body.failedIds).toHaveLength(2);
// Verify userB's documents were NOT deleted
const docB1InDb = await prisma.envelope.findFirst({
where: { id: docB1.id },
});
expect(docB1InDb).not.toBeNull();
expect(docB1InDb?.id).toBe(docB1.id);
expect(docB1InDb?.deletedAt).toBeNull();
const docB2InDb = await prisma.envelope.findFirst({
where: { id: docB2.id },
});
expect(docB2InDb).not.toBeNull();
expect(docB2InDb?.id).toBe(docB2.id);
expect(docB2InDb?.deletedAt).toBeNull();
});
});
});
@@ -1,8 +1,5 @@
import { type APIRequestContext, 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';
@@ -16,6 +13,8 @@ import type { TDistributeEnvelopeRequest } from '@documenso/trpc/server/envelope
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 type { TUpdateEnvelopeItemsRequest } from '@documenso/trpc/server/envelope-router/update-envelope-items.types';
import { type APIRequestContext, 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`;
@@ -64,11 +63,7 @@ const getEnvelope = async (request: APIRequestContext, authToken: string, envelo
* Transition an envelope from DRAFT to PENDING by adding a recipient with a
* signature field and distributing.
*/
const distributeEnvelope = async (
request: APIRequestContext,
authToken: string,
envelopeId: string,
) => {
const distributeEnvelope = async (request: APIRequestContext, authToken: string, envelopeId: string) => {
const recipientEmail = `signer-${Date.now()}@test.documenso.com`;
// Create a SIGNER recipient.
@@ -228,9 +223,7 @@ test.describe('Update envelope items', () => {
expect(dbItem.title).toBe('Updated Pending Title');
});
test('should allow title-only update when order matches existing on a PENDING envelope', async ({
request,
}) => {
test('should allow title-only update when order matches existing on a PENDING envelope', async ({ request }) => {
const envelope = await createEnvelope(request, token);
await distributeEnvelope(request, token, envelope.id);
@@ -318,9 +311,7 @@ test.describe('Update envelope items', () => {
expect(res.status()).toBe(400);
});
test('should reject combined title and order change on a PENDING envelope', async ({
request,
}) => {
test('should reject combined title and order change on a PENDING envelope', async ({ request }) => {
const envelope = await createEnvelope(request, token);
await distributeEnvelope(request, token, envelope.id);
@@ -499,9 +490,7 @@ test.describe('Update envelope items', () => {
expect(res.ok()).toBeFalsy();
});
test('should reject update for an envelope item that does not belong to the envelope', async ({
request,
}) => {
test('should reject update for an envelope item that does not belong to the envelope', async ({ request }) => {
const envelopeA = await createEnvelope(request, token);
const envelopeB = await createEnvelope(request, token);
@@ -1,23 +1,16 @@
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 { expect, type Page, test } from '@playwright/test';
import path from 'path';
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 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 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');
@@ -89,9 +82,7 @@ const uploadPdf = async (page: Page, team: { url: string }, pdfPath: string) =>
};
test.describe('PDF Placeholders with single recipient', () => {
test('[AUTO_PLACING_FIELDS]: should create placeholder recipients even with default recipients', async ({
page,
}) => {
test('[AUTO_PLACING_FIELDS]: should create placeholder recipients even with default recipients', async ({ page }) => {
const { user, team } = await seedUser();
await setTeamDefaultRecipients(team.id, [
@@ -115,9 +106,7 @@ test.describe('PDF Placeholders with single recipient', () => {
where: { envelopeId },
});
const placeholderRecipient = recipients.find(
(recipient) => recipient.email === 'recipient.1@documenso.com',
);
const placeholderRecipient = recipients.find((recipient) => recipient.email === 'recipient.1@documenso.com');
const defaultRecipient = recipients.find((recipient) => recipient.email === user.email);
@@ -133,23 +122,17 @@ test.describe('PDF Placeholders with single recipient', () => {
}).toPass();
});
test('[AUTO_PLACING_FIELDS]: should automatically create recipients from PDF placeholders', async ({
page,
}) => {
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.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,
}) => {
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);
@@ -164,9 +147,7 @@ test.describe('PDF Placeholders with single recipient', () => {
}).toPass();
});
test('[AUTO_PLACING_FIELDS]: should automatically configure fields from PDF placeholders', async ({
page,
}) => {
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);
@@ -187,26 +168,18 @@ test.describe('PDF Placeholders with single recipient', () => {
});
test.describe('PDF Placeholders with multiple recipients', () => {
test('[AUTO_PLACING_FIELDS]: should automatically create recipients from PDF placeholders', async ({
page,
}) => {
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').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(1)).toHaveValue('recipient.2@documenso.com');
await expect(page.getByTestId('signer-email-input').nth(2)).toHaveValue(
'recipient.3@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.
@@ -223,9 +196,7 @@ test.describe('PDF Placeholders with multiple recipients', () => {
}).toPass();
});
test('[AUTO_PLACING_FIELDS]: should automatically create fields from PDF placeholders', async ({
page,
}) => {
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);
@@ -244,9 +215,7 @@ test.describe('PDF Placeholders with multiple recipients', () => {
});
test.describe('PDF Placeholders without recipient identifier', () => {
test('[AUTO_PLACING_FIELDS]: should skip placeholders without a recipient identifier', async ({
page,
}) => {
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);
@@ -277,9 +246,7 @@ test.describe('PDF Placeholders without recipient identifier', () => {
});
test.describe('PDF Placeholders with invalid field types', () => {
test('[AUTO_PLACING_FIELDS]: should skip invalid field types and process valid ones', async ({
page,
}) => {
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);
@@ -1,7 +1,6 @@
import { expect, test } from '@playwright/test';
import { seedPendingDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -1,9 +1,8 @@
import { expect, test } from '@playwright/test';
import { createDocumentAuthOptions } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { seedPendingDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -1,17 +1,10 @@
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import { createDocumentAuthOptions, createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
import { seedPendingDocumentNoFields, seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { FieldType } from '@prisma/client';
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import {
createDocumentAuthOptions,
createRecipientAuthOptions,
} from '@documenso/lib/utils/document-auth';
import {
seedPendingDocumentNoFields,
seedPendingDocumentWithFullFields,
} from '@documenso/prisma/seed/documents';
import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users';
import { apiSignin, apiSignout } from '../fixtures/authentication';
import { signSignaturePad } from '../fixtures/signature';
@@ -106,9 +99,7 @@ test('[DOCUMENT_AUTH]: should allow signing with valid global auth', async ({ pa
});
// Currently document auth for signing/approving/viewing is not required.
test.skip('[DOCUMENT_AUTH]: should deny signing document when required for global auth', async ({
page,
}) => {
test.skip('[DOCUMENT_AUTH]: should deny signing document when required for global auth', async ({ page }) => {
const { user, team } = await seedUser();
const { user: recipientWithAccount } = await seedUser();
@@ -133,14 +124,10 @@ test.skip('[DOCUMENT_AUTH]: should deny signing document when required for globa
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
await page.getByRole('button', { name: 'Complete' }).click();
await expect(page.getByRole('paragraph')).toContainText(
'Reauthentication is required to sign the document',
);
await expect(page.getByRole('paragraph')).toContainText('Reauthentication is required to sign the document');
});
test('[DOCUMENT_AUTH]: should deny signing fields when required for global auth', async ({
page,
}) => {
test('[DOCUMENT_AUTH]: should deny signing fields when required for global auth', async ({ page }) => {
const { user, team } = await seedUser();
const { user: recipientWithAccount } = await seedUser();
@@ -170,17 +157,13 @@ test('[DOCUMENT_AUTH]: should deny signing fields when required for global auth'
}
await page.locator(`#field-${field.id}`).getByRole('button').click();
await expect(page.getByRole('paragraph')).toContainText(
'Reauthentication is required to sign this field',
);
await expect(page.getByRole('paragraph')).toContainText('Reauthentication is required to sign this field');
await page.getByRole('button', { name: 'Cancel' }).click();
}
}
});
test('[DOCUMENT_AUTH]: should allow field signing when required for recipient auth', async ({
page,
}) => {
test('[DOCUMENT_AUTH]: should allow field signing when required for recipient auth', async ({ page }) => {
const { user, team } = await seedUser();
const { user: recipientWithInheritAuth } = await seedUser();
@@ -190,11 +173,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
const { recipients } = await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: [
recipientWithInheritAuth,
recipientWithExplicitNoneAuth,
recipientWithExplicitAccountAuth,
],
recipients: [recipientWithInheritAuth, recipientWithExplicitNoneAuth, recipientWithExplicitAccountAuth],
recipientsCreateOptions: [
{
authOptions: createRecipientAuthOptions({
@@ -237,9 +216,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
}
await page.locator(`#field-${field.id}`).getByRole('button').click();
await expect(page.getByRole('paragraph')).toContainText(
'Reauthentication is required to sign this field',
);
await expect(page.getByRole('paragraph')).toContainText('Reauthentication is required to sign this field');
await page.getByRole('button', { name: 'Cancel' }).click();
}
@@ -278,9 +255,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
}
});
test('[DOCUMENT_AUTH]: should allow field signing when required for recipient and global auth', async ({
page,
}) => {
test('[DOCUMENT_AUTH]: should allow field signing when required for recipient and global auth', async ({ page }) => {
const { user, team } = await seedUser();
const { user: recipientWithInheritAuth } = await seedUser();
@@ -290,11 +265,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
const { recipients } = await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: [
recipientWithInheritAuth,
recipientWithExplicitNoneAuth,
recipientWithExplicitAccountAuth,
],
recipients: [recipientWithInheritAuth, recipientWithExplicitNoneAuth, recipientWithExplicitAccountAuth],
recipientsCreateOptions: [
{
authOptions: createRecipientAuthOptions({
@@ -343,9 +314,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
}
await page.locator(`#field-${field.id}`).getByRole('button').click();
await expect(page.getByRole('paragraph')).toContainText(
'Reauthentication is required to sign this field',
);
await expect(page.getByRole('paragraph')).toContainText('Reauthentication is required to sign this field');
await page.getByRole('button', { name: 'Cancel' }).click();
}
@@ -1,8 +1,7 @@
import { type APIRequestContext, expect, test } from '@playwright/test';
import { FieldType, SigningStatus } from '@prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { prisma } from '@documenso/prisma';
import { type APIRequestContext, expect, test } from '@playwright/test';
import { FieldType, SigningStatus } from '@prisma/client';
import { apiSeedPendingDocument } from '../fixtures/api-seeds';
@@ -89,11 +88,7 @@ const seedTwoPendingEnvelopes = async (request: APIRequestContext): Promise<Seed
};
};
const trpcMutation = async (
request: APIRequestContext,
procedure: string,
input: Record<string, unknown>,
) => {
const trpcMutation = async (request: APIRequestContext, procedure: string, input: Record<string, unknown>) => {
return await request.post(`${WEBAPP_BASE_URL}/api/trpc/${procedure}`, {
headers: { 'content-type': 'application/json' },
data: JSON.stringify({ json: input }),
@@ -120,9 +115,7 @@ test.describe('[ASSISTANT_SIGNING_AUTH]: cross-envelope field access', () => {
expect(fieldAfter.customText).toBe('');
});
test('field.signFieldWithToken (V1) rejects fieldId from another envelope', async ({
request,
}) => {
test('field.signFieldWithToken (V1) rejects fieldId from another envelope', async ({ request }) => {
const { assistantToken, otherEnvelopeFieldId } = await seedTwoPendingEnvelopes(request);
const res = await trpcMutation(request, 'field.signFieldWithToken', {
@@ -142,9 +135,7 @@ test.describe('[ASSISTANT_SIGNING_AUTH]: cross-envelope field access', () => {
expect(fieldAfter.customText).toBe('');
});
test('field.removeSignedFieldWithToken (V1) rejects fieldId from another envelope', async ({
request,
}) => {
test('field.removeSignedFieldWithToken (V1) rejects fieldId from another envelope', async ({ request }) => {
const { assistantToken, otherEnvelopeFieldId } = await seedTwoPendingEnvelopes(request);
// Pre-insert the field so a successful (incorrect) uninsert is detectable.
@@ -1,21 +1,12 @@
import { expect, test } from '@playwright/test';
import {
DocumentSigningOrder,
DocumentStatus,
FieldType,
RecipientRole,
SigningStatus,
} from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { DocumentSigningOrder, DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
import { signDirectSignaturePad, signSignaturePad } from '../fixtures/signature';
test('[NEXT_RECIPIENT_DICTATION]: should allow updating next recipient when dictation is enabled', async ({
page,
}) => {
test('[NEXT_RECIPIENT_DICTATION]: should allow updating next recipient when dictation is enabled', async ({ page }) => {
const { user, team } = await seedUser();
const { user: firstSigner } = await seedUser();
const { user: secondSigner } = await seedUser();
@@ -157,9 +148,7 @@ test('[NEXT_RECIPIENT_DICTATION]: should not show dictation UI when disabled', a
await page.getByRole('button', { name: 'Complete' }).click();
// Verify next recipient UI is not shown
await expect(
page.getByText('The next recipient to sign this document will be'),
).not.toBeVisible();
await expect(page.getByText('The next recipient to sign this document will be')).not.toBeVisible();
await expect(page.getByRole('button', { name: 'Update Recipient' })).not.toBeVisible();
// Submit and verify completion
@@ -243,9 +232,7 @@ test('[NEXT_RECIPIENT_DICTATION]: should work with parallel signing flow', async
await page.getByRole('button', { name: 'Complete' }).click();
// Verify next recipient UI is not shown in parallel flow
await expect(
page.getByText('The next recipient to sign this document will be'),
).not.toBeVisible();
await expect(page.getByText('The next recipient to sign this document will be')).not.toBeVisible();
await expect(page.getByRole('button', { name: 'Update Recipient' })).not.toBeVisible();
// Submit and verify completion
@@ -274,9 +261,7 @@ test('[NEXT_RECIPIENT_DICTATION]: should work with parallel signing flow', async
}).toPass();
});
test('[NEXT_RECIPIENT_DICTATION]: should allow assistant to dictate next signer', async ({
page,
}) => {
test('[NEXT_RECIPIENT_DICTATION]: should allow assistant to dictate next signer', async ({ page }) => {
const { user, team } = await seedUser();
const { user: assistant } = await seedUser();
const { user: signer } = await seedUser();
@@ -1,10 +1,9 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { prisma } from '@documenso/prisma';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -64,10 +63,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
await page
.getByTestId('field-advanced-settings-footer')
.getByRole('button', { name: 'Cancel' })
.click();
await page.getByTestId('field-advanced-settings-footer').getByRole('button', { name: 'Cancel' }).click();
await triggerAutosave(page);
@@ -118,10 +114,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
await page
.getByTestId('field-advanced-settings-footer')
.getByRole('button', { name: 'Cancel' })
.click();
await page.getByTestId('field-advanced-settings-footer').getByRole('button', { name: 'Cancel' }).click();
await triggerAutosave(page);
@@ -180,10 +173,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
await page
.getByTestId('field-advanced-settings-footer')
.getByRole('button', { name: 'Cancel' })
.click();
await page.getByTestId('field-advanced-settings-footer').getByRole('button', { name: 'Cancel' }).click();
await triggerAutosave(page);
@@ -244,10 +234,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByRole('textbox', { name: 'Field placeholder' }).fill('Test Placeholder');
await page.getByRole('textbox', { name: 'Add text to the field' }).fill('Test Text');
await page
.getByTestId('field-advanced-settings-footer')
.getByRole('button', { name: 'Save' })
.click();
await page.getByTestId('field-advanced-settings-footer').getByRole('button', { name: 'Save' }).click();
await triggerAutosave(page);
@@ -269,11 +256,7 @@ test.describe('AutoSave Fields Step', () => {
expect(textField.fieldMeta).toBeDefined();
if (
textField.fieldMeta &&
typeof textField.fieldMeta === 'object' &&
'type' in textField.fieldMeta
) {
if (textField.fieldMeta && typeof textField.fieldMeta === 'object' && 'type' in textField.fieldMeta) {
expect(textField.fieldMeta.type).toBe('text');
expect(textField.fieldMeta.label).toBe('Test Field');
expect(textField.fieldMeta.placeholder).toBe('Test Placeholder');
@@ -1,10 +1,9 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { EnvelopeType } from '@prisma/client';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { EnvelopeType } from '@prisma/client';
import { apiSignin } from '../fixtures/authentication';
@@ -1,12 +1,11 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { EnvelopeType } from '@prisma/client';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { EnvelopeType } from '@prisma/client';
import { apiSignin } from '../fixtures/authentication';
@@ -167,15 +166,9 @@ test.describe('AutoSave Signers Step', () => {
expect(retrievedDocumentData.documentMeta?.allowDictateNextSigner).toBe(true);
expect(retrievedRecipients.length).toBe(3);
const firstRecipient = retrievedRecipients.find(
(r) => r.email === 'recipient1@documenso.com',
);
const secondRecipient = retrievedRecipients.find(
(r) => r.email === 'recipient2@documenso.com',
);
const thirdRecipient = retrievedRecipients.find(
(r) => r.email === 'recipient3@documenso.com',
);
const firstRecipient = retrievedRecipients.find((r) => r.email === 'recipient1@documenso.com');
const secondRecipient = retrievedRecipients.find((r) => r.email === 'recipient2@documenso.com');
const thirdRecipient = retrievedRecipients.find((r) => r.email === 'recipient3@documenso.com');
expect(firstRecipient?.signingOrder).toBe(2);
expect(secondRecipient?.signingOrder).toBe(3);
@@ -1,11 +1,10 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { EnvelopeType } from '@prisma/client';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { EnvelopeType } from '@prisma/client';
import { apiSignin } from '../fixtures/authentication';
@@ -109,9 +108,7 @@ test.describe('AutoSave Subject Step', () => {
// Toggle some email settings checkboxes (randomly - some checked, some unchecked)
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 the document is completed', { exact: true }).click();
await page.getByText('Email recipients when a pending document is deleted').click();
await triggerAutosave(page);
@@ -132,30 +129,22 @@ test.describe('AutoSave Subject Step', () => {
await expect(page.getByText('Email the owner when a recipient signs')).toBeChecked({
checked: emailSettings?.recipientSigned,
});
await expect(
page.getByText("Email recipients when they're removed from a pending document"),
).toBeChecked({
await expect(page.getByText("Email recipients when they're removed from a pending document")).toBeChecked({
checked: emailSettings?.recipientRemoved,
});
await expect(
page.getByText('Email recipients when the document is completed', { exact: true }),
).toBeChecked({
await expect(page.getByText('Email recipients when the document is completed', { exact: true })).toBeChecked({
checked: emailSettings?.documentCompleted,
});
await expect(
page.getByText('Email recipients when a pending document is deleted'),
).toBeChecked({
await expect(page.getByText('Email recipients when a pending document is deleted')).toBeChecked({
checked: emailSettings?.documentDeleted,
});
await expect(page.getByText('Email recipients with a signing request')).toBeChecked({
checked: emailSettings?.recipientSigningRequest,
});
await expect(page.getByText('Email the signer if the document is still pending')).toBeChecked(
{
checked: emailSettings?.documentPending,
},
);
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,
});
@@ -174,9 +163,7 @@ test.describe('AutoSave Subject Step', () => {
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 the document is completed', { exact: true }).click();
await page.getByText('Email recipients when a pending document is deleted').click();
await triggerAutosave(page);
@@ -206,30 +193,22 @@ test.describe('AutoSave Subject Step', () => {
await expect(page.getByText('Email the owner when a recipient signs')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientSigned,
});
await expect(
page.getByText("Email recipients when they're removed from a pending document"),
).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('Email recipients when the document is completed', { 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('Email recipients when a pending document is deleted'),
).toBeChecked({
await expect(page.getByText('Email recipients when a pending document is deleted')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentDeleted,
});
await expect(page.getByText('Email recipients with a signing request')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientSigningRequest,
});
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 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,
});
@@ -1,8 +1,7 @@
import { expect, test } from '@playwright/test';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -1,10 +1,9 @@
import { type Page, expect, test } from '@playwright/test';
import type { Document, Team } from '@prisma/client';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { prisma } from '@documenso/prisma';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, type Page, test } from '@playwright/test';
import type { Document, Team } from '@prisma/client';
import { apiSignin } from '../fixtures/authentication';
import { signSignaturePad } from '../fixtures/signature';
@@ -12,11 +11,7 @@ import { signSignaturePad } from '../fixtures/signature';
/**
* Test helper to complete the document creation flow with duplicate recipients
*/
const completeDocumentFlowWithDuplicateRecipients = async (options: {
page: Page;
team: Team;
document: Document;
}) => {
const completeDocumentFlowWithDuplicateRecipients = async (options: { page: Page; team: Team; document: Document }) => {
const { page, team, document } = options;
// Step 1: Settings - Continue with defaults
@@ -97,9 +92,7 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
await expect(page).toHaveURL(new RegExp(`/t/${team.url}/documents`));
});
test('should allow adding duplicate recipient after saving document initially', async ({
page,
}) => {
test('should allow adding duplicate recipient after saving document initially', async ({ page }) => {
const { user, team } = await seedUser();
const document = await seedBlankDocument(user, team.id);
@@ -162,10 +155,7 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
await expect(page.getByRole('link', { name: document.title })).toBeVisible();
});
test('should isolate fields per recipient token even with duplicate emails', async ({
page,
context,
}) => {
test('should isolate fields per recipient token even with duplicate emails', async ({ page, context }) => {
const { user, team } = await seedUser();
const document = await seedBlankDocument(user, team.id);
@@ -208,9 +198,7 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
await page.waitForSelector(PDF_VIEWER_PAGE_SELECTOR);
// Verify only one signature field is visible for this recipient
expect(
await page.locator(`[data-field-type="SIGNATURE"]:not([data-readonly="true"])`).all(),
).toHaveLength(1);
expect(await page.locator(`[data-field-type="SIGNATURE"]:not([data-readonly="true"])`).all()).toHaveLength(1);
// Verify recipient name is correct
await expect(page.getByLabel('Full Name')).toHaveValue(recipient.name);
@@ -218,10 +206,7 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
// Sign the document
await signSignaturePad(page);
await page
.locator('[data-field-type="SIGNATURE"]:not([data-readonly="true"])')
.first()
.click();
await page.locator('[data-field-type="SIGNATURE"]:not([data-readonly="true"])').first().click();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
@@ -232,9 +217,7 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
}
});
test('should handle duplicate recipient workflow with different field types', async ({
page,
}) => {
test('should handle duplicate recipient workflow with different field types', async ({ page }) => {
const { user, team } = await seedUser();
const document = await seedBlankDocument(user, team.id);
@@ -300,9 +283,7 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
await expect(page.getByRole('link', { name: document.title })).toBeVisible();
});
test('should preserve field assignments when editing document with duplicates', async ({
page,
}) => {
test('should preserve field assignments when editing document with duplicates', async ({ page }) => {
const { user, team } = await seedUser();
const document = await seedBlankDocument(user, team.id);
@@ -333,18 +314,14 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
await page.getByText('Duplicate Recipient 1 (duplicate@example.com)').click();
// Verify their field is visible and can be selected
const firstRecipientFields = await page
.locator(`[data-field-type="SIGNATURE"]:not(:disabled)`)
.all();
const firstRecipientFields = await page.locator(`[data-field-type="SIGNATURE"]:not(:disabled)`).all();
expect(firstRecipientFields.length).toBeGreaterThan(0);
// Switch to second duplicate recipient
await page.getByText('Duplicate Recipient 2 (duplicate@example.com)').click();
// Verify they have their own field
const secondRecipientFields = await page
.locator(`[data-field-type="SIGNATURE"]:not(:disabled)`)
.all();
const secondRecipientFields = await page.locator(`[data-field-type="SIGNATURE"]:not(:disabled)`).all();
expect(secondRecipientFields.length).toBeGreaterThan(0);
// Add another field to the second duplicate
@@ -1,11 +1,6 @@
import { expect, test } from '@playwright/test';
import {
seedBlankDocument,
seedDraftDocument,
seedPendingDocument,
} from '@documenso/prisma/seed/documents';
import { seedBlankDocument, seedDraftDocument, seedPendingDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -1,7 +1,6 @@
import { expect, test } from '@playwright/test';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -1,21 +1,11 @@
import { expect, test } from '@playwright/test';
import {
DocumentSigningOrder,
DocumentStatus,
FieldType,
RecipientRole,
SigningStatus,
} from '@prisma/client';
import { DateTime } from 'luxon';
import path from 'node:path';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { prisma } from '@documenso/prisma';
import {
seedBlankDocument,
seedPendingDocumentWithFullFields,
} from '@documenso/prisma/seed/documents';
import { seedBlankDocument, seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { DocumentSigningOrder, DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
import { DateTime } from 'luxon';
import { apiSignin } from '../fixtures/authentication';
import { signSignaturePad } from '../fixtures/signature';
@@ -121,9 +111,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document', async ({ page }) =>
await expect(page.getByRole('link', { name: documentTitle })).toBeVisible();
});
test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipients', async ({
page,
}) => {
test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipients', async ({ page }) => {
const { user, team } = await seedUser();
const document = await seedBlankDocument(user, team.id);
@@ -304,9 +292,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
await expect(page.getByRole('link', { name: 'Test Title' })).toBeVisible();
});
test('[DOCUMENT_FLOW]: should not be able to create a document without signatures', async ({
page,
}) => {
test('[DOCUMENT_FLOW]: should not be able to create a document without signatures', async ({ page }) => {
const { user, team } = await seedUser();
const document = await seedBlankDocument(user, team.id);
@@ -337,9 +323,7 @@ test('[DOCUMENT_FLOW]: should not be able to create a document without signature
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Continue' }).click();
await expect(
page.getByRole('dialog').getByText('No signature field found').first(),
).toBeVisible();
await expect(page.getByRole('dialog').getByText('No signature field found').first()).toBeVisible();
});
test('[DOCUMENT_FLOW]: should be able to approve a document', async ({ page }) => {
@@ -382,12 +366,8 @@ test('[DOCUMENT_FLOW]: should be able to approve a document', async ({ page }) =
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
}
await page
.getByRole('button', { name: role === RecipientRole.SIGNER ? 'Complete' : 'Approve' })
.click();
await page
.getByRole('button', { name: role === RecipientRole.SIGNER ? 'Sign' : 'Approve' })
.click();
await page.getByRole('button', { name: role === RecipientRole.SIGNER ? 'Complete' : 'Approve' }).click();
await page.getByRole('button', { name: role === RecipientRole.SIGNER ? 'Sign' : 'Approve' }).click();
await page.waitForURL(`${signUrl}/complete`);
}
});
@@ -460,10 +440,7 @@ test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a
await page.getByRole('button', { name: 'Approve' }).click();
await expect(
page
.getByRole('dialog')
.getByText('You are about to complete approving the following document')
.first(),
page.getByRole('dialog').getByText('You are about to complete approving the following document').first(),
).toBeVisible();
await page.getByRole('button', { name: 'Approve' }).click();
@@ -606,9 +583,7 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
expect(createdDocument?.recipients.length).toBe(3);
for (let i = 0; i < 3; i++) {
const recipient = createdDocument?.recipients.find(
(r) => r.email === `user${i + 1}@example.com`,
);
const recipient = createdDocument?.recipients.find((r) => r.email === `user${i + 1}@example.com`);
expect(recipient).not.toBeNull();
const fields = await prisma.field.findMany({
@@ -653,9 +628,7 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
expect(finalDocument?.status).toBe(DocumentStatus.COMPLETED);
});
test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode', async ({
page,
}) => {
test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode', async ({ page }) => {
const { user, team } = await seedUser();
const { document, recipients } = await seedPendingDocumentWithFullFields({
@@ -1,8 +1,7 @@
import { expect, test } from '@playwright/test';
import { seedDraftDocument } from '@documenso/prisma/seed/documents';
import { seedBlankFolder } from '@documenso/prisma/seed/folders';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
import { expectToastTextToBeVisible } from '../fixtures/generic';
@@ -1,11 +1,6 @@
import { expect, test } from '@playwright/test';
import {
seedCompletedDocument,
seedDraftDocument,
seedPendingDocument,
} from '@documenso/prisma/seed/documents';
import { seedCompletedDocument, seedDraftDocument, seedPendingDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin, apiSignout } from '../fixtures/authentication';
import { checkDocumentTabCount } from '../fixtures/documents';
@@ -72,9 +67,7 @@ test('[DOCUMENTS]: seeded documents should be visible', async ({ page }) => {
}
});
test('[DOCUMENTS]: deleting a completed document should not remove it from recipients', async ({
page,
}) => {
test('[DOCUMENTS]: deleting a completed document should not remove it from recipients', async ({ page }) => {
const { sender, recipients } = await seedDeleteDocumentsTestRequirements();
await apiSignin({
@@ -116,9 +109,7 @@ test('[DOCUMENTS]: deleting a completed document should not remove it from recip
}
});
test('[DOCUMENTS]: deleting a pending document should remove it from recipients', async ({
page,
}) => {
test('[DOCUMENTS]: deleting a pending document should remove it from recipients', async ({ page }) => {
const { sender, pendingDocument } = await seedDeleteDocumentsTestRequirements();
await apiSignin({
@@ -223,9 +214,7 @@ test('[DOCUMENTS]: deleting pending documents should permanently remove it', asy
await checkDocumentTabCount(page, 'All', 2);
});
test('[DOCUMENTS]: deleting completed documents as an owner should hide it from only the owner', async ({
page,
}) => {
test('[DOCUMENTS]: deleting completed documents as an owner should hide it from only the owner', async ({ page }) => {
const { sender, recipients } = await seedDeleteDocumentsTestRequirements();
await apiSignin({
@@ -273,9 +262,7 @@ test('[DOCUMENTS]: deleting completed documents as an owner should hide it from
await checkDocumentTabCount(page, 'All', 2);
});
test('[DOCUMENTS]: deleting documents as a recipient should only hide it for them', async ({
page,
}) => {
test('[DOCUMENTS]: deleting documents as a recipient should only hide it for them', async ({ page }) => {
const { sender, recipients } = await seedDeleteDocumentsTestRequirements();
const recipientA = recipients[0];
const recipientB = recipients[1];
@@ -1,11 +1,3 @@
import { expect, test } from '@playwright/test';
import {
DocumentStatus,
DocumentVisibility,
OrganisationMemberRole,
TeamMemberRole,
} from '@prisma/client';
import { generateDatabaseId } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import {
@@ -17,6 +9,8 @@ import {
import { seedOrganisationMembers } from '@documenso/prisma/seed/organisations';
import { seedTeam, seedTeamEmail, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { DocumentStatus, DocumentVisibility, OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
import { apiSignin, apiSignout } from '../fixtures/authentication';
import { checkDocumentTabCount } from '../fixtures/documents';
@@ -66,9 +60,7 @@ test.describe('Find Documents UI - Personal Context', () => {
await checkDocumentTabCount(page, 'Completed', 1);
});
test('received documents from other teams should NOT appear in personal context', async ({
page,
}) => {
test('received documents from other teams should NOT appear in personal context', async ({ page }) => {
// The UI always uses the team code path (findTeamDocumentsFilter) which filters by teamId.
// Documents sent TO a user by another user's team live on the sender's teamId,
// so they do NOT appear in the recipient's personal team context.
@@ -97,12 +89,8 @@ test.describe('Find Documents UI - Personal Context', () => {
// Only the owner's own doc should appear (received docs are on sender's team)
await checkDocumentTabCount(page, 'All', 1);
await expect(page.getByRole('link', { name: 'Owner Own Draft' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'Received Pending Doc', exact: true }),
).not.toBeVisible();
await expect(
page.getByRole('link', { name: 'Received Completed Doc', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Received Pending Doc', exact: true })).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Received Completed Doc', exact: true })).not.toBeVisible();
});
test('should NOT show documents from other users', async ({ page }) => {
@@ -120,9 +108,7 @@ test.describe('Find Documents UI - Personal Context', () => {
});
await checkDocumentTabCount(page, 'All', 0);
await expect(
page.getByRole('link', { name: 'UserB Secret Document', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'UserB Secret Document', exact: true })).not.toBeVisible();
});
test('personal context without team email should show 0 inbox', async ({ page }) => {
@@ -175,9 +161,7 @@ test.describe('Find Documents UI - Personal Context', () => {
await checkDocumentTabCount(page, 'All', 1);
await expect(page.getByRole('link', { name: 'Quarterly Report 2024' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'Annual Budget Plan', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Annual Budget Plan', exact: true })).not.toBeVisible();
});
test('should not show deleted documents', async ({ page }) => {
@@ -203,9 +187,7 @@ test.describe('Find Documents UI - Personal Context', () => {
await checkDocumentTabCount(page, 'All', 1);
await expect(page.getByRole('link', { name: 'Active Personal Doc' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'Deleted Personal Doc', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Deleted Personal Doc', exact: true })).not.toBeVisible();
});
test('should only show root-level documents when not in a folder', async ({ page }) => {
@@ -235,9 +217,7 @@ test.describe('Find Documents UI - Personal Context', () => {
await checkDocumentTabCount(page, 'All', 1);
await expect(page.getByRole('link', { name: 'Root Level Doc' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'Folder Level Doc', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Folder Level Doc', exact: true })).not.toBeVisible();
});
});
@@ -290,9 +270,7 @@ test.describe('Find Documents UI - Team Context', () => {
await expect(page.getByRole('link', { name: 'Team Pending Doc' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Team Draft Doc' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Team Completed Doc' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'Outside Noise Doc', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Outside Noise Doc', exact: true })).not.toBeVisible();
await apiSignout({ page });
}
@@ -346,9 +324,7 @@ test.describe('Find Documents UI - Team Context', () => {
await expect(page.getByRole('link', { name: 'Team A Draft' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Team A Completed' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Team B Draft', exact: true })).not.toBeVisible();
await expect(
page.getByRole('link', { name: 'Team B Completed', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Team B Completed', exact: true })).not.toBeVisible();
});
test('should NOT show personal documents in team context', async ({ page }) => {
@@ -383,9 +359,7 @@ test.describe('Find Documents UI - Team Context', () => {
});
await expect(page.getByRole('link', { name: 'Team Doc by Member' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'Personal Doc not in Team', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Personal Doc not in Team', exact: true })).not.toBeVisible();
});
test('should enforce ADMIN visibility correctly across roles', async ({ page }) => {
@@ -478,16 +452,12 @@ test.describe('Find Documents UI - Team Context', () => {
});
await checkDocumentTabCount(page, 'Completed', 1);
await expect(page.getByRole('link', { name: 'Everyone Doc' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'Manager Plus Doc', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Manager Plus Doc', exact: true })).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Admin Only Doc', exact: true })).not.toBeVisible();
await apiSignout({ page });
});
test('document owner sees their document regardless of visibility restriction', async ({
page,
}) => {
test('document owner sees their document regardless of visibility restriction', async ({ page }) => {
const { team, owner } = await seedTeam();
const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER });
@@ -538,9 +508,7 @@ test.describe('Find Documents UI - Team Context', () => {
await checkDocumentTabCount(page, 'Completed', 2);
await expect(page.getByRole('link', { name: 'Member Owned Admin Doc' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Everyone Doc Control' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'Owner Admin Doc Hidden', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Owner Admin Doc Hidden', exact: true })).not.toBeVisible();
await apiSignout({ page });
});
@@ -596,9 +564,7 @@ test.describe('Find Documents UI - Team Context', () => {
await checkDocumentTabCount(page, 'Completed', 2);
await expect(page.getByRole('link', { name: 'Admin Doc Member Recipient' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Everyone Doc Baseline' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'Admin Doc No Member', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Admin Doc No Member', exact: true })).not.toBeVisible();
await apiSignout({ page });
});
@@ -785,9 +751,7 @@ test.describe('Find Documents UI - Team with Team Email', () => {
await checkDocumentTabCount(page, 'All', 2);
await expect(page.getByRole('link', { name: 'Sent by Holder Pending' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Sent by Holder Completed' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'External Own Draft', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'External Own Draft', exact: true })).not.toBeVisible();
});
});
@@ -846,15 +810,9 @@ test.describe('Find Documents UI - Data Isolation & No Leaking', () => {
// Verify no B docs leaked
await page.getByRole('tab', { name: 'All' }).click();
await expect(page.getByRole('link', { name: 'A Own Draft' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'B Draft Private', exact: true }),
).not.toBeVisible();
await expect(
page.getByRole('link', { name: 'B Pending Private', exact: true }),
).not.toBeVisible();
await expect(
page.getByRole('link', { name: 'B Completed Private', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'B Draft Private', exact: true })).not.toBeVisible();
await expect(page.getByRole('link', { name: 'B Pending Private', exact: true })).not.toBeVisible();
await expect(page.getByRole('link', { name: 'B Completed Private', exact: true })).not.toBeVisible();
});
test('team member cannot see documents from another team via search', async ({ page }) => {
@@ -903,9 +861,7 @@ test.describe('Find Documents UI - Data Isolation & No Leaking', () => {
// Should find the TeamA doc but NOT the TeamB doc
await checkDocumentTabCount(page, 'All', 1);
await expect(page.getByRole('link', { name: 'Super Secret TeamA Document' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'Super Secret TeamB Document', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Super Secret TeamB Document', exact: true })).not.toBeVisible();
});
test('search by recipient name should respect team visibility', async ({ page }) => {
@@ -957,9 +913,7 @@ test.describe('Find Documents UI - Data Isolation & No Leaking', () => {
await apiSignout({ page });
});
test('outside user does NOT see cross-team received docs in their personal context', async ({
page,
}) => {
test('outside user does NOT see cross-team received docs in their personal context', async ({ page }) => {
// The UI always uses the team code path (findTeamDocumentsFilter) which filters by teamId.
// Documents from team.id will NOT appear in outsideTeam's context.
const { team, owner } = await seedTeam();
@@ -1009,12 +963,8 @@ test.describe('Find Documents UI - Data Isolation & No Leaking', () => {
await checkDocumentTabCount(page, 'Inbox', 0); // No team email → 0
await checkDocumentTabCount(page, 'All', 1); // Check All tab last so we can verify visible links
await expect(page.getByRole('link', { name: 'Outside Own Draft' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'Team Doc For Outside User', exact: true }),
).not.toBeVisible();
await expect(
page.getByRole('link', { name: 'Team Doc For Other User Only', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Team Doc For Outside User', exact: true })).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Team Doc For Other User Only', exact: true })).not.toBeVisible();
});
});
@@ -1,12 +1,7 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import {
seedBlankDocument,
seedCompletedDocument,
seedPendingDocument,
} from '@documenso/prisma/seed/documents';
import { seedBlankDocument, seedCompletedDocument, seedPendingDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -1,8 +1,5 @@
import { type Page, expect, test } from '@playwright/test';
import { DocumentStatus, EnvelopeType, FieldType, RecipientRole } 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';
@@ -15,6 +12,8 @@ import type {
import type { TDistributeEnvelopeRequest } from '@documenso/trpc/server/envelope-router/distribute-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 { expect, type Page, test } from '@playwright/test';
import { DocumentStatus, EnvelopeType, FieldType, RecipientRole } from '@prisma/client';
import { apiSignin } from '../fixtures/authentication';
import {
@@ -34,11 +33,7 @@ const examplePdfBuffer = fs.readFileSync(path.join(__dirname, '../../../../asset
/**
* Place a field on the PDF canvas in the envelope editor.
*/
const placeFieldOnPdf = async (
root: Page,
fieldName: 'Signature' | 'Text',
position: { x: number; y: number },
) => {
const placeFieldOnPdf = async (root: Page, fieldName: 'Signature' | 'Text', position: { x: number; y: number }) => {
await root.getByRole('button', { name: fieldName, exact: true }).click();
const canvas = root.locator('.konva-container canvas').first();
@@ -72,10 +67,7 @@ const createPendingEnvelopeViaApi = async () => {
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append(
'files',
new File([examplePdfBuffer], 'example.pdf', { type: 'application/pdf' }),
);
formData.append('files', new File([examplePdfBuffer], 'example.pdf', { type: 'application/pdf' }));
const createRes = await fetch(`${V2_API_BASE_URL}/envelope/create`, {
method: 'POST',
@@ -224,9 +216,7 @@ test.describe('document editor', () => {
await expect(page.getByRole('heading', { name: 'Send Document' })).toBeVisible();
// The validation warning should be shown instead of the send form.
await expect(
page.getByText('The following signers are missing signature fields'),
).toBeVisible();
await expect(page.getByText('The following signers are missing signature fields')).toBeVisible();
// The "Send" button should not be visible since we're in validation mode.
await expect(page.getByRole('button', { name: 'Send', exact: true })).not.toBeVisible();
@@ -235,9 +225,7 @@ test.describe('document editor', () => {
await page.getByRole('button', { name: 'Close' }).click();
// The dialog should be closed.
await expect(
page.getByText('The following signers are missing signature fields'),
).not.toBeVisible();
await expect(page.getByText('The following signers are missing signature fields')).not.toBeVisible();
});
test('resend document sends reminder', async ({ page }) => {
@@ -1,16 +1,15 @@
import { type Page, expect, test } from '@playwright/test';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { expect, type Page, test } from '@playwright/test';
import {
type TEnvelopeEditorSurface,
addEnvelopeItemPdf,
getEnvelopeEditorSettingsTrigger,
openDocumentEnvelopeEditor,
openEmbeddedEnvelopeEditor,
openTemplateEnvelopeEditor,
persistEmbeddedEnvelope,
type TEnvelopeEditorSurface,
} from '../fixtures/envelope-editor';
import { expectToastTextToBeVisible } from '../fixtures/generic';
@@ -67,10 +66,7 @@ const getAttachmentItems = (root: Page) => root.locator('.rounded-md.border.bord
const getAttachmentDeleteButtons = (root: Page) => getAttachmentItems(root).locator('button');
const assertAttachmentVisibleInPopover = async (
root: Page,
{ label, url }: { label: string; url: string },
) => {
const assertAttachmentVisibleInPopover = async (root: Page, { label, url }: { label: string; url: string }) => {
const items = getAttachmentItems(root);
const matchingItem = items.filter({ hasText: label });
@@ -137,9 +133,7 @@ const assertAttachmentsInDatabase = async ({
}
};
const runAttachmentFlow = async (
surface: TEnvelopeEditorSurface,
): Promise<AttachmentFlowResult> => {
const runAttachmentFlow = async (surface: TEnvelopeEditorSurface): Promise<AttachmentFlowResult> => {
const externalId = `e2e-attachments-${nanoid()}`;
await updateExternalId(surface, externalId);
@@ -205,9 +199,7 @@ const runAttachmentFlow = async (
};
};
const runEmbeddedAttachmentFlow = async (
surface: TEnvelopeEditorSurface,
): Promise<AttachmentFlowResult> => {
const runEmbeddedAttachmentFlow = async (surface: TEnvelopeEditorSurface): Promise<AttachmentFlowResult> => {
const externalId = `e2e-attachments-${nanoid()}`;
await updateExternalId(surface, externalId);
@@ -1,20 +1,19 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import {
type TEnvelopeEditorSurface,
addEnvelopeItemPdf,
createEmbeddedEnvelopeCreateHash,
getEnvelopeEditorSettingsTrigger,
openEmbeddedEnvelopeEditor,
persistEmbeddedEnvelope,
setRecipientEmail,
type TEnvelopeEditorSurface,
} from '../fixtures/envelope-editor';
import { expectToastTextToBeVisible } from '../fixtures/generic';
@@ -185,9 +184,7 @@ const openEmbeddedCreateWithUser = async (
folderId: options.folderId,
});
await page.goto(
`/embed/v2/authoring/envelope/create?token=${encodeURIComponent(presignToken)}#${hash}`,
);
await page.goto(`/embed/v2/authoring/envelope/create?token=${encodeURIComponent(presignToken)}#${hash}`);
await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();
@@ -228,8 +225,7 @@ const setupMinimalEnvelope = async (surface: TEnvelopeEditorSurface, externalId:
* Click "Create Document" and expect a failure toast instead of the success heading.
*/
const expectCreateToFail = async (surface: TEnvelopeEditorSurface) => {
const actionButtonName =
surface.envelopeType === 'DOCUMENT' ? 'Create Document' : 'Create Template';
const actionButtonName = surface.envelopeType === 'DOCUMENT' ? 'Create Document' : 'Create Template';
await surface.root.getByRole('button', { name: actionButtonName }).click();
await expectToastTextToBeVisible(surface.root, 'Failed to create document');
@@ -1,11 +1,9 @@
import { type Page, expect, test } from '@playwright/test';
import { FieldType } from '@prisma/client';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { expect, type Page, test } from '@playwright/test';
import { FieldType } from '@prisma/client';
import {
type TEnvelopeEditorSurface,
addEnvelopeItemPdf,
clickAddMyselfButton,
clickAddSignerButton,
@@ -19,6 +17,7 @@ import {
persistEmbeddedEnvelope,
setRecipientEmail,
setRecipientName,
type TEnvelopeEditorSurface,
} from '../fixtures/envelope-editor';
import { expectToastTextToBeVisible } from '../fixtures/generic';
import { getKonvaElementCountForPage } from '../fixtures/konva';
@@ -78,11 +77,7 @@ type FieldButtonName =
| 'Checkbox'
| 'Dropdown';
const placeFieldOnPdf = async (
root: Page,
fieldName: FieldButtonName,
position: { x: number; y: number },
) => {
const placeFieldOnPdf = async (root: Page, fieldName: FieldButtonName, position: { x: number; y: number }) => {
await root.getByRole('button', { name: fieldName, exact: true }).click();
const canvas = root.locator('.konva-container canvas').first();
@@ -103,9 +98,7 @@ const selectFieldOnCanvas = async (root: Page, position: { x: number; y: number
await canvas.click({ position, force: true });
};
const runAddAndPersistSignatureTextFields = async (
surface: TEnvelopeEditorSurface,
): Promise<TFieldFlowResult> => {
const runAddAndPersistSignatureTextFields = async (surface: TEnvelopeEditorSurface): Promise<TFieldFlowResult> => {
const externalId = `e2e-fields-${nanoid()}`;
if (surface.isEmbedded && !surface.envelopeId) {
@@ -150,8 +143,7 @@ const getFieldMetaType = (fieldMeta: unknown) => {
return typeof fieldMeta.type === 'string' ? fieldMeta.type : null;
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null;
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;
const assertFieldsPersistedInDatabase = async ({
surface,
@@ -178,9 +170,7 @@ const assertFieldsPersistedInDatabase = async ({
},
});
const recipient = envelope.recipients.find(
(currentRecipient) => currentRecipient.email === recipientEmail,
);
const recipient = envelope.recipients.find((currentRecipient) => currentRecipient.email === recipientEmail);
expect(recipient).toBeDefined();
@@ -214,9 +204,7 @@ const MULTI_RECIPIENT_VALUES = {
},
};
const runMultiRecipientFieldFlow = async (
surface: TEnvelopeEditorSurface,
): Promise<TMultiRecipientFlowResult> => {
const runMultiRecipientFieldFlow = async (surface: TEnvelopeEditorSurface): Promise<TMultiRecipientFlowResult> => {
const externalId = `e2e-multi-recip-${nanoid()}`;
const root = surface.root;
@@ -322,9 +310,7 @@ type TAllFieldTypesFlowResult = {
externalId: string;
};
const runAllFieldTypesFlow = async (
surface: TEnvelopeEditorSurface,
): Promise<TAllFieldTypesFlowResult> => {
const runAllFieldTypesFlow = async (surface: TEnvelopeEditorSurface): Promise<TAllFieldTypesFlowResult> => {
const externalId = `e2e-all-fields-${nanoid()}`;
const root = surface.root;
@@ -562,9 +548,7 @@ type TDuplicateDeleteFlowResult = {
externalId: string;
};
const runDuplicateDeleteFieldFlow = async (
surface: TEnvelopeEditorSurface,
): Promise<TDuplicateDeleteFlowResult> => {
const runDuplicateDeleteFieldFlow = async (surface: TEnvelopeEditorSurface): Promise<TDuplicateDeleteFlowResult> => {
const externalId = `e2e-dup-del-${nanoid()}`;
const root = surface.root;
@@ -1,12 +1,10 @@
import { type Page, expect, test } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { expect, type Page, test } from '@playwright/test';
import {
type TEnvelopeEditorSurface,
addEnvelopeItemPdf,
clickAddMyselfButton,
clickEnvelopeEditorStep,
@@ -17,6 +15,7 @@ import {
persistEmbeddedEnvelope,
setRecipientEmail,
setRecipientName,
type TEnvelopeEditorSurface,
} from '../fixtures/envelope-editor';
import { expectToastTextToBeVisible } from '../fixtures/generic';
@@ -29,9 +28,7 @@ test.use({
const examplePdfBuffer = fs.readFileSync(path.join(__dirname, '../../../../assets/example.pdf'));
const multiPagePdfBuffer = fs.readFileSync(
path.join(__dirname, '../../../../assets/field-font-alignment.pdf'),
);
const multiPagePdfBuffer = fs.readFileSync(path.join(__dirname, '../../../../assets/field-font-alignment.pdf'));
// --- Shared helpers ---
@@ -55,27 +52,20 @@ const getEditButton = (root: Page, index: number) =>
const getEditDialog = (root: Page) => root.getByRole('dialog');
const getEditDialogTitleInput = (root: Page) =>
root.locator('[data-testid="envelope-item-edit-title-input"]');
const getEditDialogTitleInput = (root: Page) => root.locator('[data-testid="envelope-item-edit-title-input"]');
const getEditDialogDropzone = (root: Page) =>
root.locator('[data-testid="envelope-item-edit-dropzone"]');
const getEditDialogDropzone = (root: Page) => root.locator('[data-testid="envelope-item-edit-dropzone"]');
const getEditDialogSelectedFile = (root: Page) =>
root.locator('[data-testid="envelope-item-edit-selected-file"]');
const getEditDialogSelectedFile = (root: Page) => root.locator('[data-testid="envelope-item-edit-selected-file"]');
const getEditDialogClearFileButton = (root: Page) =>
root.locator('[data-testid="envelope-item-edit-clear-file"]');
const getEditDialogClearFileButton = (root: Page) => root.locator('[data-testid="envelope-item-edit-clear-file"]');
const getEditDialogUpdateButton = (root: Page) =>
root.locator('[data-testid="envelope-item-edit-update-button"]');
const getEditDialogUpdateButton = (root: Page) => root.locator('[data-testid="envelope-item-edit-update-button"]');
const assertPdfPageCount = async (root: Page, expectedCount: number) => {
await expect(root.locator('[data-pdf-content]').first()).toHaveAttribute(
'data-page-count',
String(expectedCount),
{ timeout: 15000 },
);
await expect(root.locator('[data-pdf-content]').first()).toHaveAttribute('data-page-count', String(expectedCount), {
timeout: 15000,
});
};
const navigateToFieldsPage = async (surface: TEnvelopeEditorSurface) => {
@@ -1,15 +1,13 @@
import { type Page, expect, test } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { seedPendingDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, type Page, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
import {
type TEnvelopeEditorSurface,
clickEnvelopeEditorStep,
getEnvelopeEditorSettingsTrigger,
getEnvelopeItemDragHandles,
@@ -20,6 +18,7 @@ import {
openEmbeddedEnvelopeEditor,
openTemplateEnvelopeEditor,
persistEmbeddedEnvelope,
type TEnvelopeEditorSurface,
} from '../fixtures/envelope-editor';
import { expectToastTextToBeVisible } from '../fixtures/generic';
@@ -76,10 +75,7 @@ const navigateToAddFieldsAndBack = async (root: Page) => {
await expect(root.getByRole('heading', { name: 'Recipients' })).toBeVisible();
};
const getEnvelopeItemsFromDatabase = async (
surface: TEnvelopeEditorSurface,
externalId: string,
) => {
const getEnvelopeItemsFromDatabase = async (surface: TEnvelopeEditorSurface, externalId: string) => {
const envelope = await prisma.envelope.findFirstOrThrow({
where: {
externalId,
@@ -199,9 +195,7 @@ const runEnvelopeItemCrudFlow = async ({
targetIndex: 1,
});
await expect
.poll(async () => await getCurrentTitles(root))
.toEqual(['Envelope Item B', 'Envelope Item A']);
await expect.poll(async () => await getCurrentTitles(root)).toEqual(['Envelope Item B', 'Envelope Item A']);
if (!isEmbedded) {
await navigateToAddFieldsAndBack(root);
@@ -366,14 +360,10 @@ test.describe('pending envelope title editing', () => {
expect(updatedEnvelope.envelopeItems[0].title).toBe('Updated Item Title');
// Verify audit logs were created for both title changes.
const titleAuditLog = updatedEnvelope.auditLogs.find(
(log) => log.type === 'DOCUMENT_TITLE_UPDATED',
);
const titleAuditLog = updatedEnvelope.auditLogs.find((log) => log.type === 'DOCUMENT_TITLE_UPDATED');
expect(titleAuditLog).toBeDefined();
const itemAuditLog = updatedEnvelope.auditLogs.find(
(log) => log.type === 'ENVELOPE_ITEM_UPDATED',
);
const itemAuditLog = updatedEnvelope.auditLogs.find((log) => log.type === 'ENVELOPE_ITEM_UPDATED');
expect(itemAuditLog).toBeDefined();
// Verify the upload dropzone is still disabled for pending envelopes.
@@ -1,11 +1,9 @@
import { type Page, expect, test } from '@playwright/test';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { expect, type Page, test } from '@playwright/test';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import {
type TEnvelopeEditorSurface,
addEnvelopeItemPdf,
assertRecipientRole,
clickAddMyselfButton,
@@ -24,6 +22,7 @@ import {
setRecipientName,
setRecipientRole,
setSigningOrderValue,
type TEnvelopeEditorSurface,
toggleAllowDictateSigners,
toggleSigningOrder,
} from '../fixtures/envelope-editor';
@@ -125,24 +124,17 @@ const runRecipientFlow = async (surface: TEnvelopeEditorSurface): Promise<Recipi
await navigateToAddFieldsAndBack(surface.root);
await expect(getRecipientEmailInputs(surface.root)).toHaveCount(2);
await expect(getRecipientEmailInputs(surface.root).nth(0)).toHaveValue(
TEST_RECIPIENT_VALUES.secondRecipient.email,
);
await expect(getRecipientEmailInputs(surface.root).nth(0)).toHaveValue(TEST_RECIPIENT_VALUES.secondRecipient.email);
await expect(getRecipientEmailInputs(surface.root).nth(1)).toHaveValue(primaryRecipient.email);
await expect(getRecipientNameInputs(surface.root).nth(0)).toHaveValue(
TEST_RECIPIENT_VALUES.secondRecipient.name,
);
await expect(getRecipientNameInputs(surface.root).nth(0)).toHaveValue(TEST_RECIPIENT_VALUES.secondRecipient.name);
await expect(getRecipientNameInputs(surface.root).nth(1)).toHaveValue(primaryRecipient.name);
await assertRecipientRole(surface.root, 0, 'Needs to approve');
await assertRecipientRole(surface.root, 1, 'Needs to sign');
await expect(surface.root.locator('#signingOrder')).toHaveAttribute('aria-checked', 'true');
await expect(surface.root.locator('#allowDictateNextSigner')).toHaveAttribute(
'aria-checked',
'true',
);
await expect(surface.root.locator('#allowDictateNextSigner')).toHaveAttribute('aria-checked', 'true');
await expect(getSigningOrderInputs(surface.root).nth(0)).toHaveValue('1');
await expect(getSigningOrderInputs(surface.root).nth(1)).toHaveValue('2');
@@ -210,9 +202,7 @@ const assertRecipientsPersistedInDatabase = async ({
expect(recipient.signingOrder).toBe(expectedRecipient.signingOrder);
});
expect(envelope.recipients.some((recipient) => recipient.email === removedRecipientEmail)).toBe(
false,
);
expect(envelope.recipients.some((recipient) => recipient.email === removedRecipientEmail)).toBe(false);
};
test.describe('document editor', () => {
@@ -1,13 +1,11 @@
import { type Page, expect, test } from '@playwright/test';
import { FieldType } from '@prisma/client';
import fs from 'node:fs';
import path from 'node:path';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { expect, type Page, test } from '@playwright/test';
import { FieldType } from '@prisma/client';
import {
type TEnvelopeEditorSurface,
addEnvelopeItemPdf,
clickAddMyselfButton,
clickEnvelopeEditorStep,
@@ -20,6 +18,7 @@ import {
persistEmbeddedEnvelope,
setRecipientEmail,
setRecipientName,
type TEnvelopeEditorSurface,
} from '../fixtures/envelope-editor';
import { expectToastTextToBeVisible } from '../fixtures/generic';
import { getKonvaElementCountForPage } from '../fixtures/konva';
@@ -39,9 +38,7 @@ type TestFilePayload = {
const examplePdfBuffer = fs.readFileSync(path.join(__dirname, '../../../../assets/example.pdf'));
const multiPagePdfBuffer = fs.readFileSync(
path.join(__dirname, '../../../../assets/field-font-alignment.pdf'),
);
const multiPagePdfBuffer = fs.readFileSync(path.join(__dirname, '../../../../assets/field-font-alignment.pdf'));
const createPdfPayload = (name: string, buffer: Buffer = examplePdfBuffer): TestFilePayload => ({
name,
@@ -77,10 +74,7 @@ const replaceEnvelopeItemPdf = async (
// Listen for the file chooser event before clicking so the native dialog
// is intercepted and never actually shown to the user.
const [fileChooser] = await Promise.all([
root.waitForEvent('filechooser'),
replaceButton.click(),
]);
const [fileChooser] = await Promise.all([root.waitForEvent('filechooser'), replaceButton.click()]);
await fileChooser.setFiles(file);
@@ -99,18 +93,12 @@ const replaceEnvelopeItemPdf = async (
};
const assertPdfPageCount = async (root: Page, expectedCount: number) => {
await expect(root.locator('[data-pdf-content]').first()).toHaveAttribute(
'data-page-count',
String(expectedCount),
{ timeout: 15000 },
);
await expect(root.locator('[data-pdf-content]').first()).toHaveAttribute('data-page-count', String(expectedCount), {
timeout: 15000,
});
};
const placeFieldOnPdf = async (
root: Page,
fieldName: 'Signature' | 'Text',
position: { x: number; y: number },
) => {
const placeFieldOnPdf = async (root: Page, fieldName: 'Signature' | 'Text', position: { x: number; y: number }) => {
await root.getByRole('button', { name: fieldName, exact: true }).click();
const canvas = root.locator('.konva-container canvas').first();
@@ -172,9 +160,7 @@ type BasicReplaceFlowResult = {
originalDocumentDataId: string | null;
};
const runBasicReplaceFlow = async (
surface: TEnvelopeEditorSurface,
): Promise<BasicReplaceFlowResult> => {
const runBasicReplaceFlow = async (surface: TEnvelopeEditorSurface): Promise<BasicReplaceFlowResult> => {
const { root, isEmbedded } = surface;
const externalId = `e2e-replace-${nanoid()}`;
@@ -313,9 +299,7 @@ const uploadMultiPagePdf = async (root: Page) => {
});
};
const runFieldCleanupReplaceFlow = async (
surface: TEnvelopeEditorSurface,
): Promise<FieldCleanupFlowResult> => {
const runFieldCleanupReplaceFlow = async (surface: TEnvelopeEditorSurface): Promise<FieldCleanupFlowResult> => {
const { root, isEmbedded } = surface;
const externalId = `e2e-replace-fields-${nanoid()}`;
@@ -1,8 +1,5 @@
import { type Page, expect, test } from '@playwright/test';
import { EnvelopeType, FieldType, RecipientRole } 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';
@@ -13,6 +10,8 @@ import type {
} 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 { expect, type Page, test } from '@playwright/test';
import { EnvelopeType, FieldType, RecipientRole } from '@prisma/client';
import { apiSignin } from '../fixtures/authentication';
import {
@@ -31,11 +30,7 @@ const examplePdfBuffer = fs.readFileSync(path.join(__dirname, '../../../../asset
/**
* Place a field on the PDF canvas in the envelope editor.
*/
const placeFieldOnPdf = async (
root: Page,
fieldName: 'Signature' | 'Text',
position: { x: number; y: number },
) => {
const placeFieldOnPdf = async (root: Page, fieldName: 'Signature' | 'Text', position: { x: number; y: number }) => {
await root.getByRole('button', { name: fieldName, exact: true }).click();
const canvas = root.locator('.konva-container canvas').first();
@@ -69,10 +64,7 @@ const createDocumentWithRecipientAndField = async () => {
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append(
'files',
new File([examplePdfBuffer], 'example.pdf', { type: 'application/pdf' }),
);
formData.append('files', new File([examplePdfBuffer], 'example.pdf', { type: 'application/pdf' }));
const createRes = await fetch(`${V2_API_BASE_URL}/envelope/create`, {
method: 'POST',
@@ -1,16 +1,15 @@
import { type Page, expect, test } from '@playwright/test';
import { DocumentDistributionMethod, DocumentVisibility } from '@prisma/client';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { expect, type Page, test } from '@playwright/test';
import { DocumentDistributionMethod, DocumentVisibility } from '@prisma/client';
import {
type TEnvelopeEditorSurface,
getEnvelopeEditorSettingsTrigger,
openDocumentEnvelopeEditor,
openEmbeddedEnvelopeEditor,
openTemplateEnvelopeEditor,
persistEmbeddedEnvelope,
type TEnvelopeEditorSurface,
} from '../fixtures/envelope-editor';
import { expectToastTextToBeVisible } from '../fixtures/generic';
@@ -78,11 +77,7 @@ const clickSettingsDialogHeader = async (root: Page) => {
};
const getComboboxByLabel = (root: Page, label: string) =>
root
.locator(`label:has-text("${label}")`)
.locator('xpath=..')
.locator('[role="combobox"]')
.first();
root.locator(`label:has-text("${label}")`).locator('xpath=..').locator('[role="combobox"]').first();
const selectMultiSelectOption = async (
root: Page,
@@ -96,10 +91,7 @@ const selectMultiSelectOption = async (
await clickSettingsDialogHeader(root);
};
const runSettingsFlow = async (
{ root }: TEnvelopeEditorSurface,
{ externalId, isEmbedded }: SettingsFlowData,
) => {
const runSettingsFlow = async ({ root }: TEnvelopeEditorSurface, { externalId, isEmbedded }: SettingsFlowData) => {
await openSettingsDialog(root);
await getComboboxByLabel(root, 'Language').click();
@@ -161,9 +153,7 @@ const runSettingsFlow = async (
await clickSettingsDialogHeader(root);
await root.locator('[data-testid="reminder-repeat-amount"]').clear();
await root
.locator('[data-testid="reminder-repeat-amount"]')
.fill(String(TEST_SETTINGS_VALUES.reminderRepeatAmount));
await root.locator('[data-testid="reminder-repeat-amount"]').fill(String(TEST_SETTINGS_VALUES.reminderRepeatAmount));
await root.locator('[data-testid="reminder-repeat-unit"]').click();
await root.getByRole('option', { name: TEST_SETTINGS_VALUES.reminderRepeatUnit }).click();
@@ -221,11 +211,7 @@ const runSettingsFlow = async (
const hasActionAuthSelect = (await actionAuthSelect.count()) > 0;
if (hasActionAuthSelect) {
await selectMultiSelectOption(
root,
'documentActionSelectValue',
TEST_SETTINGS_VALUES.actionAuth,
);
await selectMultiSelectOption(root, 'documentActionSelectValue', TEST_SETTINGS_VALUES.actionAuth);
}
if (isEmbedded) {
@@ -245,36 +231,23 @@ const runSettingsFlow = async (
await openSettingsDialog(root);
await expect(root.locator('input[name="externalId"]')).toHaveValue(externalId);
await expect(root.locator('input[name="meta.redirectUrl"]')).toHaveValue(
TEST_SETTINGS_VALUES.redirectUrl,
);
await expect(root.locator('input[name="meta.redirectUrl"]')).toHaveValue(TEST_SETTINGS_VALUES.redirectUrl);
await expect(getComboboxByLabel(root, 'Language')).toContainText(TEST_SETTINGS_VALUES.language);
await expect(getComboboxByLabel(root, 'Allowed Signature Types')).not.toContainText('Upload');
await expect(getComboboxByLabel(root, 'Date Format')).toContainText(
TEST_SETTINGS_VALUES.dateFormat,
);
await expect(getComboboxByLabel(root, 'Date Format')).toContainText(TEST_SETTINGS_VALUES.dateFormat);
await expect(getComboboxByLabel(root, 'Time Zone')).toContainText(TEST_SETTINGS_VALUES.timezone);
await expect(root.locator('[data-testid="documentDistributionMethodSelectValue"]')).toContainText(
TEST_SETTINGS_VALUES.distributionMethod,
);
await expect(getComboboxByLabel(root, 'Expiration')).toContainText(
TEST_SETTINGS_VALUES.expirationMode,
);
await expect(root.getByRole('spinbutton')).toHaveValue(
String(TEST_SETTINGS_VALUES.expirationAmount),
);
await expect(getComboboxByLabel(root, 'Expiration')).toContainText(TEST_SETTINGS_VALUES.expirationMode);
await expect(root.getByRole('spinbutton')).toHaveValue(String(TEST_SETTINGS_VALUES.expirationAmount));
await expect(
root
.locator('button[role="combobox"]')
.filter({ hasText: TEST_SETTINGS_VALUES.expirationUnit })
.first(),
root.locator('button[role="combobox"]').filter({ hasText: TEST_SETTINGS_VALUES.expirationUnit }).first(),
).toBeVisible();
// Verify reminder settings persisted in UI.
await root.getByRole('button', { name: 'Reminders' }).click();
await expect(root.locator('[data-testid="reminder-mode-select"]')).toContainText(
TEST_SETTINGS_VALUES.reminderMode,
);
await expect(root.locator('[data-testid="reminder-mode-select"]')).toContainText(TEST_SETTINGS_VALUES.reminderMode);
await expect(root.locator('[data-testid="reminder-send-after-amount"]')).toHaveValue(
String(TEST_SETTINGS_VALUES.reminderSendAfterAmount),
);
@@ -301,15 +274,9 @@ const runSettingsFlow = async (
await expect(root.locator('#ownerDocumentCompleted')).toHaveAttribute('aria-checked', 'false');
await expect(root.locator('#ownerRecipientExpired')).toHaveAttribute('aria-checked', 'false');
await expect(root.locator('#ownerDocumentCreated')).toHaveAttribute('aria-checked', 'false');
await expect(root.locator('input[name="meta.emailReplyTo"]')).toHaveValue(
TEST_SETTINGS_VALUES.replyTo,
);
await expect(root.locator('input[name="meta.subject"]')).toHaveValue(
TEST_SETTINGS_VALUES.subject,
);
await expect(root.locator('textarea[name="meta.message"]')).toHaveValue(
TEST_SETTINGS_VALUES.message,
);
await expect(root.locator('input[name="meta.emailReplyTo"]')).toHaveValue(TEST_SETTINGS_VALUES.replyTo);
await expect(root.locator('input[name="meta.subject"]')).toHaveValue(TEST_SETTINGS_VALUES.subject);
await expect(root.locator('textarea[name="meta.message"]')).toHaveValue(TEST_SETTINGS_VALUES.message);
await root.getByRole('button', { name: 'Security' }).click();
await expect(root.locator('[data-testid="documentAccessSelectValue"]')).toContainText(
@@ -373,9 +340,7 @@ const assertEnvelopeSettingsPersistedInDatabase = async ({
expect(envelope.documentMeta.dateFormat).toBe(DB_EXPECTED_VALUES.dateFormat);
expect(envelope.documentMeta.timezone).toBe(DB_EXPECTED_VALUES.timezone);
expect(envelope.documentMeta.distributionMethod).toBe(DB_EXPECTED_VALUES.distributionMethod);
expect(envelope.documentMeta.envelopeExpirationPeriod).toEqual(
DB_EXPECTED_VALUES.envelopeExpirationPeriod,
);
expect(envelope.documentMeta.envelopeExpirationPeriod).toEqual(DB_EXPECTED_VALUES.envelopeExpirationPeriod);
expect(envelope.documentMeta.reminderSettings).toEqual(DB_EXPECTED_VALUES.reminderSettings);
expect(envelope.documentMeta.redirectUrl).toBe(TEST_SETTINGS_VALUES.redirectUrl);
expect(envelope.documentMeta.emailReplyTo).toBe(TEST_SETTINGS_VALUES.replyTo);
@@ -395,9 +360,7 @@ const assertEnvelopeSettingsPersistedInDatabase = async ({
}
};
const parseAuthOptions = (
authOptions: unknown,
): { globalAccessAuth: string[]; globalActionAuth: string[] } => {
const parseAuthOptions = (authOptions: unknown): { globalAccessAuth: string[]; globalActionAuth: string[] } => {
if (!isRecord(authOptions)) {
return {
globalAccessAuth: [],
@@ -415,8 +378,7 @@ const parseAuthOptions = (
};
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null;
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;
test.describe('document editor', () => {
test('update and persist settings', async ({ page }) => {
@@ -1,12 +1,11 @@
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 { type APIRequestContext, expect, type Page, test } from '@playwright/test';
import { DocumentStatus, EnvelopeType, FieldType } from '@prisma/client';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../lib/constants/app';
import { createApiToken } from '../../../lib/server-only/public-api/create-api-token';
@@ -202,10 +201,7 @@ test('cert and audit log pages match A4 page dimensions', async ({ page, request
});
});
test('cert and audit log pages match tabloid landscape page dimensions', async ({
page,
request,
}) => {
test('cert and audit log pages match tabloid landscape page dimensions', async ({ page, request }) => {
await signAndVerifyPageDimensions({
page,
request,
@@ -1,17 +1,16 @@
import { createCanvas } from '@napi-rs/canvas';
import type { TestInfo } from '@playwright/test';
import { 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 pixelMatch from 'pixelmatch';
import { PNG } from 'pngjs';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
import { prisma } from '@documenso/prisma';
import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed';
import { seedUser } from '@documenso/prisma/seed/users';
import { createCanvas } from '@napi-rs/canvas';
import type { TestInfo } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { DocumentStatus, EnvelopeType, FieldType } from '@prisma/client';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import pixelMatch from 'pixelmatch';
import { PNG } from 'pngjs';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../lib/constants/app';
import { isBase64Image } from '../../../lib/constants/signatures';
@@ -72,9 +71,7 @@ test('field placement visual regression', async ({ page, request }, testInfo) =>
});
// Step 1: Create initial envelope with Prisma (with first envelope item)
const alignmentPdf = fs.readFileSync(
path.join(__dirname, '../../../../assets/field-font-alignment.pdf'),
);
const alignmentPdf = fs.readFileSync(path.join(__dirname, '../../../../assets/field-font-alignment.pdf'));
const fieldMetaPdf = fs.readFileSync(path.join(__dirname, '../../../../assets/field-meta.pdf'));
@@ -223,9 +220,7 @@ test('field placement visual regression', async ({ page, request }, testInfo) =>
? {
create: {
recipientId: envelope.recipients[0].id,
signatureImageAsBase64: isBase64Image(foundField.signature)
? foundField.signature
: null,
signatureImageAsBase64: isBase64Image(foundField.signature) ? foundField.signature : null,
typedSignature: isBase64Image(foundField.signature) ? null : foundField.signature,
},
}
@@ -254,10 +249,7 @@ test('field placement visual regression', async ({ page, request }, testInfo) =>
await Promise.all(
emailFields.map(async (field) => {
const testFields =
field.envelopeItem.title === 'alignment-pdf'
? ALIGNMENT_TEST_FIELDS
: FIELD_META_TEST_FIELDS;
const testFields = field.envelopeItem.title === 'alignment-pdf' ? ALIGNMENT_TEST_FIELDS : FIELD_META_TEST_FIELDS;
const foundField = testFields.find(
(f) =>
@@ -396,10 +388,7 @@ test.skip('download envelope images', async ({ page, request }) => {
await Promise.all(
emailFields.map(async (field) => {
const testFields =
field.envelopeItem.title === 'alignment-pdf'
? ALIGNMENT_TEST_FIELDS
: FIELD_META_TEST_FIELDS;
const testFields = field.envelopeItem.title === 'alignment-pdf' ? ALIGNMENT_TEST_FIELDS : FIELD_META_TEST_FIELDS;
const foundField = testFields.find(
(f) =>
@@ -528,17 +517,10 @@ type CompareSignedPdfWithImagesOptions = {
testInfo: TestInfo;
};
const compareSignedPdfWithImages = async ({
id,
pdfData,
images,
testInfo,
}: CompareSignedPdfWithImagesOptions) => {
const compareSignedPdfWithImages = async ({ id, pdfData, images, testInfo }: CompareSignedPdfWithImagesOptions) => {
const renderedImages = await renderPdfToImage(pdfData);
const blankCertificateFile = fs.readFileSync(
path.join(__dirname, '../../visual-regression/blank-certificate.png'),
);
const blankCertificateFile = fs.readFileSync(path.join(__dirname, '../../visual-regression/blank-certificate.png'));
const blankCertificateImage = PNG.sync.read(blankCertificateFile).data;
for (const [index, { image, width, height }] of renderedImages.entries()) {
@@ -1,7 +1,5 @@
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';
@@ -13,6 +11,7 @@ import type {
TCreateEnvelopeResponse,
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
import type { TDistributeEnvelopeRequest } from '@documenso/trpc/server/envelope-router/distribute-envelope.types';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
import { openDropdownMenu } from '../fixtures/generic';
@@ -24,9 +23,7 @@ const examplePdf = fs.readFileSync(path.join(__dirname, '../../../../assets/exam
test.describe.configure({ mode: 'parallel' });
test('[ENVELOPE_EXPIRATION]: sending document sets expiresAt on recipients', async ({
request,
}) => {
test('[ENVELOPE_EXPIRATION]: sending document sets expiresAt on recipients', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
@@ -98,9 +95,7 @@ test('[ENVELOPE_EXPIRATION]: sending document sets expiresAt on recipients', asy
expect(diffDays).toBeLessThan(100);
});
test('[ENVELOPE_EXPIRATION]: sending document with custom org expiration period', async ({
request,
}) => {
test('[ENVELOPE_EXPIRATION]: sending document with custom org expiration period', async ({ request }) => {
const { user, organisation, team } = await seedUser();
// Set org expiration to 7 days.
@@ -1,16 +1,13 @@
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 { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
test.describe.configure({ mode: 'parallel' });
test('[ENVELOPE_EXPIRATION]: set custom expiration period at organisation level', async ({
page,
}) => {
test('[ENVELOPE_EXPIRATION]: set custom expiration period at organisation level', async ({ page }) => {
const { user, organisation } = await seedUser({
isPersonalOrganisation: false,
});
@@ -1,9 +1,8 @@
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 { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
import { signSignaturePad } from '../fixtures/signature';
@@ -1,17 +1,16 @@
import { createCanvas } from '@napi-rs/canvas';
import type { TestInfo } from '@playwright/test';
import { 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 pixelMatch from 'pixelmatch';
import { PNG } from 'pngjs';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
import { prisma } from '@documenso/prisma';
import { seedOverflowTestDocument } from '@documenso/prisma/seed/initial-seed';
import { seedUser } from '@documenso/prisma/seed/users';
import { createCanvas } from '@napi-rs/canvas';
import type { TestInfo } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { DocumentStatus, EnvelopeType, FieldType } from '@prisma/client';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import pixelMatch from 'pixelmatch';
import { PNG } from 'pngjs';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../lib/constants/app';
import { isBase64Image } from '../../../lib/constants/signatures';
@@ -71,9 +70,7 @@ test('overflow visual regression', async ({ page, request }, testInfo) => {
});
// Step 1: Create initial envelope with overflow PDF
const overflowPdf = fs.readFileSync(
path.join(__dirname, '../../../../assets/field-overflow.pdf'),
);
const overflowPdf = fs.readFileSync(path.join(__dirname, '../../../../assets/field-overflow.pdf'));
const formData = new FormData();
@@ -192,9 +189,7 @@ test('overflow visual regression', async ({ page, request }, testInfo) => {
? {
create: {
recipientId: envelope.recipients[0].id,
signatureImageAsBase64: isBase64Image(foundField.signature)
? foundField.signature
: null,
signatureImageAsBase64: isBase64Image(foundField.signature) ? foundField.signature : null,
typedSignature: isBase64Image(foundField.signature) ? null : foundField.signature,
},
}
@@ -292,9 +287,7 @@ test('overflow visual regression', async ({ page, request }, testInfo) => {
const loadedImages = storedImages
.filter((image) => image.startsWith(`field-overflow-`))
.sort((leftImage, rightImage) => {
return (
getVisualRegressionImageIndex(leftImage) - getVisualRegressionImageIndex(rightImage)
);
return getVisualRegressionImageIndex(leftImage) - getVisualRegressionImageIndex(rightImage);
})
.map((image) => fs.readFileSync(path.join(__dirname, '../../visual-regression', image)));
@@ -482,12 +475,7 @@ type CompareSignedPdfWithImagesOptions = {
testInfo: TestInfo;
};
const compareSignedPdfWithImages = async ({
id,
pdfData,
images,
testInfo,
}: CompareSignedPdfWithImagesOptions) => {
const compareSignedPdfWithImages = async ({ id, pdfData, images, testInfo }: CompareSignedPdfWithImagesOptions) => {
const renderedImages = await renderPdfToImage(pdfData);
expect(images).toHaveLength(renderedImages.length);
@@ -1,10 +1,9 @@
import { expect, test } from '@playwright/test';
import { DocumentStatus, FieldType } from '@prisma/client';
import { DateTime } from 'luxon';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import { prisma } from '@documenso/prisma';
import { expect, test } from '@playwright/test';
import { DocumentStatus, FieldType } from '@prisma/client';
import { DateTime } from 'luxon';
import { apiSeedPendingDocument } from '../fixtures/api-seeds';
@@ -58,11 +57,9 @@ test.describe('V2 envelope field insertion during signing', () => {
}
const x =
(Number(sigField.positionX) / 100) * canvasBox.width +
((Number(sigField.width) / 100) * canvasBox.width) / 2;
(Number(sigField.positionX) / 100) * canvasBox.width + ((Number(sigField.width) / 100) * canvasBox.width) / 2;
const y =
(Number(sigField.positionY) / 100) * canvasBox.height +
((Number(sigField.height) / 100) * canvasBox.height) / 2;
(Number(sigField.positionY) / 100) * canvasBox.height + ((Number(sigField.height) / 100) * canvasBox.height) / 2;
await canvas.click({ position: { x, y } });
await page.waitForTimeout(500);
@@ -165,17 +162,12 @@ test.describe('V2 envelope field insertion during signing', () => {
}
// Only NAME and SIGNATURE fields need manual interaction (DATE and EMAIL are auto-filled).
const manualFields = fields.filter(
(f) => f.type !== FieldType.DATE && f.type !== FieldType.EMAIL,
);
const manualFields = fields.filter((f) => f.type !== FieldType.DATE && f.type !== FieldType.EMAIL);
for (const field of manualFields) {
const x =
(Number(field.positionX) / 100) * canvasBox.width +
((Number(field.width) / 100) * canvasBox.width) / 2;
const x = (Number(field.positionX) / 100) * canvasBox.width + ((Number(field.width) / 100) * canvasBox.width) / 2;
const y =
(Number(field.positionY) / 100) * canvasBox.height +
((Number(field.height) / 100) * canvasBox.height) / 2;
(Number(field.positionY) / 100) * canvasBox.height + ((Number(field.height) / 100) * canvasBox.height) / 2;
await canvas.click({ position: { x, y } });
@@ -1,13 +1,12 @@
import { PDF } from '@libpdf/core';
import { expect, test } from '@playwright/test';
import { DocumentStatus, FieldType } from '@prisma/client';
import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
import { prisma } from '@documenso/prisma';
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
import { PDF } from '@libpdf/core';
import { expect, test } from '@playwright/test';
import { DocumentStatus, FieldType } from '@prisma/client';
import { apiSignin } from '../fixtures/authentication';
import { signSignaturePad } from '../fixtures/signature';
@@ -106,9 +105,7 @@ test.describe('Signing Certificate Tests', () => {
expect(pdfDoc.getPageCount()).toBe(originalPdf.getPageCount() + 1); // Original + Certificate
});
test('team document with signing certificate enabled should include certificate', async ({
page,
}) => {
test('team document with signing certificate enabled should include certificate', async ({ page }) => {
const { owner, team } = await seedTeam();
const { document, recipients } = await seedPendingDocumentWithFullFields({
@@ -211,9 +208,7 @@ test.describe('Signing Certificate Tests', () => {
expect(completedPdf.getPageCount()).toBe(originalPdf.getPageCount() + 1); // Original + Certificate
});
test('team document with signing certificate disabled should not include certificate', async ({
page,
}) => {
test('team document with signing certificate disabled should not include certificate', async ({ page }) => {
const { owner, team } = await seedTeam();
const { document, recipients } = await seedPendingDocumentWithFullFields({
@@ -304,9 +299,7 @@ test.describe('Signing Certificate Tests', () => {
version: 'signed',
});
const completedDocumentData = await fetch(documentUrl).then(
async (res) => await res.arrayBuffer(),
);
const completedDocumentData = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
// Load the PDF and check number of pages
const completedPdf = await PDF.load(new Uint8Array(completedDocumentData));
+7 -26
View File
@@ -16,10 +16,9 @@
* });
* });
*/
import { type APIRequestContext, expect } 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 { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
@@ -34,6 +33,7 @@ import type {
} from '@documenso/trpc/server/envelope-router/distribute-envelope.types';
import type { TCreateEnvelopeRecipientsResponse } 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 { type APIRequestContext, expect } from '@playwright/test';
// ---------------------------------------------------------------------------
// Constants
@@ -543,12 +543,7 @@ export const apiSeedDraftDocument = async (
// If we need per-recipient fields, add recipients and fields separately
if (options.recipients && options.fieldsPerRecipient) {
const recipientsRes = await apiCreateRecipients(
request,
ctx.token,
envelopeId,
options.recipients,
);
const recipientsRes = await apiCreateRecipients(request, ctx.token, envelopeId, options.recipients);
// Get envelope to resolve envelope item IDs
const envelopeData = await apiGetEnvelope(request, ctx.token, envelopeId);
@@ -694,12 +689,7 @@ export const apiSeedPendingDocument = async (
}
// Distribute
const distributeResult = await apiDistributeEnvelope(
request,
ctx.token,
envelopeId,
options.distributeMeta,
);
const distributeResult = await apiDistributeEnvelope(request, ctx.token, envelopeId, options.distributeMeta);
const envelope = await apiGetEnvelope(request, ctx.token, envelopeId);
@@ -758,12 +748,7 @@ export const apiSeedTemplate = async (
const { id: envelopeId } = await apiCreateEnvelope(request, ctx.token, createOptions);
if (options.recipients && options.fieldsPerRecipient) {
const recipientsRes = await apiCreateRecipients(
request,
ctx.token,
envelopeId,
options.recipients,
);
const recipientsRes = await apiCreateRecipients(request, ctx.token, envelopeId, options.recipients);
const envelopeData = await apiGetEnvelope(request, ctx.token, envelopeId);
const firstItemId = envelopeData.envelopeItems[0]?.id;
@@ -833,9 +818,7 @@ export const apiSeedDirectTemplate = async (
// Find the recipient ID for the direct link
const directRecipientEmail = options.directRecipient?.email ?? recipients[0].email;
const directRecipient = templateResult.envelope.recipients.find(
(r) => r.email === directRecipientEmail,
);
const directRecipient = templateResult.envelope.recipients.find((r) => r.email === directRecipientEmail);
if (!directRecipient) {
throw new Error(`Direct template recipient not found: ${directRecipientEmail}`);
@@ -887,9 +870,7 @@ export const apiSeedMultipleDraftDocuments = async (
const ctx = context ?? (await apiCreateTestContext('e2e-multi-doc'));
const results = await Promise.all(
documents.map(async (docOptions) =>
apiSeedDraftDocument(request, { ...docOptions, context: ctx }),
),
documents.map(async (docOptions) => apiSeedDraftDocument(request, { ...docOptions, context: ctx })),
);
return {
@@ -1,6 +1,5 @@
import type { Page } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import type { Page } from '@playwright/test';
type LoginOptions = {
page: Page;
@@ -1,14 +1,13 @@
import type { Page } from '@playwright/test';
import { expect } 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 { DEFAULT_EMBEDDED_EDITOR_CONFIG } from '@documenso/lib/types/envelope-editor';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { apiSignin } from './authentication';
@@ -216,9 +215,7 @@ export const openEmbeddedEnvelopeEditor = async (
darkModeDisabled,
});
await page.goto(
`/embed/v2/authoring/envelope/create?token=${encodeURIComponent(embeddedToken)}#${hash}`,
);
await page.goto(`/embed/v2/authoring/envelope/create?token=${encodeURIComponent(embeddedToken)}#${hash}`);
}
await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();
@@ -235,14 +232,11 @@ export const openEmbeddedEnvelopeEditor = async (
};
};
export const getEnvelopeEditorSettingsTrigger = (root: Page) =>
root.locator('button[title="Settings"]');
export const getEnvelopeEditorSettingsTrigger = (root: Page) => root.locator('button[title="Settings"]');
export const getEnvelopeItemTitleInputs = (root: Page) =>
root.locator('[data-testid^="envelope-item-title-input-"]');
export const getEnvelopeItemTitleInputs = (root: Page) => root.locator('[data-testid^="envelope-item-title-input-"]');
export const getEnvelopeItemDragHandles = (root: Page) =>
root.locator('[data-testid^="envelope-item-drag-handle-"]');
export const getEnvelopeItemDragHandles = (root: Page) => root.locator('[data-testid^="envelope-item-drag-handle-"]');
export const getEnvelopeItemRemoveButtons = (root: Page) =>
root.locator('[data-testid^="envelope-item-remove-button-"]');
@@ -261,25 +255,18 @@ export const addEnvelopeItemPdf = async (root: Page, fileName = 'embedded-envelo
});
};
export const getRecipientEmailInputs = (root: Page) =>
root.locator('[data-testid="signer-email-input"]');
export const getRecipientEmailInputs = (root: Page) => root.locator('[data-testid="signer-email-input"]');
export const getRecipientNameInputs = (root: Page) =>
root.locator('input[placeholder^="Recipient "]');
export const getRecipientNameInputs = (root: Page) => root.locator('input[placeholder^="Recipient "]');
export const getRecipientRows = (root: Page) =>
root.locator('[data-testid="signer-email-input"]').locator('xpath=ancestor::fieldset[1]');
export const getRecipientRemoveButtons = (root: Page) =>
root.locator('[data-testid="remove-signer-button"]');
export const getRecipientRemoveButtons = (root: Page) => root.locator('[data-testid="remove-signer-button"]');
export const getSigningOrderInputs = (root: Page) =>
root.locator('[data-testid="signing-order-input"]');
export const getSigningOrderInputs = (root: Page) => root.locator('[data-testid="signing-order-input"]');
export const clickEnvelopeEditorStep = async (
root: Page,
stepId: 'upload' | 'addFields' | 'preview',
) => {
export const clickEnvelopeEditorStep = async (root: Page, stepId: 'upload' | 'addFields' | 'preview') => {
await root.waitForTimeout(200);
await root.locator(`[data-testid="envelope-editor-step-${stepId}"]`).first().click();
};
@@ -303,12 +290,7 @@ export const setRecipientName = async (root: Page, index: number, name: string)
export const setRecipientRole = async (
root: Page,
index: number,
roleLabel:
| 'Needs to sign'
| 'Needs to approve'
| 'Needs to view'
| 'Receives copy'
| 'Can prepare',
roleLabel: 'Needs to sign' | 'Needs to approve' | 'Needs to view' | 'Receives copy' | 'Can prepare',
) => {
const row = getRecipientRows(root).nth(index);
@@ -319,12 +301,7 @@ export const setRecipientRole = async (
export const assertRecipientRole = async (
root: Page,
index: number,
roleLabel:
| 'Needs to sign'
| 'Needs to approve'
| 'Needs to view'
| 'Receives copy'
| 'Can prepare',
roleLabel: 'Needs to sign' | 'Needs to approve' | 'Needs to view' | 'Receives copy' | 'Can prepare',
) => {
const row = getRecipientRows(root).nth(index);
const roleValueByLabel: Record<typeof roleLabel, string> = {
@@ -335,10 +312,7 @@ export const assertRecipientRole = async (
'Can prepare': 'ASSISTANT',
};
await expect(row.locator('button[role="combobox"]').first()).toHaveAttribute(
'title',
roleValueByLabel[roleLabel],
);
await expect(row.locator('button[role="combobox"]').first()).toHaveAttribute('title', roleValueByLabel[roleLabel]);
};
export const toggleSigningOrder = async (root: Page, enabled: boolean) => {
@@ -397,11 +371,7 @@ export const persistEmbeddedEnvelope = async (surface: TEnvelopeEditorSurface) =
await expect(surface.root.getByRole('heading', { name: completionHeading })).toBeVisible();
};
const resolveEmbeddingToken = async (
page: Page,
inputToken: string,
scope?: string,
): Promise<string> => {
const resolveEmbeddingToken = async (page: Page, inputToken: string, scope?: string): Promise<string> => {
if (!inputToken.startsWith('api_')) {
return inputToken;
}
+1 -1
View File
@@ -1,5 +1,5 @@
import type { Locator } from '@playwright/test';
import { type Page, expect } from '@playwright/test';
import { expect, type Page } from '@playwright/test';
export const expectTextToBeVisible = async (page: Page, text: string) => {
await expect(page.getByText(text).first()).toBeVisible();
+1 -5
View File
@@ -1,11 +1,7 @@
import type { Page } from '@playwright/test';
import type Konva from 'konva';
export const getKonvaElementCountForPage = async (
page: Page,
pageNumber: number,
elementSelector: string,
) => {
export const getKonvaElementCountForPage = async (page: Page, pageNumber: number, elementSelector: string) => {
await page.locator('.konva-container canvas').first().waitFor({ state: 'visible' });
return await page.evaluate(
@@ -1,12 +1,11 @@
import { expect, test } from '@playwright/test';
import path from 'node:path';
import { prisma } from '@documenso/prisma';
import { DocumentVisibility, FolderType, TeamMemberRole } from '@documenso/prisma/client';
import { seedBlankDocument, seedTeamDocuments } from '@documenso/prisma/seed/documents';
import { seedBlankFolder } from '@documenso/prisma/seed/folders';
import { seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
import { expectTextToBeVisible, openDropdownMenu } from '../fixtures/generic';
@@ -88,9 +87,7 @@ test('[TEAMS]: can create a document inside a document folder', async ({ page })
page.getByRole('button', { name: 'Document (Legacy)' }).click(),
]);
await fileChooser.setFiles(
path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'),
);
await fileChooser.setFiles(path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'));
await page.waitForTimeout(3000);
@@ -405,9 +402,7 @@ test('[TEAMS]: can create a template inside a template folder', async ({ page })
page.getByRole('button', { name: 'Template (Legacy)' }).click(),
]);
await fileChooser.setFiles(
path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'),
);
await fileChooser.setFiles(path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'));
await page.waitForTimeout(3000);
@@ -677,9 +672,7 @@ test('[TEAMS]: can navigate between template folders', async ({ page }) => {
await expect(page.getByText('Team Contract Template 1')).toBeVisible();
});
test('[TEAMS]: folder visibility is properly applied based on team member roles', async ({
page,
}) => {
test('[TEAMS]: folder visibility is properly applied based on team member roles', async ({ page }) => {
const { team, teamOwner } = await seedTeamDocuments();
const teamMember1 = await seedTeamMember({
@@ -893,9 +886,7 @@ test('[TEAMS]: documents inherit folder visibility', async ({ page }) => {
page.getByRole('button', { name: 'Document (Legacy)' }).click(),
]);
await fileChooser.setFiles(
path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'),
);
await fileChooser.setFiles(path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'));
await page.waitForTimeout(3000);
@@ -2119,9 +2110,7 @@ test('[TEAMS]: team member cannot see admin folder in folder list', async ({ pag
await expect(page.getByText('Admin Only Folder')).not.toBeVisible();
});
test('[TEAMS]: team member can access admin folder via URL and see everyone documents', async ({
page,
}) => {
test('[TEAMS]: team member can access admin folder via URL and see everyone documents', async ({ page }) => {
const { team, teamOwner } = await seedTeamDocuments();
const teamMember = await seedTeamMember({
@@ -2203,9 +2192,7 @@ test('[TEAMS]: team member cannot see manager folder in folder list', async ({ p
await expect(page.getByText('Manager Folder')).not.toBeVisible();
});
test('[TEAMS]: team member can access manager folder via URL and see everyone documents', async ({
page,
}) => {
test('[TEAMS]: team member can access manager folder via URL and see everyone documents', async ({ page }) => {
const { team, teamOwner } = await seedTeamDocuments();
const teamMember = await seedTeamMember({
@@ -2305,9 +2292,7 @@ test('[TEAMS]: team member can see everyone folders', async ({ page }) => {
await expect(page.getByText('Everyone Folder')).toBeVisible();
});
test('[TEAMS]: team member can only see everyone documents in everyone folder', async ({
page,
}) => {
test('[TEAMS]: team member can only see everyone documents in everyone folder', async ({ page }) => {
const { team, teamOwner } = await seedTeamDocuments();
const teamMember = await seedTeamMember({
@@ -2362,9 +2347,7 @@ test('[TEAMS]: team member can only see everyone documents in everyone folder',
await expect(page.getByText('Admin Document')).not.toBeVisible();
});
test('[TEAMS]: team manager can see manager and everyone folders in folder list', async ({
page,
}) => {
test('[TEAMS]: team manager can see manager and everyone folders in folder list', async ({ page }) => {
const { team, teamOwner } = await seedTeamDocuments();
const teamManager = await seedTeamMember({
@@ -2408,9 +2391,7 @@ test('[TEAMS]: team manager can see manager and everyone folders in folder list'
await expect(page.getByText('Everyone Folder')).toBeVisible();
});
test('[TEAMS]: team manager can see manager and everyone documents in manager folder', async ({
page,
}) => {
test('[TEAMS]: team manager can see manager and everyone documents in manager folder', async ({ page }) => {
const { team, teamOwner } = await seedTeamDocuments();
const teamManager = await seedTeamMember({
@@ -2460,18 +2441,14 @@ test('[TEAMS]: team manager can see manager and everyone documents in manager fo
redirectPath: `/t/${team.url}/documents/f/${managerFolder.id}`,
});
await expect(
page.getByTestId('folder-grid-breadcrumbs').getByRole('link', { name: 'Manager Folder' }),
).toBeVisible();
await expect(page.getByTestId('folder-grid-breadcrumbs').getByRole('link', { name: 'Manager Folder' })).toBeVisible();
await expect(page.getByText('Manager Folder - Everyone Document')).toBeVisible();
await expect(page.getByText('Manager Folder - Manager Document')).toBeVisible();
await expect(page.getByText('Manager Folder - Admin Document')).not.toBeVisible();
});
test('[TEAMS]: team manager can see manager and everyone documents in everyone folder', async ({
page,
}) => {
test('[TEAMS]: team manager can see manager and everyone documents in everyone folder', async ({ page }) => {
const { team, teamOwner } = await seedTeamDocuments();
const teamManager = await seedTeamMember({
@@ -2723,9 +2700,7 @@ test('[TEAMS]: team owner can see all documents in manager folder', async ({ pag
redirectPath: `/t/${team.url}/documents/f/${managerFolder.id}`,
});
await expect(
page.getByTestId('folder-grid-breadcrumbs').getByRole('link', { name: 'Manager Folder' }),
).toBeVisible();
await expect(page.getByTestId('folder-grid-breadcrumbs').getByRole('link', { name: 'Manager Folder' })).toBeVisible();
await expect(page.getByText('Manager Folder - Everyone Document')).toBeVisible();
await expect(page.getByText('Manager Folder - Manager Document')).toBeVisible();
await expect(page.getByText('Manager Folder - Admin Document')).toBeVisible();
@@ -2936,9 +2911,7 @@ test('[TEAMS]: team admin can see all documents in manager folder', async ({ pag
redirectPath: `/t/${team.url}/documents/f/${managerFolder.id}`,
});
await expect(
page.getByTestId('folder-grid-breadcrumbs').getByRole('link', { name: 'Manager Folder' }),
).toBeVisible();
await expect(page.getByTestId('folder-grid-breadcrumbs').getByRole('link', { name: 'Manager Folder' })).toBeVisible();
await expect(page.getByText('Manager Folder - Everyone Document')).toBeVisible();
await expect(page.getByText('Manager Folder - Manager Document')).toBeVisible();
await expect(page.getByText('Manager Folder - Admin Document')).toBeVisible();
@@ -1,9 +1,8 @@
import { expect, test } from '@playwright/test';
import fs from 'node:fs/promises';
import path from 'node:path';
import type { TCachedLicense, TLicenseClaim } from '@documenso/lib/types/license';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -101,226 +100,215 @@ const createMockLicenseWithFlags = (flags: TLicenseClaim): TCachedLicense => {
test.describe.configure({ mode: 'serial' });
// SKIPPING TEST UNTIL WE ADD A WAY TO OVERRIDE THE LICENSE FILE.
test.describe.skip('Enterprise Feature Restrictions', () => {
test.beforeAll(async () => {
// Backup any existing license file before running tests
await backupLicenseFile();
});
test.afterAll(async () => {
// Restore the backup license file after all tests complete
await restoreLicenseFile();
});
test.beforeEach(async () => {
// Clean up license file before each test to ensure clean state
await writeLicenseFile(null);
});
test.afterEach(async () => {
// Clean up license file after each test
await writeLicenseFile(null);
});
test('[ADMIN CLAIMS]: shows restricted features with asterisk when no license', async ({
page,
}) => {
// Ensure no license file exists
await writeLicenseFile(null);
const { user: adminUser } = await seedUser({
isAdmin: true,
test.describe
.skip('Enterprise Feature Restrictions', () => {
test.beforeAll(async () => {
// Backup any existing license file before running tests
await backupLicenseFile();
});
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/claims',
test.afterAll(async () => {
// Restore the backup license file after all tests complete
await restoreLicenseFile();
});
// Click Create claim button to open the dialog
await page.getByRole('button', { name: 'Create claim' }).click();
test.beforeEach(async () => {
// Clean up license file before each test to ensure clean state
await writeLicenseFile(null);
});
// Wait for dialog to open
await expect(page.getByRole('dialog')).toBeVisible();
test.afterEach(async () => {
// Clean up license file after each test
await writeLicenseFile(null);
});
// Check that enterprise features have asterisks (are restricted)
// These are the enterprise features that should be marked with *
await expect(page.getByText(/Email domains\s¹/)).toBeVisible();
await expect(page.getByText(/Embed authoring\s¹/)).toBeVisible();
await expect(page.getByText(/White label for embed authoring\s¹/)).toBeVisible();
await expect(page.getByText(/21 CFR\s¹/)).toBeVisible();
await expect(page.getByText(/Authentication portal\s¹/)).toBeVisible();
test('[ADMIN CLAIMS]: shows restricted features with asterisk when no license', async ({ page }) => {
// Ensure no license file exists
await writeLicenseFile(null);
// Check that the alert is visible
await expect(
page.getByText('Your current license does not include these features.'),
).toBeVisible();
await expect(page.getByRole('link', { name: 'Learn more' })).toBeVisible();
const { user: adminUser } = await seedUser({
isAdmin: true,
});
// Check that enterprise feature checkboxes are disabled
const emailDomainsCheckbox = page.locator('#flag-emailDomains');
await expect(emailDomainsCheckbox).toBeDisabled();
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/claims',
});
const cfr21Checkbox = page.locator('#flag-cfr21');
await expect(cfr21Checkbox).toBeDisabled();
// Click Create claim button to open the dialog
await page.getByRole('button', { name: 'Create claim' }).click();
const authPortalCheckbox = page.locator('#flag-authenticationPortal');
await expect(authPortalCheckbox).toBeDisabled();
// Wait for dialog to open
await expect(page.getByRole('dialog')).toBeVisible();
// Check that enterprise features have asterisks (are restricted)
// These are the enterprise features that should be marked with *
await expect(page.getByText(/Email domains\s¹/)).toBeVisible();
await expect(page.getByText(/Embed authoring\s¹/)).toBeVisible();
await expect(page.getByText(/White label for embed authoring\s¹/)).toBeVisible();
await expect(page.getByText(/21 CFR\s¹/)).toBeVisible();
await expect(page.getByText(/Authentication portal\s¹/)).toBeVisible();
// Check that the alert is visible
await expect(page.getByText('Your current license does not include these features.')).toBeVisible();
await expect(page.getByRole('link', { name: 'Learn more' })).toBeVisible();
// Check that enterprise feature checkboxes are disabled
const emailDomainsCheckbox = page.locator('#flag-emailDomains');
await expect(emailDomainsCheckbox).toBeDisabled();
const cfr21Checkbox = page.locator('#flag-cfr21');
await expect(cfr21Checkbox).toBeDisabled();
const authPortalCheckbox = page.locator('#flag-authenticationPortal');
await expect(authPortalCheckbox).toBeDisabled();
});
test('[ADMIN CLAIMS]: no restrictions when license has all enterprise features', async ({ page }) => {
// Create a license with ALL enterprise features enabled
await writeLicenseFile(
createMockLicenseWithFlags({
emailDomains: true,
embedAuthoring: true,
embedAuthoringWhiteLabel: true,
cfr21: true,
authenticationPortal: true,
billing: true,
}),
);
const { user: adminUser } = await seedUser({
isAdmin: true,
});
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/claims',
});
// Click Create claim button to open the dialog
await page.getByRole('button', { name: 'Create claim' }).click();
// Wait for dialog to open
await expect(page.getByRole('dialog')).toBeVisible();
// Check that enterprise features do NOT have asterisks
// They should show without the * since the license covers them
await expect(page.getByText(/Email domains\s¹/)).not.toBeVisible();
await expect(page.getByText(/Embed authoring\s¹/)).not.toBeVisible();
await expect(page.getByText(/21 CFR\s¹/)).not.toBeVisible();
await expect(page.getByText(/Authentication portal\s¹/)).not.toBeVisible();
// The plain labels should be visible (without asterisks)
await expect(page.locator('label[for="flag-emailDomains"]')).toContainText('Email domains');
await expect(page.locator('label[for="flag-cfr21"]')).toContainText('21 CFR');
// The alert should NOT be visible
await expect(page.getByText('Your current license does not include these features.')).not.toBeVisible();
// Check that enterprise feature checkboxes are enabled
const emailDomainsCheckbox = page.locator('#flag-emailDomains');
await expect(emailDomainsCheckbox).toBeEnabled();
const cfr21Checkbox = page.locator('#flag-cfr21');
await expect(cfr21Checkbox).toBeEnabled();
const authPortalCheckbox = page.locator('#flag-authenticationPortal');
await expect(authPortalCheckbox).toBeEnabled();
});
test('[ADMIN CLAIMS]: only unlicensed features show asterisk with partial license', async ({ page }) => {
// Create a license with SOME enterprise features (emailDomains and cfr21)
await writeLicenseFile(
createMockLicenseWithFlags({
emailDomains: true,
cfr21: true,
// embedAuthoring, embedAuthoringWhiteLabel, authenticationPortal are NOT included
}),
);
const { user: adminUser } = await seedUser({
isAdmin: true,
});
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/claims',
});
// Click Create claim button to open the dialog
await page.getByRole('button', { name: 'Create claim' }).click();
// Wait for dialog to open
await expect(page.getByRole('dialog')).toBeVisible();
// Features NOT in license should have asterisks
await expect(page.getByText(/Embed authoring\s¹/)).toBeVisible();
await expect(page.getByText(/White label for embed authoring\s¹/)).toBeVisible();
await expect(page.getByText(/Authentication portal\s¹/)).toBeVisible();
// Features IN license should NOT have asterisks
await expect(page.getByText(/Email domains\s¹/)).not.toBeVisible();
await expect(page.getByText(/21 CFR\s¹/)).not.toBeVisible();
// The plain labels for licensed features should be visible
await expect(page.locator('label[for="flag-emailDomains"]')).toContainText('Email domains');
await expect(page.locator('label[for="flag-cfr21"]')).toContainText('21 CFR');
// Alert should be visible since some features are restricted
await expect(page.getByText('Your current license does not include these features.')).toBeVisible();
// Licensed features should be enabled
const emailDomainsCheckbox = page.locator('#flag-emailDomains');
await expect(emailDomainsCheckbox).toBeEnabled();
const cfr21Checkbox = page.locator('#flag-cfr21');
await expect(cfr21Checkbox).toBeEnabled();
// Unlicensed features should be disabled
const embedAuthoringCheckbox = page.locator('#flag-embedAuthoring');
await expect(embedAuthoringCheckbox).toBeDisabled();
const authPortalCheckbox = page.locator('#flag-authenticationPortal');
await expect(authPortalCheckbox).toBeDisabled();
});
test('[ADMIN CLAIMS]: non-enterprise features are always enabled', async ({ page }) => {
// Ensure no license file exists
await writeLicenseFile(null);
const { user: adminUser } = await seedUser({
isAdmin: true,
});
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/claims',
});
// Click Create claim button to open the dialog
await page.getByRole('button', { name: 'Create claim' }).click();
// Wait for dialog to open
await expect(page.getByRole('dialog')).toBeVisible();
// Non-enterprise features should NOT have asterisks
await expect(page.getByText(/Unlimited documents\s¹/)).not.toBeVisible();
await expect(page.getByText(/Branding\s¹/)).not.toBeVisible();
await expect(page.getByText(/Embed signing\s¹/)).not.toBeVisible();
// Non-enterprise features should always be enabled
const unlimitedDocsCheckbox = page.locator('#flag-unlimitedDocuments');
await expect(unlimitedDocsCheckbox).toBeEnabled();
const brandingCheckbox = page.locator('#flag-allowCustomBranding');
await expect(brandingCheckbox).toBeEnabled();
const embedSigningCheckbox = page.locator('#flag-embedSigning');
await expect(embedSigningCheckbox).toBeEnabled();
});
});
test('[ADMIN CLAIMS]: no restrictions when license has all enterprise features', async ({
page,
}) => {
// Create a license with ALL enterprise features enabled
await writeLicenseFile(
createMockLicenseWithFlags({
emailDomains: true,
embedAuthoring: true,
embedAuthoringWhiteLabel: true,
cfr21: true,
authenticationPortal: true,
billing: true,
}),
);
const { user: adminUser } = await seedUser({
isAdmin: true,
});
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/claims',
});
// Click Create claim button to open the dialog
await page.getByRole('button', { name: 'Create claim' }).click();
// Wait for dialog to open
await expect(page.getByRole('dialog')).toBeVisible();
// Check that enterprise features do NOT have asterisks
// They should show without the * since the license covers them
await expect(page.getByText(/Email domains\s¹/)).not.toBeVisible();
await expect(page.getByText(/Embed authoring\s¹/)).not.toBeVisible();
await expect(page.getByText(/21 CFR\s¹/)).not.toBeVisible();
await expect(page.getByText(/Authentication portal\s¹/)).not.toBeVisible();
// The plain labels should be visible (without asterisks)
await expect(page.locator('label[for="flag-emailDomains"]')).toContainText('Email domains');
await expect(page.locator('label[for="flag-cfr21"]')).toContainText('21 CFR');
// The alert should NOT be visible
await expect(
page.getByText('Your current license does not include these features.'),
).not.toBeVisible();
// Check that enterprise feature checkboxes are enabled
const emailDomainsCheckbox = page.locator('#flag-emailDomains');
await expect(emailDomainsCheckbox).toBeEnabled();
const cfr21Checkbox = page.locator('#flag-cfr21');
await expect(cfr21Checkbox).toBeEnabled();
const authPortalCheckbox = page.locator('#flag-authenticationPortal');
await expect(authPortalCheckbox).toBeEnabled();
});
test('[ADMIN CLAIMS]: only unlicensed features show asterisk with partial license', async ({
page,
}) => {
// Create a license with SOME enterprise features (emailDomains and cfr21)
await writeLicenseFile(
createMockLicenseWithFlags({
emailDomains: true,
cfr21: true,
// embedAuthoring, embedAuthoringWhiteLabel, authenticationPortal are NOT included
}),
);
const { user: adminUser } = await seedUser({
isAdmin: true,
});
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/claims',
});
// Click Create claim button to open the dialog
await page.getByRole('button', { name: 'Create claim' }).click();
// Wait for dialog to open
await expect(page.getByRole('dialog')).toBeVisible();
// Features NOT in license should have asterisks
await expect(page.getByText(/Embed authoring\s¹/)).toBeVisible();
await expect(page.getByText(/White label for embed authoring\s¹/)).toBeVisible();
await expect(page.getByText(/Authentication portal\s¹/)).toBeVisible();
// Features IN license should NOT have asterisks
await expect(page.getByText(/Email domains\s¹/)).not.toBeVisible();
await expect(page.getByText(/21 CFR\s¹/)).not.toBeVisible();
// The plain labels for licensed features should be visible
await expect(page.locator('label[for="flag-emailDomains"]')).toContainText('Email domains');
await expect(page.locator('label[for="flag-cfr21"]')).toContainText('21 CFR');
// Alert should be visible since some features are restricted
await expect(
page.getByText('Your current license does not include these features.'),
).toBeVisible();
// Licensed features should be enabled
const emailDomainsCheckbox = page.locator('#flag-emailDomains');
await expect(emailDomainsCheckbox).toBeEnabled();
const cfr21Checkbox = page.locator('#flag-cfr21');
await expect(cfr21Checkbox).toBeEnabled();
// Unlicensed features should be disabled
const embedAuthoringCheckbox = page.locator('#flag-embedAuthoring');
await expect(embedAuthoringCheckbox).toBeDisabled();
const authPortalCheckbox = page.locator('#flag-authenticationPortal');
await expect(authPortalCheckbox).toBeDisabled();
});
test('[ADMIN CLAIMS]: non-enterprise features are always enabled', async ({ page }) => {
// Ensure no license file exists
await writeLicenseFile(null);
const { user: adminUser } = await seedUser({
isAdmin: true,
});
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/claims',
});
// Click Create claim button to open the dialog
await page.getByRole('button', { name: 'Create claim' }).click();
// Wait for dialog to open
await expect(page.getByRole('dialog')).toBeVisible();
// Non-enterprise features should NOT have asterisks
await expect(page.getByText(/Unlimited documents\s¹/)).not.toBeVisible();
await expect(page.getByText(/Branding\s¹/)).not.toBeVisible();
await expect(page.getByText(/Embed signing\s¹/)).not.toBeVisible();
// Non-enterprise features should always be enabled
const unlimitedDocsCheckbox = page.locator('#flag-unlimitedDocuments');
await expect(unlimitedDocsCheckbox).toBeEnabled();
const brandingCheckbox = page.locator('#flag-allowCustomBranding');
await expect(brandingCheckbox).toBeEnabled();
const embedSigningCheckbox = page.locator('#flag-embedSigning');
await expect(embedSigningCheckbox).toBeEnabled();
});
});
@@ -1,9 +1,8 @@
import { expect, test } from '@playwright/test';
import fs from 'node:fs/promises';
import path from 'node:path';
import type { TCachedLicense } from '@documenso/lib/types/license';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -116,277 +115,250 @@ const createMockUnauthorizedWithoutLicense = (): TCachedLicense => {
test.describe.configure({ mode: 'serial' });
// SKIPPING TEST UNTIL WE ADD A WAY TO OVERRIDE THE LICENSE FILE.
test.describe.skip('License Status Banner', () => {
test.beforeAll(async () => {
// Backup any existing license file before running tests
await backupLicenseFile();
});
test.afterAll(async () => {
// Restore the backup license file after all tests complete
await restoreLicenseFile();
});
test.beforeEach(async () => {
// Clean up license file before each test to ensure clean state
await writeLicenseFile(null);
});
test.afterEach(async () => {
// Clean up license file after each test
await writeLicenseFile(null);
});
test('[ADMIN]: no banner when license file is missing', async ({ page }) => {
// Ensure no license file exists BEFORE any page loads
await writeLicenseFile(null);
const { user: adminUser } = await seedUser({
isAdmin: true,
test.describe
.skip('License Status Banner', () => {
test.beforeAll(async () => {
// Backup any existing license file before running tests
await backupLicenseFile();
});
// Navigate to admin page - license is read during page load
await apiSignin({
test.afterAll(async () => {
// Restore the backup license file after all tests complete
await restoreLicenseFile();
});
test.beforeEach(async () => {
// Clean up license file before each test to ensure clean state
await writeLicenseFile(null);
});
test.afterEach(async () => {
// Clean up license file after each test
await writeLicenseFile(null);
});
test('[ADMIN]: no banner when license file is missing', async ({ page }) => {
// Ensure no license file exists BEFORE any page loads
await writeLicenseFile(null);
const { user: adminUser } = await seedUser({
isAdmin: true,
});
// Navigate to admin page - license is read during page load
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin',
});
// Verify we're on the admin page
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
// Global banner should not be visible (no license file)
await expect(page.getByText('This is an expired license instance of Documenso')).not.toBeVisible();
// Admin banner messages should not be visible (no license file means no banner)
await expect(page.getByText('License payment overdue')).not.toBeVisible();
await expect(page.getByText('License expired')).not.toBeVisible();
await expect(page.getByText('Invalid License Type')).not.toBeVisible();
await expect(page.getByText('Missing License')).not.toBeVisible();
});
test('[ADMIN]: no banner when license is ACTIVE', async ({ page }) => {
// Create an ACTIVE license BEFORE any page loads
await writeLicenseFile(createMockLicense('ACTIVE', false));
const { user: adminUser } = await seedUser({
isAdmin: true,
});
// Navigate to admin page - license is read during page load
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin',
});
// Verify we're on the admin page
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
// Global banner should not be visible (license is ACTIVE)
await expect(page.getByText('This is an expired license instance of Documenso')).not.toBeVisible();
// Admin banner messages should not be visible (license is ACTIVE)
await expect(page.getByText('License payment overdue')).not.toBeVisible();
await expect(page.getByText('License expired')).not.toBeVisible();
await expect(page.getByText('Invalid License Type')).not.toBeVisible();
});
test('[ADMIN]: admin banner shows PAST_DUE warning', async ({ page }) => {
// Create a PAST_DUE license BEFORE any page loads
await writeLicenseFile(createMockLicense('PAST_DUE', false));
const { user: adminUser } = await seedUser({
isAdmin: true,
});
// Navigate to admin page - license is read during page load
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin',
});
// Verify we're on the admin page
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
// Global banner should NOT be visible (only shows for EXPIRED + unauthorized)
await expect(page.getByText('This is an expired license instance of Documenso')).not.toBeVisible();
// Admin banner should show PAST_DUE message
await expect(page.getByText('License payment overdue')).toBeVisible();
await expect(page.getByText('Please update your payment to avoid service disruptions.')).toBeVisible();
// Should have the "See Documentation" link
await expect(page.getByRole('link', { name: 'See Documentation' })).toBeVisible();
});
test('[ADMIN]: admin banner shows EXPIRED error', async ({ page }) => {
// Create an EXPIRED license WITHOUT unauthorized usage BEFORE any page loads
await writeLicenseFile(createMockLicense('EXPIRED', false));
const { user: adminUser } = await seedUser({
isAdmin: true,
});
// Navigate to admin page - license is read during page load
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin',
});
// Verify we're on the admin page
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
// Global banner should NOT be visible (requires BOTH expired AND unauthorized)
await expect(page.getByText('This is an expired license instance of Documenso')).not.toBeVisible();
// Admin banner should show EXPIRED message
await expect(page.getByText('License expired')).toBeVisible();
await expect(page.getByText('Please renew your license to continue using enterprise features.')).toBeVisible();
// Should have the "See Documentation" link
await expect(page.getByRole('link', { name: 'See Documentation' })).toBeVisible();
});
test.skip('[ADMIN]: global banner shows when EXPIRED with unauthorized usage', async ({ page }) => {
// Create an EXPIRED license WITH unauthorized usage BEFORE any page loads
await writeLicenseFile(createMockLicense('EXPIRED', true));
const { user: adminUser } = await seedUser({
isAdmin: true,
});
// Navigate to admin page - license is read during page load
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin',
});
// Verify we're on the admin page
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
// Global banner SHOULD be visible (EXPIRED + unauthorized)
await expect(page.getByText('This is an expired license instance of Documenso')).toBeVisible();
// Admin banner should show UNAUTHORIZED message (takes precedence over EXPIRED)
await expect(page.getByText('Invalid License Type')).toBeVisible();
await expect(
page.getByText('Your Documenso instance is using features that are not part of your license.'),
).toBeVisible();
});
test('[ADMIN]: admin banner shows UNAUTHORIZED when flags are misused with license', async ({ page }) => {
// Create an ACTIVE license but WITH unauthorized flag usage BEFORE any page loads
await writeLicenseFile(createMockLicense('ACTIVE', true));
const { user: adminUser } = await seedUser({
isAdmin: true,
});
// Navigate to admin page - license is read during page load
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin',
});
// Verify we're on the admin page
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
// Global banner should NOT be visible (requires EXPIRED status)
await expect(page.getByText('This is an expired license instance of Documenso')).not.toBeVisible();
// Admin banner should show UNAUTHORIZED message
await expect(page.getByText('Invalid License Type')).toBeVisible();
await expect(
page.getByText('Your Documenso instance is using features that are not part of your license.'),
).toBeVisible();
// Should have the "See Documentation" link
await expect(page.getByRole('link', { name: 'See Documentation' })).toBeVisible();
});
test('[ADMIN]: admin banner shows Invalid License Type when unauthorized without license data', async ({
page,
email: adminUser.email,
redirectPath: '/admin',
}) => {
// Create a license file with unauthorized flag but no license data BEFORE any page loads
// Note: Even without license data, the banner shows "Invalid License Type" because the
// license file exists (just with license: null). The "Missing License" message would only
// show if the entire license prop was null, which doesn't happen with a valid file.
await writeLicenseFile(createMockUnauthorizedWithoutLicense());
const { user: adminUser } = await seedUser({
isAdmin: true,
});
// Navigate to admin page - license is read during page load
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin',
});
// Verify we're on the admin page
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
// Global banner should NOT be visible (no EXPIRED status, only unauthorized flag)
await expect(page.getByText('This is an expired license instance of Documenso')).not.toBeVisible();
// Admin banner should show Invalid License Type message (unauthorized flag is set)
await expect(page.getByText('Invalid License Type')).toBeVisible();
await expect(
page.getByText('Your Documenso instance is using features that are not part of your license.'),
).toBeVisible();
// Should have the "See Documentation" link
await expect(page.getByRole('link', { name: 'See Documentation' })).toBeVisible();
});
// Verify we're on the admin page
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
test.skip('[ADMIN]: global banner visible on non-admin pages when EXPIRED with unauthorized', async ({ page }) => {
// Create an EXPIRED license WITH unauthorized usage BEFORE any page loads
await writeLicenseFile(createMockLicense('EXPIRED', true));
// Global banner should not be visible (no license file)
await expect(
page.getByText('This is an expired license instance of Documenso'),
).not.toBeVisible();
const { user } = await seedUser();
// Admin banner messages should not be visible (no license file means no banner)
await expect(page.getByText('License payment overdue')).not.toBeVisible();
await expect(page.getByText('License expired')).not.toBeVisible();
await expect(page.getByText('Invalid License Type')).not.toBeVisible();
await expect(page.getByText('Missing License')).not.toBeVisible();
// Navigate to documents page - license is read during page load
await apiSignin({
page,
email: user.email,
redirectPath: '/documents',
});
// Global banner SHOULD be visible on any authenticated page (EXPIRED + unauthorized)
await expect(page.getByText('This is an expired license instance of Documenso')).toBeVisible();
});
});
test('[ADMIN]: no banner when license is ACTIVE', async ({ page }) => {
// Create an ACTIVE license BEFORE any page loads
await writeLicenseFile(createMockLicense('ACTIVE', false));
const { user: adminUser } = await seedUser({
isAdmin: true,
});
// Navigate to admin page - license is read during page load
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin',
});
// Verify we're on the admin page
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
// Global banner should not be visible (license is ACTIVE)
await expect(
page.getByText('This is an expired license instance of Documenso'),
).not.toBeVisible();
// Admin banner messages should not be visible (license is ACTIVE)
await expect(page.getByText('License payment overdue')).not.toBeVisible();
await expect(page.getByText('License expired')).not.toBeVisible();
await expect(page.getByText('Invalid License Type')).not.toBeVisible();
});
test('[ADMIN]: admin banner shows PAST_DUE warning', async ({ page }) => {
// Create a PAST_DUE license BEFORE any page loads
await writeLicenseFile(createMockLicense('PAST_DUE', false));
const { user: adminUser } = await seedUser({
isAdmin: true,
});
// Navigate to admin page - license is read during page load
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin',
});
// Verify we're on the admin page
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
// Global banner should NOT be visible (only shows for EXPIRED + unauthorized)
await expect(
page.getByText('This is an expired license instance of Documenso'),
).not.toBeVisible();
// Admin banner should show PAST_DUE message
await expect(page.getByText('License payment overdue')).toBeVisible();
await expect(
page.getByText('Please update your payment to avoid service disruptions.'),
).toBeVisible();
// Should have the "See Documentation" link
await expect(page.getByRole('link', { name: 'See Documentation' })).toBeVisible();
});
test('[ADMIN]: admin banner shows EXPIRED error', async ({ page }) => {
// Create an EXPIRED license WITHOUT unauthorized usage BEFORE any page loads
await writeLicenseFile(createMockLicense('EXPIRED', false));
const { user: adminUser } = await seedUser({
isAdmin: true,
});
// Navigate to admin page - license is read during page load
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin',
});
// Verify we're on the admin page
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
// Global banner should NOT be visible (requires BOTH expired AND unauthorized)
await expect(
page.getByText('This is an expired license instance of Documenso'),
).not.toBeVisible();
// Admin banner should show EXPIRED message
await expect(page.getByText('License expired')).toBeVisible();
await expect(
page.getByText('Please renew your license to continue using enterprise features.'),
).toBeVisible();
// Should have the "See Documentation" link
await expect(page.getByRole('link', { name: 'See Documentation' })).toBeVisible();
});
test.skip('[ADMIN]: global banner shows when EXPIRED with unauthorized usage', async ({
page,
}) => {
// Create an EXPIRED license WITH unauthorized usage BEFORE any page loads
await writeLicenseFile(createMockLicense('EXPIRED', true));
const { user: adminUser } = await seedUser({
isAdmin: true,
});
// Navigate to admin page - license is read during page load
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin',
});
// Verify we're on the admin page
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
// Global banner SHOULD be visible (EXPIRED + unauthorized)
await expect(page.getByText('This is an expired license instance of Documenso')).toBeVisible();
// Admin banner should show UNAUTHORIZED message (takes precedence over EXPIRED)
await expect(page.getByText('Invalid License Type')).toBeVisible();
await expect(
page.getByText(
'Your Documenso instance is using features that are not part of your license.',
),
).toBeVisible();
});
test('[ADMIN]: admin banner shows UNAUTHORIZED when flags are misused with license', async ({
page,
}) => {
// Create an ACTIVE license but WITH unauthorized flag usage BEFORE any page loads
await writeLicenseFile(createMockLicense('ACTIVE', true));
const { user: adminUser } = await seedUser({
isAdmin: true,
});
// Navigate to admin page - license is read during page load
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin',
});
// Verify we're on the admin page
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
// Global banner should NOT be visible (requires EXPIRED status)
await expect(
page.getByText('This is an expired license instance of Documenso'),
).not.toBeVisible();
// Admin banner should show UNAUTHORIZED message
await expect(page.getByText('Invalid License Type')).toBeVisible();
await expect(
page.getByText(
'Your Documenso instance is using features that are not part of your license.',
),
).toBeVisible();
// Should have the "See Documentation" link
await expect(page.getByRole('link', { name: 'See Documentation' })).toBeVisible();
});
test('[ADMIN]: admin banner shows Invalid License Type when unauthorized without license data', async ({
page,
}) => {
// Create a license file with unauthorized flag but no license data BEFORE any page loads
// Note: Even without license data, the banner shows "Invalid License Type" because the
// license file exists (just with license: null). The "Missing License" message would only
// show if the entire license prop was null, which doesn't happen with a valid file.
await writeLicenseFile(createMockUnauthorizedWithoutLicense());
const { user: adminUser } = await seedUser({
isAdmin: true,
});
// Navigate to admin page - license is read during page load
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin',
});
// Verify we're on the admin page
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
// Global banner should NOT be visible (no EXPIRED status, only unauthorized flag)
await expect(
page.getByText('This is an expired license instance of Documenso'),
).not.toBeVisible();
// Admin banner should show Invalid License Type message (unauthorized flag is set)
await expect(page.getByText('Invalid License Type')).toBeVisible();
await expect(
page.getByText(
'Your Documenso instance is using features that are not part of your license.',
),
).toBeVisible();
// Should have the "See Documentation" link
await expect(page.getByRole('link', { name: 'See Documentation' })).toBeVisible();
});
test.skip('[ADMIN]: global banner visible on non-admin pages when EXPIRED with unauthorized', async ({
page,
}) => {
// Create an EXPIRED license WITH unauthorized usage BEFORE any page loads
await writeLicenseFile(createMockLicense('EXPIRED', true));
const { user } = await seedUser();
// Navigate to documents page - license is read during page load
await apiSignin({
page,
email: user.email,
redirectPath: '/documents',
});
// Global banner SHOULD be visible on any authenticated page (EXPIRED + unauthorized)
await expect(page.getByText('This is an expired license instance of Documenso')).toBeVisible();
});
});
@@ -1,16 +1,11 @@
import { expect, test } from '@playwright/test';
import { createTeam } from '@documenso/lib/server-only/team/create-team';
import { nanoid } from '@documenso/lib/universal/id';
import { seedOrganisationMembers } from '@documenso/prisma/seed/organisations';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin, apiSignout } from '../fixtures/authentication';
import {
expectTextToBeVisible,
expectTextToNotBeVisible,
openDropdownMenu,
} from '../fixtures/generic';
import { expectTextToBeVisible, expectTextToNotBeVisible, openDropdownMenu } from '../fixtures/generic';
test('[ORGANISATIONS]: create and delete organisation', async ({ page }) => {
const { user, organisation } = await seedUser({
@@ -29,9 +24,7 @@ test('[ORGANISATIONS]: create and delete organisation', async ({ page }) => {
await page.waitForURL(`/o/${organisation.url}/settings/general`);
await page.getByRole('button', { name: 'Delete' }).click();
await page
.getByLabel(`Confirm by typing delete ${organisation.name}`)
.fill(`delete ${organisation.name}`);
await page.getByLabel(`Confirm by typing delete ${organisation.name}`).fill(`delete ${organisation.name}`);
await page.getByRole('button', { name: 'Delete' }).click();
await page.waitForURL(`/settings/organisations`);
@@ -138,15 +131,9 @@ test('[ORGANISATIONS]: inherit members', async ({ page }) => {
// Check from admin POV that member counts are correct
// You should only see the manager/admins from the organisation in this table.
await expect(
page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(managerEmail),
).toBeVisible();
await expect(
page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(adminEmail),
).toBeVisible();
await expect(
page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(ownerEmail),
).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(managerEmail)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(adminEmail)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(ownerEmail)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: memberEmail })).not.toBeVisible();
await expect(page.getByRole('row').filter({ hasText: memberEmail2 })).not.toBeVisible();
await expect(page.getByRole('row').filter({ hasText: memberEmail3 })).not.toBeVisible();
@@ -157,31 +144,17 @@ test('[ORGANISATIONS]: inherit members', async ({ page }) => {
await page.getByRole('option', { name: 'Member 1' }).first().click();
await page.getByRole('button', { name: 'Next' }).click();
await page.getByRole('button', { name: 'Add Members' }).click();
await expect(
page.getByRole('row').filter({ hasText: 'Team Member' }).getByText(memberEmail),
).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Member' }).getByText(memberEmail)).toBeVisible();
await page.goto(`/t/${teamWithInheritMembers.url}/settings/members`);
// Check from member POV that member counts are correct for inherit members team.
await expect(
page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(managerEmail),
).toBeVisible();
await expect(
page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(adminEmail),
).toBeVisible();
await expect(
page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(ownerEmail),
).toBeVisible();
await expect(
page.getByRole('row').filter({ hasText: 'Team Member' }).getByText(memberEmail),
).toBeVisible();
await expect(
page.getByRole('row').filter({ hasText: 'Team Member' }).getByText(memberEmail2),
).toBeVisible();
await expect(
page.getByRole('row').filter({ hasText: 'Team Member' }).getByText(memberEmail3),
).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(managerEmail)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(adminEmail)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(ownerEmail)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Member' }).getByText(memberEmail)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Member' }).getByText(memberEmail2)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Member' }).getByText(memberEmail3)).toBeVisible();
// Disable inherit mode.
await page.goto(`/t/${teamWithInheritMembers.url}/settings/groups`);
@@ -191,15 +164,9 @@ test('[ORGANISATIONS]: inherit members', async ({ page }) => {
// Expect the inherited members to disappear
await page.goto(`/t/${teamWithInheritMembers.url}/settings/members`);
await expect(
page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(managerEmail),
).toBeVisible();
await expect(
page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(adminEmail),
).toBeVisible();
await expect(
page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(ownerEmail),
).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(managerEmail)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(adminEmail)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Admin' }).getByText(ownerEmail)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: memberEmail })).not.toBeVisible();
await expect(page.getByRole('row').filter({ hasText: memberEmail2 })).not.toBeVisible();
await expect(page.getByRole('row').filter({ hasText: memberEmail3 })).not.toBeVisible();
@@ -324,15 +291,9 @@ test('[ORGANISATIONS]: manage groups and members', async ({ page }) => {
await page.goto(`/o/${organisation.url}/settings/members`);
// Confirm org roles have been applied to these members.
await expect(
page.getByRole('row').filter({ hasText: 'Organisation Admin' }).getByText(memberEmail1),
).toBeVisible();
await expect(
page.getByRole('row').filter({ hasText: 'Organisation Admin' }).getByText(memberEmail2),
).toBeVisible();
await expect(
page.getByRole('row').filter({ hasText: 'Organisation Admin' }).getByText(memberEmail3),
).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Organisation Admin' }).getByText(memberEmail1)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Organisation Admin' }).getByText(memberEmail2)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Organisation Admin' }).getByText(memberEmail3)).toBeVisible();
// Test updating the group.
await page.goto(`/o/${organisation.url}/settings/groups`);
@@ -366,15 +327,9 @@ test('[ORGANISATIONS]: manage groups and members', async ({ page }) => {
await page.goto(`/o/${organisation.url}/settings/members`);
// Confirm admins still get admin roles.
await expect(
page.getByRole('row').filter({ hasText: 'Organisation Admin' }).getByText(adminEmail1),
).toBeVisible();
await expect(
page.getByRole('row').filter({ hasText: 'Organisation Admin' }).getByText(adminEmail2),
).toBeVisible();
await expect(
page.getByRole('row').filter({ hasText: 'Organisation Admin' }).getByText(adminEmail3),
).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Organisation Admin' }).getByText(adminEmail1)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Organisation Admin' }).getByText(adminEmail2)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Organisation Admin' }).getByText(adminEmail3)).toBeVisible();
// Create another custom group with 3 members with "ORGANISATION MEMBER" role.
await page.goto(`/o/${organisation.url}/settings/groups`);
@@ -440,12 +395,8 @@ test('[ORGANISATIONS]: manage groups and members', async ({ page }) => {
// Navigate to team members and validate members are there.
await page.goto(`/t/${teamA}/settings/members`);
await expect(
page.getByRole('row').filter({ hasText: 'Team Manager' }).getByText(memberEmail1),
).toBeVisible();
await expect(
page.getByRole('row').filter({ hasText: 'Team Manager' }).getByText(memberEmail2),
).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Manager' }).getByText(memberEmail1)).toBeVisible();
await expect(page.getByRole('row').filter({ hasText: 'Team Manager' }).getByText(memberEmail2)).toBeVisible();
// Member 1 should see inherit team and teamA
await apiSignout({ page });
@@ -586,8 +537,6 @@ test('[ORGANISATIONS]: leave organisation', async ({ page }) => {
await page.getByRole('button', { name: 'Leave' }).click();
await page.getByRole('button', { name: 'Leave' }).click();
await expect(
page.getByText('You have successfully left this organisation').first(),
).toBeVisible();
await expect(page.getByText('You have successfully left this organisation').first()).toBeVisible();
await expect(page.getByText('No results found').first()).toBeVisible();
});
@@ -1,10 +1,9 @@
import { expect, test } from '@playwright/test';
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
import { prisma } from '@documenso/prisma';
import { DocumentVisibility } from '@documenso/prisma/client';
import { seedTeamDocumentWithMeta } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -206,12 +205,8 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
// Update email document settings by enabling/disabling some checkboxes
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('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();
@@ -247,12 +242,8 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
// Update some email settings
await page.getByRole('checkbox', { name: 'Email recipients with a signing request' }).uncheck();
await page
.getByRole('checkbox', { name: 'Email recipients when the document is completed', exact: true })
.uncheck();
await page
.getByRole('checkbox', { name: 'Email the owner when the document is completed' })
.uncheck();
await page.getByRole('checkbox', { name: 'Email recipients when the document is completed', exact: true }).uncheck();
await page.getByRole('checkbox', { name: 'Email the owner when the document is completed' }).uncheck();
await page.getByRole('button', { name: 'Update' }).first().click();
await expect(page.getByText('Your email preferences have been updated').first()).toBeVisible();
@@ -1,15 +1,9 @@
import { expect, test } from '@playwright/test';
import { FieldType } from '@prisma/client';
import path from 'node:path';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/create-embedding-presign-token';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { prefixedId } from '@documenso/lib/universal/id';
import {
mapSecondaryIdToDocumentId,
mapSecondaryIdToTemplateId,
} from '@documenso/lib/utils/envelope';
import { mapSecondaryIdToDocumentId, mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
import { prisma } from '@documenso/prisma';
import {
@@ -19,6 +13,8 @@ import {
} from '@documenso/prisma/seed/documents';
import { seedBlankTemplate, seedDirectTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { FieldType } from '@prisma/client';
import { apiSignin } from '../fixtures/authentication';
@@ -118,14 +114,13 @@ test.describe('PDF Viewer Rendering', () => {
fields: [FieldType.SIGNATURE],
});
const { document: documentV2, recipients: recipientsV2 } =
await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: ['signer-v2@test.documenso.com'],
fields: [FieldType.SIGNATURE],
updateDocumentOptions: { internalVersion: 2 },
});
const { document: documentV2, recipients: recipientsV2 } = await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: ['signer-v2@test.documenso.com'],
fields: [FieldType.SIGNATURE],
updateDocumentOptions: { internalVersion: 2 },
});
await addSecondEnvelopeItem(documentV2.id);
await page.goto(`/sign/${recipientsV1[0].token}`);
@@ -173,24 +168,14 @@ test.describe('PDF Viewer Rendering', () => {
const qrTokenV1 = prefixedId('qr');
const qrTokenV2 = prefixedId('qr');
const documentV1 = await seedCompletedDocument(
user,
team.id,
['share-v1@test.documenso.com'],
{
createDocumentOptions: { qrToken: qrTokenV1 },
},
);
const documentV1 = await seedCompletedDocument(user, team.id, ['share-v1@test.documenso.com'], {
createDocumentOptions: { qrToken: qrTokenV1 },
});
const documentV2 = await seedCompletedDocument(
user,
team.id,
['share-v2@test.documenso.com'],
{
createDocumentOptions: { qrToken: qrTokenV2 },
internalVersion: 2,
},
);
const documentV2 = await seedCompletedDocument(user, team.id, ['share-v2@test.documenso.com'], {
createDocumentOptions: { qrToken: qrTokenV2 },
internalVersion: 2,
});
await addSecondEnvelopeItem(documentV2.id);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/share/${qrTokenV1}`);
@@ -224,14 +209,13 @@ test.describe('PDF Viewer Rendering', () => {
fields: [FieldType.SIGNATURE],
});
const { document: documentV2, recipients: recipientsV2 } =
await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: ['embed-signer-v2@test.documenso.com'],
fields: [FieldType.SIGNATURE],
updateDocumentOptions: { internalVersion: 2 },
});
const { document: documentV2, recipients: recipientsV2 } = await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: ['embed-signer-v2@test.documenso.com'],
fields: [FieldType.SIGNATURE],
updateDocumentOptions: { internalVersion: 2 },
});
await addSecondEnvelopeItem(documentV2.id);
await page.goto(`/embed/sign/${recipientsV1[0].token}`);
@@ -281,14 +265,13 @@ test.describe('PDF Viewer Rendering', () => {
fields: [FieldType.SIGNATURE],
});
const { document: documentV2, recipients: recipientsV2 } =
await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: ['multisign-v2@test.documenso.com'],
fields: [FieldType.SIGNATURE],
updateDocumentOptions: { internalVersion: 2 },
});
const { document: documentV2, recipients: recipientsV2 } = await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: ['multisign-v2@test.documenso.com'],
fields: [FieldType.SIGNATURE],
updateDocumentOptions: { internalVersion: 2 },
});
await addSecondEnvelopeItem(documentV2.id);
await page.goto(`/embed/v1/multisign?token=${recipientsV1[0].token}`);
@@ -325,9 +308,7 @@ test.describe('PDF Viewer Rendering', () => {
const embedParams = { darkModeDisabled: false, features: {} };
const hash = btoa(encodeURIComponent(JSON.stringify(embedParams)));
await page.goto(
`${NEXT_PUBLIC_WEBAPP_URL()}/embed/v1/authoring/document/create?token=${presignToken}#${hash}`,
);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/embed/v1/authoring/document/create?token=${presignToken}#${hash}`);
await expect(page.getByText('Configure Document')).toBeVisible({ timeout: 15_000 });
@@ -1,9 +1,8 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { prisma } from '@documenso/prisma';
import { seedDirectTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
import { expectToastTextToBeVisible } from '../fixtures/generic';
@@ -31,9 +30,7 @@ test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
await page.getByRole('button', { name: 'Update' }).click();
await expect(page.getByRole('status').first()).toContainText(
'Your public profile has been updated.',
);
await expect(page.getByRole('status').first()).toContainText('Your public profile has been updated.');
// Link direct template to public profile.
await page.getByRole('button', { name: 'Link template' }).click();
@@ -41,9 +38,7 @@ test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('textbox', { name: 'Title *' }).fill('public-direct-template-title');
await page
.getByRole('textbox', { name: 'Description *' })
.fill('public-direct-template-description');
await page.getByRole('textbox', { name: 'Description *' }).fill('public-direct-template-description');
await page.getByRole('button', { name: 'Update' }).click();
// Wait for toast
@@ -1,8 +1,5 @@
import { PDF } from '@libpdf/core';
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 { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
@@ -14,6 +11,8 @@ import type {
TCreateEnvelopePayload,
TCreateEnvelopeResponse,
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
import { PDF } from '@libpdf/core';
import { expect, test } from '@playwright/test';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
const baseUrl = `${WEBAPP_BASE_URL}/api/v2-beta`;
@@ -68,10 +67,7 @@ async function getPdfFormFieldNames(pdfBuffer: Uint8Array): Promise<string[]> {
/**
* Helper to get the value of a text field in a PDF.
*/
async function getPdfTextFieldValue(
pdfBuffer: Uint8Array,
fieldName: string,
): Promise<string | undefined> {
async function getPdfTextFieldValue(pdfBuffer: Uint8Array, fieldName: string): Promise<string | undefined> {
const pdfDoc = await PDF.load(new Uint8Array(pdfBuffer));
const form = await pdfDoc.getForm();
@@ -94,14 +90,10 @@ test.describe.configure({
});
test.describe('Form Flattening', () => {
const formFieldsPdf = fs.readFileSync(
path.join(__dirname, '../../../../assets/form-fields-test.pdf'),
);
const formFieldsPdf = fs.readFileSync(path.join(__dirname, '../../../../assets/form-fields-test.pdf'));
test.describe('Envelope Creation (DOCUMENT type)', () => {
test('should flatten form fields when creating a DOCUMENT envelope with formValues', async ({
request,
}) => {
test('should flatten form fields when creating a DOCUMENT envelope with formValues', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
@@ -119,10 +111,7 @@ test.describe('Form Flattening', () => {
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
formData.append('files', new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }));
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
@@ -156,9 +145,7 @@ test.describe('Form Flattening', () => {
expect(hasFormFields).toBe(false);
});
test('should flatten form fields when creating a DOCUMENT envelope without formValues', async ({
request,
}) => {
test('should flatten form fields when creating a DOCUMENT envelope without formValues', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
@@ -176,10 +163,7 @@ test.describe('Form Flattening', () => {
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
formData.append('files', new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }));
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
@@ -211,9 +195,7 @@ test.describe('Form Flattening', () => {
});
test.describe('Template Creation (TEMPLATE type)', () => {
test('should NOT flatten form fields when creating a TEMPLATE envelope', async ({
request,
}) => {
test('should NOT flatten form fields when creating a TEMPLATE envelope', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
@@ -231,10 +213,7 @@ test.describe('Form Flattening', () => {
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
formData.append('files', new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }));
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
@@ -274,9 +253,7 @@ test.describe('Form Flattening', () => {
expect(fieldNames).toContain(FORM_FIELDS.DROPDOWN);
});
test('should preserve form fields in template even when formValues are provided', async ({
request,
}) => {
test('should preserve form fields in template even when formValues are provided', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
@@ -294,10 +271,7 @@ test.describe('Form Flattening', () => {
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
formData.append('files', new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }));
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
@@ -336,9 +310,7 @@ test.describe('Form Flattening', () => {
});
test.describe('Document from Template', () => {
test('should flatten form fields when creating document from template with formValues', async ({
request,
}) => {
test('should flatten form fields when creating document from template with formValues', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
@@ -362,10 +334,7 @@ test.describe('Form Flattening', () => {
const templateFormData = new FormData();
templateFormData.append('payload', JSON.stringify(templatePayload));
templateFormData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
templateFormData.append('files', new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }));
const templateRes = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
@@ -431,9 +400,7 @@ test.describe('Form Flattening', () => {
expect(hasFormFields).toBe(false);
});
test('should flatten form fields when creating document from template without formValues', async ({
request,
}) => {
test('should flatten form fields when creating document from template without formValues', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
@@ -457,10 +424,7 @@ test.describe('Form Flattening', () => {
const templateFormData = new FormData();
templateFormData.append('payload', JSON.stringify(templatePayload));
templateFormData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
templateFormData.append('files', new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }));
const templateRes = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
@@ -516,9 +480,7 @@ test.describe('Form Flattening', () => {
expect(hasFormFields).toBe(false);
});
test('should use template formValues when creating document without override', async ({
request,
}) => {
test('should use template formValues when creating document without override', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
@@ -546,10 +508,7 @@ test.describe('Form Flattening', () => {
const templateFormData = new FormData();
templateFormData.append('payload', JSON.stringify(templatePayload));
templateFormData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
templateFormData.append('files', new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }));
const templateRes = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
@@ -636,10 +595,7 @@ test.describe('Form Flattening', () => {
const templateFormData = new FormData();
templateFormData.append('payload', JSON.stringify(templatePayload));
templateFormData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
templateFormData.append('files', new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }));
const templateRes = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
@@ -773,10 +729,7 @@ test.describe('Form Flattening', () => {
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
formData.append('files', new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }));
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
@@ -822,10 +775,7 @@ test.describe('Form Flattening', () => {
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
formData.append('files', new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }));
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
@@ -1,7 +1,5 @@
import { expect, test } from '@playwright/test';
import * as fs from 'node:fs';
import * as 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';
@@ -12,6 +10,7 @@ import type {
TCreateEnvelopePayload,
TCreateEnvelopeResponse,
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -46,9 +45,7 @@ const setTeamDefaultRecipients = async (
};
test.describe('Default Recipients', () => {
test('[DEFAULT_RECIPIENTS]: default recipients are added to documents created via UI', async ({
page,
}) => {
test('[DEFAULT_RECIPIENTS]: default recipients are added to documents created via UI', async ({ page }) => {
const { team, owner } = await seedTeam({
createTeamMembers: 2,
});
@@ -142,17 +139,13 @@ test.describe('Default Recipients', () => {
expect(defaultRecipient).toBeDefined();
expect(defaultRecipient?.role).toBe(RecipientRole.CC);
const regularSigner = envelope.recipients.find(
(r) => r.email === 'regular-signer@documenso.com',
);
const regularSigner = envelope.recipients.find((r) => r.email === 'regular-signer@documenso.com');
expect(regularSigner).toBeDefined();
}).toPass();
});
// TODO: Are we intending to allow default recipients to be removed from a document?
test.skip('[DEFAULT_RECIPIENTS]: default recipients cannot be removed from a document', async ({
page,
}) => {
test.skip('[DEFAULT_RECIPIENTS]: default recipients cannot be removed from a document', async ({ page }) => {
const { team, owner } = await seedTeam({
createTeamMembers: 2,
});
@@ -245,9 +238,7 @@ test.describe('Default Recipients', () => {
await expect(removeButton).toBeDisabled();
});
test('[DEFAULT_RECIPIENTS]: documents created via API have default recipients', async ({
request,
}) => {
test('[DEFAULT_RECIPIENTS]: documents created via API have default recipients', async ({ request }) => {
const { team, owner } = await seedTeam({
createTeamMembers: 2,
});
@@ -336,9 +327,7 @@ test.describe('Default Recipients', () => {
expect(defaultRecipient?.role).toBe(RecipientRole.CC);
});
test('[DEFAULT_RECIPIENTS]: documents created from template have default recipients', async ({
page,
}) => {
test('[DEFAULT_RECIPIENTS]: documents created from template have default recipients', async ({ page }) => {
const { team, owner } = await seedTeam({
createTeamMembers: 2,
});
@@ -413,9 +402,7 @@ test.describe('Default Recipients', () => {
expect(document.recipients.length).toBe(2);
const templateRecipient = document.recipients.find(
(r) => r.email === 'template-recipient@documenso.com',
);
const templateRecipient = document.recipients.find((r) => r.email === 'template-recipient@documenso.com');
expect(templateRecipient).toBeDefined();
const defaultRecipient = document.recipients.find(
@@ -1,7 +1,6 @@
import { test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { seedUser } from '@documenso/prisma/seed/users';
import { test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
import { expectTextToBeVisible } from '../fixtures/generic';
@@ -9,10 +8,7 @@ import { expectTextToBeVisible } from '../fixtures/generic';
test('[TEAMS]: create team', async ({ page }) => {
const { user, organisation } = await seedUser();
test.skip(
process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true',
'Test skipped because billing is enabled.',
);
test.skip(process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true', 'Test skipped because billing is enabled.');
await apiSignin({
page,
@@ -1,12 +1,11 @@
import { expect, test } from '@playwright/test';
import { DocumentStatus, OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
import { generateDatabaseId } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
import { seedOrganisationMembers } from '@documenso/prisma/seed/organisations';
import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { DocumentStatus, OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
import { apiSignin, apiSignout } from '../fixtures/authentication';
import { checkDocumentTabCount } from '../fixtures/documents';
@@ -110,11 +109,7 @@ test('[TEAMS]: search respects team document visibility', async ({ page }) => {
});
test('[TEAMS]: search does not reveal documents from other teams', async ({ page }) => {
const {
team: teamA,
teamOwner: teamAOwner,
teamMember2: teamAMember,
} = await seedTeamDocuments();
const { team: teamA, teamOwner: teamAOwner, teamMember2: teamAMember } = await seedTeamDocuments();
const { team: teamB, teamOwner: teamBOwner } = await seedTeamDocuments();
await seedDocuments([
@@ -157,9 +152,7 @@ test('[TEAMS]: search does not reveal documents from other teams', async ({ page
await apiSignout({ page });
});
test('[TEAMS]: search respects recipient visibility regardless of team visibility', async ({
page,
}) => {
test('[TEAMS]: search respects recipient visibility regardless of team visibility', async ({ page }) => {
const { team, owner } = await seedTeam();
const memberUser = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER });
@@ -186,9 +179,7 @@ test('[TEAMS]: search respects recipient visibility regardless of team visibilit
await page.waitForURL(/query=Admin(%20|\+|\s)Document/);
await checkDocumentTabCount(page, 'All', 1);
await expect(
page.getByRole('link', { name: 'Admin Document with Member Recipient' }),
).toBeVisible();
await expect(page.getByRole('link', { name: 'Admin Document with Member Recipient' })).toBeVisible();
await apiSignout({ page });
});
@@ -228,9 +219,7 @@ test('[TEAMS]: search by recipient name respects visibility', async ({ page }) =
await page.waitForURL(/query=Unique(%20|\+|\s)Recipient/);
await checkDocumentTabCount(page, 'All', 1);
await expect(
page.getByRole('link', { name: 'Admin Document for Unique Recipient' }),
).toBeVisible();
await expect(page.getByRole('link', { name: 'Admin Document for Unique Recipient' })).toBeVisible();
await apiSignout({ page });
@@ -245,9 +234,7 @@ test('[TEAMS]: search by recipient name respects visibility', async ({ page }) =
await page.waitForURL(/query=Unique(%20|\+|\s)Recipient/);
await checkDocumentTabCount(page, 'All', 0);
await expect(
page.getByRole('link', { name: 'Admin Document for Unique Recipient' }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Admin Document for Unique Recipient' })).not.toBeVisible();
await apiSignout({ page });
});
@@ -1,21 +1,12 @@
import { seedBlankDocument, seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
import { seedTeam, seedTeamEmail, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@prisma/client';
import {
seedBlankDocument,
seedDocuments,
seedTeamDocuments,
} from '@documenso/prisma/seed/documents';
import { seedTeam, seedTeamEmail, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin, apiSignout } from '../fixtures/authentication';
import { checkDocumentTabCount } from '../fixtures/documents';
import {
expectTextToBeVisible,
expectToastTextToBeVisible,
openDropdownMenu,
} from '../fixtures/generic';
import { expectTextToBeVisible, expectToastTextToBeVisible, openDropdownMenu } from '../fixtures/generic';
test('[TEAMS]: check team documents count', async ({ page }) => {
const { team, teamOwner, teamMember2 } = await seedTeamDocuments();
@@ -53,11 +44,7 @@ test('[TEAMS]: check team documents count', async ({ page }) => {
test('[TEAMS]: check team documents count with internal team email', async ({ page }) => {
const { team, teamOwner, teamMember2, teamMember4 } = await seedTeamDocuments();
const {
team: team2,
teamOwner: team2Owner,
teamMember2: team2Member2,
} = await seedTeamDocuments();
const { team: team2, teamOwner: team2Owner, teamMember2: team2Member2 } = await seedTeamDocuments();
const teamEmailMember = teamMember4;
@@ -499,9 +486,7 @@ test('[TEAMS]: check document visibility based on team member role', async ({ pa
await expectTextToBeVisible(page, 'Document Visible to Admin with Recipient');
});
test('[TEAMS]: ensure document owner can see document regardless of visibility', async ({
page,
}) => {
test('[TEAMS]: ensure document owner can see document regardless of visibility', async ({ page }) => {
const { team, owner } = await seedTeam();
// Seed a member user
@@ -568,9 +553,7 @@ test('[TEAMS]: ensure recipient can see document regardless of visibility', asyn
});
// Check that the member user can see the document
await expect(
page.getByRole('link', { name: 'Admin Document with Member Recipient', exact: true }),
).toBeVisible();
await expect(page.getByRole('link', { name: 'Admin Document with Member Recipient', exact: true })).toBeVisible();
await apiSignout({ page });
});
@@ -605,16 +588,12 @@ test('[TEAMS]: check that MEMBER role cannot see ADMIN-only documents', async ({
});
// Check that the member user cannot see the ADMIN-only document
await expect(
page.getByRole('link', { name: 'Admin Only Document', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Admin Only Document', exact: true })).not.toBeVisible();
await apiSignout({ page });
});
test('[TEAMS]: check that MEMBER role cannot see MANAGER_AND_ABOVE-only documents', async ({
page,
}) => {
test('[TEAMS]: check that MEMBER role cannot see MANAGER_AND_ABOVE-only documents', async ({ page }) => {
const { team, owner } = await seedTeam();
// Seed a member user
@@ -644,9 +623,7 @@ test('[TEAMS]: check that MEMBER role cannot see MANAGER_AND_ABOVE-only document
});
// Check that the member user cannot see the ADMIN-only document
await expect(
page.getByRole('link', { name: 'Admin Only Document', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Admin Only Document', exact: true })).not.toBeVisible();
await apiSignout({ page });
});
@@ -681,9 +658,7 @@ test('[TEAMS]: check that MANAGER role cannot see ADMIN-only documents', async (
});
// Check that the manager user cannot see the ADMIN-only document
await expect(
page.getByRole('link', { name: 'Admin Only Document', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Admin Only Document', exact: true })).not.toBeVisible();
await apiSignout({ page });
});
@@ -718,9 +693,7 @@ test('[TEAMS]: check that ADMIN role can see MANAGER_AND_ABOVE documents', async
});
// Check that the admin user can see the MANAGER_AND_ABOVE document
await expect(
page.getByRole('link', { name: 'Manager and Above Document', exact: true }),
).toBeVisible();
await expect(page.getByRole('link', { name: 'Manager and Above Document', exact: true })).toBeVisible();
await apiSignout({ page });
});
@@ -757,9 +730,7 @@ test('[TEAMS]: check that ADMIN role can change document visibility', async ({ p
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText('Admins only');
});
test('[TEAMS]: check that MEMBER role cannot change visibility of EVERYONE documents', async ({
page,
}) => {
test('[TEAMS]: check that MEMBER role cannot change visibility of EVERYONE documents', async ({ page }) => {
const { team, owner } = await seedTeam();
const teamMember = await seedTeamMember({
@@ -783,9 +754,7 @@ test('[TEAMS]: check that MEMBER role cannot change visibility of EVERYONE docum
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
});
test('[TEAMS]: check that MEMBER role cannot change visibility of MANAGER_AND_ABOVE documents', async ({
page,
}) => {
test('[TEAMS]: check that MEMBER role cannot change visibility of MANAGER_AND_ABOVE documents', async ({ page }) => {
const { team, owner } = await seedTeam();
const teamMember = await seedTeamMember({
@@ -809,9 +778,7 @@ test('[TEAMS]: check that MEMBER role cannot change visibility of MANAGER_AND_AB
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
});
test('[TEAMS]: check that MEMBER role cannot change visibility of ADMIN documents', async ({
page,
}) => {
test('[TEAMS]: check that MEMBER role cannot change visibility of ADMIN documents', async ({ page }) => {
const { team, owner } = await seedTeam();
const teamMember = await seedTeamMember({
@@ -835,9 +802,7 @@ test('[TEAMS]: check that MEMBER role cannot change visibility of ADMIN document
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
});
test('[TEAMS]: check that MANAGER role cannot change visibility of ADMIN documents', async ({
page,
}) => {
test('[TEAMS]: check that MANAGER role cannot change visibility of ADMIN documents', async ({ page }) => {
const { team, owner } = await seedTeam();
const teamManager = await seedTeamMember({
@@ -863,16 +828,8 @@ test('[TEAMS]: check that MANAGER role cannot change visibility of ADMIN documen
test('[TEAMS]: users cannot see documents from other teams', async ({ page }) => {
// Seed two teams with documents
const {
team: teamA,
teamOwner: teamAOwner,
teamMember2: teamAMember,
} = await seedTeamDocuments();
const {
team: teamB,
teamOwner: teamBOwner,
teamMember2: teamBMember,
} = await seedTeamDocuments();
const { team: teamA, teamOwner: teamAOwner, teamMember2: teamAMember } = await seedTeamDocuments();
const { team: teamB, teamOwner: teamBOwner, teamMember2: teamBMember } = await seedTeamDocuments();
// Seed a document in team B
await seedDocuments([
@@ -928,9 +885,7 @@ test('[TEAMS]: personal documents are not visible in team context', async ({ pag
});
// Verify that the personal document is not visible in the team context
await expect(
page.getByRole('link', { name: 'Personal Document', exact: true }),
).not.toBeVisible();
await expect(page.getByRole('link', { name: 'Personal Document', exact: true })).not.toBeVisible();
await apiSignout({ page });
});
@@ -1,8 +1,7 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { seedTeamEmailVerification } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
import { openDropdownMenu } from '../fixtures/generic';
@@ -25,10 +24,7 @@ test('[TEAMS]: send team email request', async ({ page }) => {
await page.getByRole('button', { name: 'Add' }).click();
await expect(
page
.getByRole('status')
.filter({ hasText: 'We have sent a confirmation email for verification.' })
.first(),
page.getByRole('status').filter({ hasText: 'We have sent a confirmation email for verification.' }).first(),
).toBeVisible();
});
@@ -55,10 +51,7 @@ test('[TEAMS]: delete team email', async ({ page }) => {
redirectPath: `/t/${team.url}/settings`,
});
const settingsBtn = page
.locator('section div')
.filter({ hasText: 'Team email' })
.getByRole('button');
const settingsBtn = page.locator('section div').filter({ hasText: 'Team email' }).getByRole('button');
await openDropdownMenu(page, settingsBtn);
await expect(page.getByRole('menuitem', { name: 'Remove' })).toBeVisible();
@@ -1,15 +1,8 @@
import { expect, test } from '@playwright/test';
import {
mapSecondaryIdToDocumentId,
mapSecondaryIdToTemplateId,
} from '@documenso/lib/utils/envelope';
import { mapSecondaryIdToDocumentId, mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import {
seedTeamDocumentWithMeta,
seedTeamTemplateWithMeta,
} from '@documenso/prisma/seed/documents';
import { seedTeamDocumentWithMeta, seedTeamTemplateWithMeta } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -25,9 +18,7 @@ test('[TEAMS]: check that default team signature settings are all enabled', asyn
const document = await seedTeamDocumentWithMeta(team);
// Create a document and check the settings
await page.goto(
`/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`,
);
await page.goto(`/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`);
// Verify that the settings match
await page.getByRole('button', { name: 'Advanced Options' }).click();
@@ -1,21 +1,16 @@
import { type Page, expect, test } from '@playwright/test';
import type { Envelope, Team } from '@prisma/client';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { prisma } from '@documenso/prisma';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, type Page, test } from '@playwright/test';
import type { Envelope, Team } from '@prisma/client';
import { apiSignin } from '../fixtures/authentication';
/**
* Test helper to complete template creation with duplicate recipients
*/
const completeTemplateFlowWithDuplicateRecipients = async (options: {
page: Page;
team: Team;
template: Envelope;
}) => {
const completeTemplateFlowWithDuplicateRecipients = async (options: { page: Page; team: Team; template: Envelope }) => {
const { page, team, template } = options;
// Step 1: Settings - Continue with defaults
await page.getByRole('button', { name: 'Continue' }).click();
@@ -85,10 +80,7 @@ test.describe('[TEMPLATE_FLOW]: Duplicate Recipients', () => {
await expect(page).toHaveURL(`/t/${team.url}/templates`);
});
test('should create document from template with duplicate recipients using same email', async ({
page,
context,
}) => {
test('should create document from template with duplicate recipients using same email', async ({ page, context }) => {
const { user, team } = await seedUser();
const template = await seedBlankTemplate(user, team.id);
@@ -105,10 +97,7 @@ test.describe('[TEMPLATE_FLOW]: Duplicate Recipients', () => {
// Navigate to template and create document
await page.goto(`/t/${team.url}/templates`);
await page
.getByRole('row', { name: template.title })
.getByRole('button', { name: 'Use Template' })
.click();
await page.getByRole('row', { name: template.title }).getByRole('button', { name: 'Use Template' }).click();
// Fill recipient information with same email for both instances
await expect(page.getByRole('heading', { name: 'Create document' })).toBeVisible();
@@ -172,9 +161,7 @@ test.describe('[TEMPLATE_FLOW]: Duplicate Recipients', () => {
await expect(page.getByLabel('Full Name')).toHaveValue(recipient.name);
// Verify only one signature field is visible for this recipient
expect(
await page.locator(`[data-field-type="SIGNATURE"]:not([data-readonly="true"])`).all(),
).toHaveLength(1);
expect(await page.locator(`[data-field-type="SIGNATURE"]:not([data-readonly="true"])`).all()).toHaveLength(1);
}
});
@@ -1,11 +1,10 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -65,10 +64,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
await page
.getByTestId('field-advanced-settings-footer')
.getByRole('button', { name: 'Cancel' })
.click();
await page.getByTestId('field-advanced-settings-footer').getByRole('button', { name: 'Cancel' }).click();
await triggerAutosave(page);
@@ -99,9 +95,7 @@ test.describe('AutoSave Fields Step', () => {
expect(fields.length).toBe(3);
expect(fields.map((field) => field.type).toSorted()).toEqual(
['SIGNATURE', 'TEXT', 'SIGNATURE'].toSorted(),
);
expect(fields.map((field) => field.type).toSorted()).toEqual(['SIGNATURE', 'TEXT', 'SIGNATURE'].toSorted());
}).toPass();
});
@@ -128,10 +122,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
await page
.getByTestId('field-advanced-settings-footer')
.getByRole('button', { name: 'Cancel' })
.click();
await page.getByTestId('field-advanced-settings-footer').getByRole('button', { name: 'Cancel' }).click();
await triggerAutosave(page);
@@ -197,10 +188,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
await page
.getByTestId('field-advanced-settings-footer')
.getByRole('button', { name: 'Cancel' })
.click();
await page.getByTestId('field-advanced-settings-footer').getByRole('button', { name: 'Cancel' }).click();
await triggerAutosave(page);
@@ -272,10 +260,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
await page
.getByTestId('field-advanced-settings-footer')
.getByRole('button', { name: 'Save' })
.click();
await page.getByTestId('field-advanced-settings-footer').getByRole('button', { name: 'Save' }).click();
await page.waitForTimeout(2500);
await triggerAutosave(page);
@@ -293,9 +278,7 @@ test.describe('AutoSave Fields Step', () => {
const fields = retrievedTemplate.fields;
expect(fields.length).toBe(2);
expect(fields.map((field) => field.type).toSorted()).toEqual(
['SIGNATURE', 'TEXT'].toSorted(),
);
expect(fields.map((field) => field.type).toSorted()).toEqual(['SIGNATURE', 'TEXT'].toSorted());
const textField = fields.find((field) => field.type === 'TEXT');
expect(textField).toBeDefined();
@@ -306,11 +289,7 @@ test.describe('AutoSave Fields Step', () => {
expect(textField.fieldMeta).toBeDefined();
if (
textField.fieldMeta &&
typeof textField.fieldMeta === 'object' &&
'type' in textField.fieldMeta
) {
if (textField.fieldMeta && typeof textField.fieldMeta === 'object' && 'type' in textField.fieldMeta) {
expect(textField.fieldMeta.type).toBe('text');
expect(textField.fieldMeta.label).toBe('Test Field');
expect(textField.fieldMeta.placeholder).toBe('Test Placeholder');
@@ -1,10 +1,9 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -50,9 +49,7 @@ test.describe('AutoSave Settings Step - Templates', () => {
teamId: team.id,
});
await expect(page.getByRole('textbox', { name: 'Title *' })).toHaveValue(
retrievedTemplate.title,
);
await expect(page.getByRole('textbox', { name: 'Title *' })).toHaveValue(retrievedTemplate.title);
}).toPass();
});
@@ -1,13 +1,12 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { EnvelopeType } from '@prisma/client';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { EnvelopeType } from '@prisma/client';
import { apiSignin } from '../fixtures/authentication';
@@ -121,18 +120,12 @@ test.describe('AutoSave Signers Step - Templates', () => {
await page.getByRole('button', { name: 'Add placeholder recipient' }).click();
await page
.getByTestId('placeholder-recipient-email-input')
.nth(1)
.fill('recipient2@documenso.com');
await page.getByTestId('placeholder-recipient-email-input').nth(1).fill('recipient2@documenso.com');
await page.getByTestId('placeholder-recipient-name-input').nth(1).fill('Recipient 2');
await page.getByRole('button', { name: 'Add placeholder recipient' }).click();
await page
.getByTestId('placeholder-recipient-email-input')
.nth(2)
.fill('recipient3@documenso.com');
await page.getByTestId('placeholder-recipient-email-input').nth(2).fill('recipient3@documenso.com');
await page.getByTestId('placeholder-recipient-name-input').nth(2).fill('Recipient 3');
await triggerAutosave(page);
@@ -185,11 +178,7 @@ export interface GetRecipientsForTemplateOptions {
teamId: number;
}
const getRecipientsForTemplate = async ({
templateId,
userId,
teamId,
}: GetRecipientsForTemplateOptions) => {
const getRecipientsForTemplate = async ({ templateId, userId, teamId }: GetRecipientsForTemplateOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'templateId',
@@ -1,10 +1,9 @@
import { expect, test } from '@playwright/test';
import { TeamMemberRole } from '@prisma/client';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { TeamMemberRole } from '@prisma/client';
import { apiSignin } from '../fixtures/authentication';
@@ -55,9 +54,7 @@ test('[TEMPLATE_FLOW] add document visibility settings', async ({ page }) => {
// Set document visibility.
await page.getByTestId('documentVisibilitySelectValue').click();
await page.getByLabel('Managers and above').click();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
'Managers and above',
);
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText('Managers and above');
// Save the settings by going to the next step.
await page.getByRole('button', { name: 'Continue' }).click();
@@ -67,9 +64,7 @@ test('[TEMPLATE_FLOW] add document visibility settings', async ({ page }) => {
await page.goto(`/t/${team.url}/templates/${template.id}/edit`);
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
'Managers and above',
);
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText('Managers and above');
});
test('[TEMPLATE_FLOW] team member visibility permissions', async ({ page }) => {
@@ -103,9 +98,7 @@ test('[TEMPLATE_FLOW] team member visibility permissions', async ({ page }) => {
// Manager should be able to set visibility to managers and above
await page.getByTestId('documentVisibilitySelectValue').click();
await page.getByLabel('Managers and above').click();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
'Managers and above',
);
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText('Managers and above');
await expect(page.getByText('Admins only')).toBeDisabled();
// Save and verify
@@ -133,9 +126,7 @@ test('[TEMPLATE_FLOW] team member visibility permissions', async ({ page }) => {
});
// Navigate to the new template
await page.goto(
`/t/${team.url}/templates/${mapSecondaryIdToTemplateId(everyoneTemplate.secondaryId)}/edit`,
);
await page.goto(`/t/${team.url}/templates/${mapSecondaryIdToTemplateId(everyoneTemplate.secondaryId)}/edit`);
// Regular member should be able to see but not modify visibility
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
@@ -1,7 +1,6 @@
import { expect, test } from '@playwright/test';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -1,9 +1,8 @@
import { expect, test } from '@playwright/test';
import { FolderType } from '@documenso/prisma/client';
import { seedBlankFolder } from '@documenso/prisma/seed/folders';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
import { expectToastTextToBeVisible } from '../fixtures/generic';
@@ -1,20 +1,16 @@
import { expect, test } from '@playwright/test';
import { DocumentDataType, TeamMemberRole } from '@prisma/client';
import path from 'path';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { DocumentDataType, TeamMemberRole } from '@prisma/client';
import path from 'path';
import { apiSignin } from '../fixtures/authentication';
const EXAMPLE_PDF_PATH = path.join(__dirname, '../../../../assets/example.pdf');
const FIELD_ALIGNMENT_TEST_PDF_PATH = path.join(
__dirname,
'../../../../assets/field-font-alignment.pdf',
);
const FIELD_ALIGNMENT_TEST_PDF_PATH = path.join(__dirname, '../../../../assets/field-font-alignment.pdf');
/**
* 1. Create a template with all settings filled out
@@ -230,9 +226,7 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
* This test verifies that we can create a document from a template using a custom document
* instead of the template's default document.
*/
test('[TEMPLATE]: should create a document from a template with custom document', async ({
page,
}) => {
test('[TEMPLATE]: should create a document from a template with custom document', async ({ page }) => {
const { user, team } = await seedUser();
const template = await seedBlankTemplate(user, team.id);
@@ -267,13 +261,11 @@ test('[TEMPLATE]: should create a document from a template with custom document'
// Upload document.
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page
.locator(`#template-use-dialog-file-input-${template.envelopeItems[0].id}`)
.evaluate((e) => {
if (e instanceof HTMLInputElement) {
e.click();
}
}),
page.locator(`#template-use-dialog-file-input-${template.envelopeItems[0].id}`).evaluate((e) => {
if (e instanceof HTMLInputElement) {
e.click();
}
}),
]);
await fileChooser.setFiles(FIELD_ALIGNMENT_TEST_PDF_PATH);
@@ -305,9 +297,7 @@ test('[TEMPLATE]: should create a document from a template with custom document'
const firstDocumentData = document.envelopeItems[0].documentData;
const expectedDocumentDataType =
process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT === 's3'
? DocumentDataType.S3_PATH
: DocumentDataType.BYTES_64;
process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT === 's3' ? DocumentDataType.S3_PATH : DocumentDataType.BYTES_64;
expect(document.title).toEqual('TEMPLATE_WITH_CUSTOM_DOC');
expect(firstDocumentData.type).toEqual(expectedDocumentDataType);
@@ -316,9 +306,7 @@ test('[TEMPLATE]: should create a document from a template with custom document'
// Todo: Doesn't really work due to normalization of the PDF which won't let us directly compare the data.
// Probably need to do a pixel match
expect(firstDocumentData.data).not.toEqual(template.envelopeItems[0].documentData.data);
expect(firstDocumentData.initialData).not.toEqual(
template.envelopeItems[0].documentData.initialData,
);
expect(firstDocumentData.initialData).not.toEqual(template.envelopeItems[0].documentData.initialData);
} else {
// For S3, we expect the data/initialData to be the S3 path (non-empty string)
expect(firstDocumentData.data).toBeTruthy();
@@ -330,9 +318,7 @@ test('[TEMPLATE]: should create a document from a template with custom document'
* This test verifies that we can create a team document from a template using a custom document
* instead of the template's default document.
*/
test('[TEMPLATE]: should create a team document from a template with custom document', async ({
page,
}) => {
test('[TEMPLATE]: should create a team document from a template with custom document', async ({ page }) => {
const { team, owner, organisation } = await seedTeam({
createTeamMembers: 2,
});
@@ -370,13 +356,11 @@ test('[TEMPLATE]: should create a team document from a template with custom docu
// Upload document.
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page
.locator(`#template-use-dialog-file-input-${template.envelopeItems[0].id}`)
.evaluate((e) => {
if (e instanceof HTMLInputElement) {
e.click();
}
}),
page.locator(`#template-use-dialog-file-input-${template.envelopeItems[0].id}`).evaluate((e) => {
if (e instanceof HTMLInputElement) {
e.click();
}
}),
]);
await fileChooser.setFiles(FIELD_ALIGNMENT_TEST_PDF_PATH);
@@ -406,9 +390,7 @@ test('[TEMPLATE]: should create a team document from a template with custom docu
});
const expectedDocumentDataType =
process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT === 's3'
? DocumentDataType.S3_PATH
: DocumentDataType.BYTES_64;
process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT === 's3' ? DocumentDataType.S3_PATH : DocumentDataType.BYTES_64;
const firstDocumentData = document.envelopeItems[0].documentData;
@@ -420,9 +402,7 @@ test('[TEMPLATE]: should create a team document from a template with custom docu
// Todo: Doesn't really work due to normalization of the PDF which won't let us directly compare the data.
// Probably need to do a pixel match
expect(firstDocumentData.data).not.toEqual(template.envelopeItems[0].documentData.data);
expect(firstDocumentData.initialData).not.toEqual(
template.envelopeItems[0].documentData.initialData,
);
expect(firstDocumentData.initialData).not.toEqual(template.envelopeItems[0].documentData.initialData);
} else {
// For S3, we expect the data/initialData to be the S3 path (non-empty string)
expect(firstDocumentData.data).toBeTruthy();
@@ -505,18 +485,12 @@ test('[TEMPLATE]: should create a document from a template using template docume
});
expect(document.title).toEqual('TEMPLATE_WITH_ORIGINAL_DOC');
expect(firstDocumentData.initialData).toEqual(
templateWithData.envelopeItems[0].documentData.data,
);
expect(firstDocumentData.initialData).toEqual(
templateWithData.envelopeItems[0].documentData.initialData,
);
expect(firstDocumentData.initialData).toEqual(templateWithData.envelopeItems[0].documentData.data);
expect(firstDocumentData.initialData).toEqual(templateWithData.envelopeItems[0].documentData.initialData);
expect(firstDocumentData.type).toEqual(templateWithData.envelopeItems[0].documentData.type);
});
test('[TEMPLATE]: should persist document visibility when creating from template', async ({
page,
}) => {
test('[TEMPLATE]: should persist document visibility when creating from template', async ({ page }) => {
const { team, owner, organisation } = await seedTeam({
createTeamMembers: 2,
});
@@ -533,9 +507,7 @@ test('[TEMPLATE]: should persist document visibility when creating from template
await page.getByLabel('Title').fill('TEMPLATE_WITH_VISIBILITY');
await page.getByTestId('documentVisibilitySelectValue').click();
await page.getByLabel('Managers and above').click();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
'Managers and above',
);
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText('Managers and above');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
@@ -1,7 +1,3 @@
import { expect, test } from '@playwright/test';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { customAlphabet } from 'nanoid';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createDocumentAuthOptions } from '@documenso/lib/utils/document-auth';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
@@ -10,6 +6,9 @@ import { prisma } from '@documenso/prisma';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedDirectTemplate, seedTemplate } from '@documenso/prisma/seed/templates';
import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { customAlphabet } from 'nanoid';
import { apiSignin } from '../fixtures/authentication';
@@ -139,9 +138,7 @@ test('[DIRECT_TEMPLATES]: V1 direct template link auth access', async ({ page })
},
});
const directTemplatePath = formatDirectTemplatePath(
directTemplateWithAuth.directLink?.token || '',
);
const directTemplatePath = formatDirectTemplatePath(directTemplateWithAuth.directLink?.token || '');
await page.goto(directTemplatePath);
@@ -181,9 +178,7 @@ test('[DIRECT_TEMPLATES]: V2 direct template link auth access', async ({ page })
},
});
const directTemplatePath = formatDirectTemplatePath(
directTemplateWithAuth.directLink?.token || '',
);
const directTemplatePath = formatDirectTemplatePath(directTemplateWithAuth.directLink?.token || '');
await page.goto(directTemplatePath);
@@ -312,9 +307,7 @@ test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with nex
},
});
const updatedSecondRecipient = createdEnvelopeRecipients.find(
(recipient) => recipient.signingOrder === 2,
);
const updatedSecondRecipient = createdEnvelopeRecipients.find((recipient) => recipient.signingOrder === 2);
expect(updatedSecondRecipient?.name).toBe(newName);
expect(updatedSecondRecipient?.email).toBe(newSecondSignerEmail);
@@ -399,9 +392,7 @@ test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with nex
},
});
const updatedSecondRecipient = createdEnvelopeRecipients.find(
(recipient) => recipient.signingOrder === 2,
);
const updatedSecondRecipient = createdEnvelopeRecipients.find((recipient) => recipient.signingOrder === 2);
expect(updatedSecondRecipient?.name).toBe(newName);
expect(updatedSecondRecipient?.email).toBe(newSecondSignerEmail);
@@ -1,8 +1,7 @@
import { expect, test } from '@playwright/test';
import { TeamMemberRole } from '@prisma/client';
import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedTemplate } from '@documenso/prisma/seed/templates';
import { expect, test } from '@playwright/test';
import { TeamMemberRole } from '@prisma/client';
import { apiSignin } from '../fixtures/authentication';
import { openDropdownMenu } from '../fixtures/generic';
@@ -1,8 +1,3 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { TemplateType } from '@prisma/client';
import { customAlphabet } from 'nanoid';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createTeam } from '@documenso/lib/server-only/team/create-team';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
@@ -10,6 +5,10 @@ import { prisma } from '@documenso/prisma';
import { seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { TemplateType } from '@prisma/client';
import { customAlphabet } from 'nanoid';
import { apiSignin, apiSignout } from '../fixtures/authentication';
@@ -65,12 +64,7 @@ const seedOrgTemplateScenario = async () => {
/**
* Helper to make tRPC queries via the authenticated page context.
*/
const trpcQuery = async (
page: Page,
procedure: string,
input: Record<string, unknown>,
teamId?: number,
) => {
const trpcQuery = async (page: Page, procedure: string, input: Record<string, unknown>, teamId?: number) => {
const inputParam = encodeURIComponent(JSON.stringify({ json: input }));
const url = `${WEBAPP_BASE_URL}/api/trpc/${procedure}?input=${inputParam}`;
@@ -85,12 +79,7 @@ const trpcQuery = async (
return { res, json: res.ok() ? await res.json() : null };
};
const trpcMutation = async (
page: Page,
procedure: string,
input: Record<string, unknown>,
teamId?: number,
) => {
const trpcMutation = async (page: Page, procedure: string, input: Record<string, unknown>, teamId?: number) => {
const url = `${WEBAPP_BASE_URL}/api/trpc/${procedure}`;
const headers: Record<string, string> = {
@@ -142,9 +131,7 @@ test.describe('Organisation Templates - UI Tabs', () => {
// ─── UI: Listing Organisation Templates ──────────────────────────────────────
test.describe('Organisation Templates - Listing', () => {
test('should list org templates from other teams under the Organisation tab', async ({
page,
}) => {
test('should list org templates from other teams under the Organisation tab', async ({ page }) => {
const { memberB, teamB, orgTemplate } = await seedOrgTemplateScenario();
await apiSignin({
@@ -163,9 +150,7 @@ test.describe('Organisation Templates - Listing', () => {
await expect(page.getByText(orgTemplate.title)).toBeVisible();
});
test('should not show private templates from other teams under Organisation tab', async ({
page,
}) => {
test('should not show private templates from other teams under Organisation tab', async ({ page }) => {
const { ownerA, teamA, memberB, teamB } = await seedOrgTemplateScenario();
// Create a private template on teamA — should NOT appear in org tab.
@@ -245,12 +230,7 @@ test.describe('Organisation Templates - findOrganisationTemplates API', () => {
await apiSignin({ page, email: memberB.email });
const { json } = await trpcQuery(
page,
'template.findOrganisationTemplates',
{ page: 1, perPage: 50 },
teamB.id,
);
const { json } = await trpcQuery(page, 'template.findOrganisationTemplates', { page: 1, perPage: 50 }, teamB.id);
const titles = json.result.data.json.data.map((t: { title: string }) => t.title);
expect(titles).not.toContain(privateTemplate.title);
@@ -297,12 +277,7 @@ test.describe('Organisation Templates - findOrganisationTemplates API', () => {
// memberB has role MEMBER on teamB — should only see EVERYONE visibility.
await apiSignin({ page, email: memberB.email });
const { json } = await trpcQuery(
page,
'template.findOrganisationTemplates',
{ page: 1, perPage: 50 },
teamB.id,
);
const { json } = await trpcQuery(page, 'template.findOrganisationTemplates', { page: 1, perPage: 50 }, teamB.id);
const titles = json.result.data.json.data.map((t: { title: string }) => t.title);
expect(titles).toContain(everyoneTemplate.title);
@@ -389,9 +364,7 @@ test.describe('Organisation Templates - getOrganisationTemplateById API', () =>
// ─── API: createDocumentFromTemplate with org template ───────────────────────
test.describe('Organisation Templates - Use from different team', () => {
test('should allow creating a document from an org template owned by a sibling team', async ({
page,
}) => {
test('should allow creating a document from an org template owned by a sibling team', async ({ page }) => {
const { memberB, teamB, orgTemplate } = await seedOrgTemplateScenario();
// Add a recipient to the org template so we can use it.
@@ -443,19 +416,12 @@ test.describe('Organisation Templates - Adversarial', () => {
await apiSignin({ page, email: outsider.email });
// Try to fetch via the standard envelope.get endpoint.
const { res } = await trpcQuery(
page,
'envelope.get',
{ envelopeId: orgTemplate.id },
outsiderTeam.id,
);
const { res } = await trpcQuery(page, 'envelope.get', { envelopeId: orgTemplate.id }, outsiderTeam.id);
expect(res.ok()).toBeFalsy();
});
test('should not allow a sibling team member to fetch a private template via org endpoint', async ({
page,
}) => {
test('should not allow a sibling team member to fetch a private template via org endpoint', async ({ page }) => {
const { ownerA, teamA, memberB, teamB } = await seedOrgTemplateScenario();
const privateTemplate = await seedBlankTemplate(ownerA, teamA.id, {
@@ -477,18 +443,11 @@ test.describe('Organisation Templates - Adversarial', () => {
expect(orgRes.ok()).toBeFalsy();
// Attempt 2: Try via standard envelope endpoint.
const { res: envelopeRes } = await trpcQuery(
page,
'envelope.get',
{ envelopeId: privateTemplate.id },
teamB.id,
);
const { res: envelopeRes } = await trpcQuery(page, 'envelope.get', { envelopeId: privateTemplate.id }, teamB.id);
expect(envelopeRes.ok()).toBeFalsy();
});
test('should not list org templates from a completely unrelated organisation', async ({
page,
}) => {
test('should not list org templates from a completely unrelated organisation', async ({ page }) => {
// Create scenario A.
const { orgTemplate: orgTemplateA } = await seedOrgTemplateScenario();
@@ -532,20 +491,13 @@ test.describe('Organisation Templates - Adversarial', () => {
expect(getRes.status()).toBe(401);
});
test('should not return org template data via findTemplates (team endpoint)', async ({
page,
}) => {
test('should not return org template data via findTemplates (team endpoint)', async ({ page }) => {
const { memberB, teamB, orgTemplate } = await seedOrgTemplateScenario();
await apiSignin({ page, email: memberB.email });
// The standard findTemplates endpoint should NOT include org templates from other teams.
const { json } = await trpcQuery(
page,
'template.findTemplates',
{ page: 1, perPage: 50 },
teamB.id,
);
const { json } = await trpcQuery(page, 'template.findTemplates', { page: 1, perPage: 50 }, teamB.id);
const titles = json.result.data.json.data.map((t: { title: string }) => t.title);
expect(titles).not.toContain(orgTemplate.title);
@@ -555,9 +507,7 @@ test.describe('Organisation Templates - Adversarial', () => {
// ─── API: envelope.item.getManyByToken (org template fallback) ───────────────
test.describe('Organisation Templates - envelope.item.getManyByToken API', () => {
test('should allow a sibling team member to fetch envelope items for an org template', async ({
page,
}) => {
test('should allow a sibling team member to fetch envelope items for an org template', async ({ page }) => {
const { memberB, teamB, orgTemplate } = await seedOrgTemplateScenario();
await apiSignin({ page, email: memberB.email });
@@ -577,9 +527,7 @@ test.describe('Organisation Templates - envelope.item.getManyByToken API', () =>
expect(items[0].envelopeId).toBe(orgTemplate.id);
});
test('should allow the owning team member to fetch envelope items (own-team path)', async ({
page,
}) => {
test('should allow the owning team member to fetch envelope items (own-team path)', async ({ page }) => {
const { ownerA, teamA, orgTemplate } = await seedOrgTemplateScenario();
await apiSignin({ page, email: ownerA.email });
@@ -614,9 +562,7 @@ test.describe('Organisation Templates - envelope.item.getManyByToken API', () =>
expect(res.ok()).toBeFalsy();
});
test('should reject fetching items for a PRIVATE template from a sibling team', async ({
page,
}) => {
test('should reject fetching items for a PRIVATE template from a sibling team', async ({ page }) => {
const { ownerA, teamA, memberB, teamB } = await seedOrgTemplateScenario();
const privateTemplate = await seedBlankTemplate(ownerA, teamA.id, {
@@ -1,8 +1,7 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
@@ -1,11 +1,6 @@
import { type Page, expect, test } from '@playwright/test';
import { prisma } from '@documenso/prisma';
import {
extractUserVerificationToken,
seedTestEmail,
seedUser,
} from '@documenso/prisma/seed/users';
import { extractUserVerificationToken, seedTestEmail, seedUser } from '@documenso/prisma/seed/users';
import { expect, type Page, test } from '@playwright/test';
import { signSignaturePad } from '../fixtures/signature';
@@ -1,6 +1,5 @@
import { type Page, expect, test } from '@playwright/test';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, type Page, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
import { expectTextToBeVisible } from '../fixtures/generic';
@@ -59,24 +58,14 @@ test('[USER] revoke sessions', async ({ page }: { page: Page }) => {
});
// Find table row which does not have text 'Current' and click the button called Revoke within the row.
await page
.getByRole('row')
.filter({ hasNotText: 'Current' })
.nth(1)
.getByRole('button', { name: 'Revoke' })
.click();
await page.getByRole('row').filter({ hasNotText: 'Current' }).nth(1).getByRole('button', { name: 'Revoke' }).click();
await expectTextToBeVisible(page, 'Session revoked');
// Expect (1 sessions + 1 header) rows length
await expect(page.getByRole('row')).toHaveCount(2);
// Revoke own session.
await page
.getByRole('row')
.filter({ hasText: 'Current' })
.first()
.getByRole('button', { name: 'Revoke' })
.click();
await page.getByRole('row').filter({ hasText: 'Current' }).first().getByRole('button', { name: 'Revoke' }).click();
await expect(page).toHaveURL('/signin');
});
@@ -1,8 +1,7 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../fixtures/authentication';
+3 -10
View File
@@ -1,7 +1,6 @@
import { type Page, expect, test } from '@playwright/test';
import { prisma } from '@documenso/prisma';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, type Page, test } from '@playwright/test';
import { apiSignin, apiSignout, checkSessionValid } from '../fixtures/authentication';
@@ -49,9 +48,7 @@ test('[USER] can reset password via forgot password', async ({ page }: { page: P
await expect(page.getByLabel('Repeat Password')).toHaveValue(oldPassword);
await page.getByRole('button', { name: 'Reset Password' }).click();
await expect(page.locator('body')).toContainText(
'Your new password cannot be the same as your old password.',
);
await expect(page.locator('body')).toContainText('Your new password cannot be the same as your old password.');
// Assert password reset.
await page.getByLabel('Password', { exact: true }).fill(newPassword);
@@ -171,11 +168,7 @@ test('[USER] password reset invalidates all sessions', async ({ page }: { page:
expect(await checkSessionValid(page)).toBe(true);
});
test('[USER] password update invalidates other sessions but keeps current', async ({
page,
}: {
page: Page;
}) => {
test('[USER] password update invalidates other sessions but keeps current', async ({ page }: { page: Page }) => {
const oldPassword = 'Test123!';
const newPassword = 'Test124!';

Some files were not shown because too many files have changed in this diff Show More