mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 01:15:49 +10:00
fix: lint project (#2693)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
@@ -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,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
@@ -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
+16
-39
@@ -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({
|
||||
|
||||
+337
-347
@@ -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));
|
||||
|
||||
@@ -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,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,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';
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user