mirror of
https://github.com/documenso/documenso.git
synced 2026-07-24 08:54:20 +10:00
fix: add tests
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
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 {
|
||||
type TEnvelopeEditorSurface,
|
||||
addEnvelopeItemPdf,
|
||||
clickAddMyselfButton,
|
||||
clickEnvelopeEditorStep,
|
||||
getEnvelopeEditorSettingsTrigger,
|
||||
getRecipientEmailInputs,
|
||||
openDocumentEnvelopeEditor,
|
||||
openEmbeddedEnvelopeEditor,
|
||||
openTemplateEnvelopeEditor,
|
||||
persistEmbeddedEnvelope,
|
||||
setRecipientEmail,
|
||||
setRecipientName,
|
||||
} from '../fixtures/envelope-editor';
|
||||
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
||||
|
||||
type TFieldFlowResult = {
|
||||
externalId: string;
|
||||
recipientEmail: string;
|
||||
};
|
||||
|
||||
const TEST_FIELD_VALUES = {
|
||||
embeddedRecipient: {
|
||||
email: 'embedded-field-recipient@documenso.com',
|
||||
name: 'Embedded Field Recipient',
|
||||
},
|
||||
};
|
||||
|
||||
const openSettingsDialog = async (root: Page) => {
|
||||
await getEnvelopeEditorSettingsTrigger(root).click();
|
||||
await expect(root.getByRole('heading', { name: 'Document Settings' })).toBeVisible();
|
||||
};
|
||||
|
||||
const updateExternalId = async (surface: TEnvelopeEditorSurface, externalId: string) => {
|
||||
await openSettingsDialog(surface.root);
|
||||
await surface.root.locator('input[name="externalId"]').fill(externalId);
|
||||
await surface.root.getByRole('button', { name: 'Update' }).click();
|
||||
|
||||
if (!surface.isEmbedded) {
|
||||
await expectToastTextToBeVisible(surface.root, 'Envelope updated');
|
||||
}
|
||||
};
|
||||
|
||||
const setupRecipientsForFieldPlacement = async (surface: TEnvelopeEditorSurface) => {
|
||||
if (surface.isEmbedded) {
|
||||
await expect(surface.root.getByRole('button', { name: 'Add Myself' })).toHaveCount(0);
|
||||
await setRecipientEmail(surface.root, 0, TEST_FIELD_VALUES.embeddedRecipient.email);
|
||||
await setRecipientName(surface.root, 0, TEST_FIELD_VALUES.embeddedRecipient.name);
|
||||
|
||||
return TEST_FIELD_VALUES.embeddedRecipient.email;
|
||||
}
|
||||
|
||||
await expect(surface.root.getByRole('button', { name: 'Add Myself' })).toBeVisible();
|
||||
await clickAddMyselfButton(surface.root);
|
||||
await expect(getRecipientEmailInputs(surface.root).first()).toHaveValue(surface.userEmail);
|
||||
|
||||
return surface.userEmail;
|
||||
};
|
||||
|
||||
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();
|
||||
await expect(canvas).toBeVisible();
|
||||
await canvas.click({ position });
|
||||
};
|
||||
|
||||
const runFieldFlow = async (surface: TEnvelopeEditorSurface): Promise<TFieldFlowResult> => {
|
||||
const externalId = `e2e-fields-${nanoid()}`;
|
||||
|
||||
if (surface.isEmbedded && !surface.envelopeId) {
|
||||
await addEnvelopeItemPdf(surface.root, 'embedded-fields.pdf');
|
||||
}
|
||||
|
||||
await updateExternalId(surface, externalId);
|
||||
const recipientEmail = await setupRecipientsForFieldPlacement(surface);
|
||||
|
||||
await clickEnvelopeEditorStep(surface.root, 'addFields');
|
||||
await expect(surface.root.getByText('Selected Recipient')).toBeVisible();
|
||||
await expect(surface.root.locator('.konva-container canvas').first()).toBeVisible();
|
||||
|
||||
await placeFieldOnPdf(surface.root, 'Signature', { x: 120, y: 140 });
|
||||
await expect(surface.root.getByText('1 Field')).toBeVisible();
|
||||
|
||||
await placeFieldOnPdf(surface.root, 'Text', { x: 220, y: 240 });
|
||||
await expect(surface.root.getByText('2 Fields')).toBeVisible();
|
||||
|
||||
await clickEnvelopeEditorStep(surface.root, 'upload');
|
||||
await expect(surface.root.getByRole('heading', { name: 'Recipients' })).toBeVisible();
|
||||
|
||||
await clickEnvelopeEditorStep(surface.root, 'addFields');
|
||||
await expect(surface.root.getByText('Selected Recipient')).toBeVisible();
|
||||
await expect(surface.root.getByText('2 Fields')).toBeVisible();
|
||||
|
||||
return {
|
||||
externalId,
|
||||
recipientEmail,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldMetaType = (fieldMeta: unknown) => {
|
||||
if (!isRecord(fieldMeta)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return typeof fieldMeta.type === 'string' ? fieldMeta.type : null;
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null;
|
||||
|
||||
const assertFieldsPersistedInDatabase = async ({
|
||||
surface,
|
||||
externalId,
|
||||
recipientEmail,
|
||||
}: {
|
||||
surface: TEnvelopeEditorSurface;
|
||||
externalId: string;
|
||||
recipientEmail: string;
|
||||
}) => {
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
externalId,
|
||||
userId: surface.userId,
|
||||
teamId: surface.teamId,
|
||||
type: surface.envelopeType,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
fields: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
const recipient = envelope.recipients.find(
|
||||
(currentRecipient) => currentRecipient.email === recipientEmail,
|
||||
);
|
||||
|
||||
expect(recipient).toBeDefined();
|
||||
|
||||
const fieldTypes = envelope.fields.map((field) => field.type).sort();
|
||||
const expectedFieldTypes = [FieldType.SIGNATURE, FieldType.TEXT].sort();
|
||||
|
||||
expect(envelope.fields).toHaveLength(2);
|
||||
expect(fieldTypes).toEqual(expectedFieldTypes);
|
||||
expect(new Set(envelope.fields.map((field) => field.envelopeItemId)).size).toBe(1);
|
||||
expect(envelope.fields.every((field) => field.recipientId === recipient?.id)).toBe(true);
|
||||
|
||||
const signatureField = envelope.fields.find((field) => field.type === FieldType.SIGNATURE);
|
||||
const textField = envelope.fields.find((field) => field.type === FieldType.TEXT);
|
||||
|
||||
expect(getFieldMetaType(signatureField?.fieldMeta)).toBe('signature');
|
||||
expect(getFieldMetaType(textField?.fieldMeta)).toBe('text');
|
||||
};
|
||||
|
||||
test.describe('Envelope Editor V2 - Fields', () => {
|
||||
test('documents/<id>: add and persist signature/text fields', async ({ page }) => {
|
||||
const surface = await openDocumentEnvelopeEditor(page);
|
||||
const result = await runFieldFlow(surface);
|
||||
|
||||
await assertFieldsPersistedInDatabase({
|
||||
surface,
|
||||
...result,
|
||||
});
|
||||
});
|
||||
|
||||
test('templates/<id>: add and persist signature/text fields', async ({ page }) => {
|
||||
const surface = await openTemplateEnvelopeEditor(page);
|
||||
const result = await runFieldFlow(surface);
|
||||
|
||||
await assertFieldsPersistedInDatabase({
|
||||
surface,
|
||||
...result,
|
||||
});
|
||||
});
|
||||
|
||||
test('/embed/v2/authoring/envelope/create DOCUMENT: add and persist signature/text fields', async ({
|
||||
page,
|
||||
}) => {
|
||||
const surface = await openEmbeddedEnvelopeEditor(page, {
|
||||
envelopeType: 'DOCUMENT',
|
||||
tokenNamePrefix: 'e2e-embed-fields',
|
||||
});
|
||||
const result = await runFieldFlow(surface);
|
||||
|
||||
await persistEmbeddedEnvelope(surface);
|
||||
|
||||
await assertFieldsPersistedInDatabase({
|
||||
surface,
|
||||
...result,
|
||||
});
|
||||
});
|
||||
|
||||
test('/embed/v2/authoring/envelope/edit/<id> TEMPLATE: add and persist signature/text fields', async ({
|
||||
page,
|
||||
}) => {
|
||||
const surface = await openEmbeddedEnvelopeEditor(page, {
|
||||
envelopeType: 'TEMPLATE',
|
||||
mode: 'edit',
|
||||
tokenNamePrefix: 'e2e-embed-fields',
|
||||
});
|
||||
const result = await runFieldFlow(surface);
|
||||
|
||||
await persistEmbeddedEnvelope(surface);
|
||||
|
||||
await assertFieldsPersistedInDatabase({
|
||||
surface,
|
||||
...result,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
import { type Page, expect, test } from '@playwright/test';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
type TEnvelopeEditorSurface,
|
||||
getEnvelopeItemDragHandles,
|
||||
getEnvelopeItemDropzoneInput,
|
||||
getEnvelopeItemRemoveButtons,
|
||||
getEnvelopeItemTitleInputs,
|
||||
openDocumentEnvelopeEditor,
|
||||
openEmbeddedEnvelopeEditor,
|
||||
openTemplateEnvelopeEditor,
|
||||
} from '../fixtures/envelope-editor';
|
||||
|
||||
test.use({
|
||||
storageState: {
|
||||
cookies: [],
|
||||
origins: [],
|
||||
},
|
||||
});
|
||||
|
||||
type TestFilePayload = {
|
||||
name: string;
|
||||
mimeType: string;
|
||||
buffer: Buffer;
|
||||
};
|
||||
|
||||
const examplePdfBuffer = fs.readFileSync(path.join(__dirname, '../../../../assets/example.pdf'));
|
||||
|
||||
const createPdfPayload = (name: string): TestFilePayload => ({
|
||||
name,
|
||||
mimeType: 'application/pdf',
|
||||
buffer: examplePdfBuffer,
|
||||
});
|
||||
|
||||
const getCurrentTitles = async (root: Page) => {
|
||||
const titleInputs = getEnvelopeItemTitleInputs(root);
|
||||
const count = await titleInputs.count();
|
||||
|
||||
return await Promise.all(
|
||||
Array.from({ length: count }, async (_, index) => await titleInputs.nth(index).inputValue()),
|
||||
);
|
||||
};
|
||||
|
||||
const uploadFiles = async (root: Page, files: TestFilePayload[]) => {
|
||||
const input = getEnvelopeItemDropzoneInput(root);
|
||||
|
||||
await input.setInputFiles(files);
|
||||
};
|
||||
|
||||
const dragEnvelopeItemByHandle = async ({
|
||||
root,
|
||||
sourceIndex,
|
||||
targetIndex,
|
||||
}: {
|
||||
root: Page;
|
||||
sourceIndex: number;
|
||||
targetIndex: number;
|
||||
}) => {
|
||||
const sourceHandle = getEnvelopeItemDragHandles(root).nth(sourceIndex);
|
||||
const targetHandle = getEnvelopeItemDragHandles(root).nth(targetIndex);
|
||||
|
||||
await expect(sourceHandle).toBeVisible();
|
||||
await expect(targetHandle).toBeVisible();
|
||||
|
||||
const sourceBox = await sourceHandle.boundingBox();
|
||||
const targetBox = await targetHandle.boundingBox();
|
||||
|
||||
if (!sourceBox || !targetBox) {
|
||||
throw new Error('Could not resolve drag handle bounding boxes');
|
||||
}
|
||||
|
||||
const sourceX = sourceBox.x + sourceBox.width / 2;
|
||||
const sourceY = sourceBox.y + sourceBox.height / 2;
|
||||
const targetX = targetBox.x + targetBox.width / 2;
|
||||
const targetY = targetBox.y + targetBox.height / 2;
|
||||
|
||||
await root.mouse.move(sourceX, sourceY);
|
||||
await root.mouse.down();
|
||||
await root.mouse.move(targetX, targetY, { steps: 20 });
|
||||
await root.mouse.up();
|
||||
};
|
||||
|
||||
const runEnvelopeItemCrudFlow = async ({
|
||||
root,
|
||||
isEmbedded,
|
||||
initialCount,
|
||||
filesToUpload,
|
||||
}: TEnvelopeEditorSurface & {
|
||||
initialCount: number;
|
||||
filesToUpload: TestFilePayload[];
|
||||
}) => {
|
||||
await expect(root.getByRole('heading', { name: 'Documents' })).toBeVisible();
|
||||
|
||||
await expect(getEnvelopeItemTitleInputs(root)).toHaveCount(initialCount);
|
||||
|
||||
await uploadFiles(root, filesToUpload);
|
||||
|
||||
const expectedCountAfterUpload = initialCount + filesToUpload.length;
|
||||
|
||||
await expect(getEnvelopeItemTitleInputs(root)).toHaveCount(expectedCountAfterUpload);
|
||||
|
||||
await getEnvelopeItemTitleInputs(root).nth(0).fill('Envelope Item A');
|
||||
await getEnvelopeItemTitleInputs(root).nth(1).fill('Envelope Item B');
|
||||
|
||||
await expect(getEnvelopeItemTitleInputs(root).nth(0)).toHaveValue('Envelope Item A');
|
||||
await expect(getEnvelopeItemTitleInputs(root).nth(1)).toHaveValue('Envelope Item B');
|
||||
|
||||
await dragEnvelopeItemByHandle({
|
||||
root,
|
||||
sourceIndex: 0,
|
||||
targetIndex: 1,
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(async () => await getCurrentTitles(root))
|
||||
.toEqual(['Envelope Item B', 'Envelope Item A']);
|
||||
|
||||
await getEnvelopeItemRemoveButtons(root).first().click();
|
||||
|
||||
if (!isEmbedded) {
|
||||
await root.getByRole('button', { name: 'Delete' }).click();
|
||||
}
|
||||
|
||||
await expect(getEnvelopeItemTitleInputs(root)).toHaveCount(expectedCountAfterUpload - 1);
|
||||
};
|
||||
|
||||
test.describe('Envelope Editor V2 - Envelope item CRUD', () => {
|
||||
test('documents/<id>: add, remove, reorder and retitle items', async ({ page }) => {
|
||||
const surface = await openDocumentEnvelopeEditor(page);
|
||||
|
||||
await runEnvelopeItemCrudFlow({
|
||||
...surface,
|
||||
initialCount: 1,
|
||||
filesToUpload: [createPdfPayload('document-item-added.pdf')],
|
||||
});
|
||||
});
|
||||
|
||||
test('templates/<id>: add, remove, reorder and retitle items', async ({ page }) => {
|
||||
const surface = await openTemplateEnvelopeEditor(page);
|
||||
|
||||
await runEnvelopeItemCrudFlow({
|
||||
...surface,
|
||||
initialCount: 1,
|
||||
filesToUpload: [createPdfPayload('template-item-added.pdf')],
|
||||
});
|
||||
});
|
||||
|
||||
test('/embed/v2/authoring/envelope/create DOCUMENT: add, remove, reorder and retitle items', async ({
|
||||
page,
|
||||
}) => {
|
||||
const surface = await openEmbeddedEnvelopeEditor(page, {
|
||||
envelopeType: 'DOCUMENT',
|
||||
});
|
||||
|
||||
await runEnvelopeItemCrudFlow({
|
||||
...surface,
|
||||
initialCount: 0,
|
||||
filesToUpload: [
|
||||
createPdfPayload('embedded-document-item-a.pdf'),
|
||||
createPdfPayload('embedded-document-item-b.pdf'),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('/embed/v2/authoring/envelope/edit/<id> TEMPLATE: add, remove, reorder and retitle items', async ({
|
||||
page,
|
||||
}) => {
|
||||
const surface = await openEmbeddedEnvelopeEditor(page, {
|
||||
envelopeType: 'TEMPLATE',
|
||||
mode: 'edit',
|
||||
tokenNamePrefix: 'e2e-embed-items',
|
||||
});
|
||||
|
||||
await runEnvelopeItemCrudFlow({
|
||||
...surface,
|
||||
initialCount: 1,
|
||||
filesToUpload: [createPdfPayload('embedded-template-item-updated.pdf')],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,279 @@
|
||||
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 {
|
||||
type TEnvelopeEditorSurface,
|
||||
addEnvelopeItemPdf,
|
||||
assertRecipientRole,
|
||||
clickAddMyselfButton,
|
||||
clickAddSignerButton,
|
||||
clickEnvelopeEditorStep,
|
||||
getEnvelopeEditorSettingsTrigger,
|
||||
getRecipientEmailInputs,
|
||||
getRecipientNameInputs,
|
||||
getRecipientRemoveButtons,
|
||||
getSigningOrderInputs,
|
||||
openDocumentEnvelopeEditor,
|
||||
openEmbeddedEnvelopeEditor,
|
||||
openTemplateEnvelopeEditor,
|
||||
persistEmbeddedEnvelope,
|
||||
setRecipientEmail,
|
||||
setRecipientName,
|
||||
setRecipientRole,
|
||||
setSigningOrderValue,
|
||||
toggleAllowDictateSigners,
|
||||
toggleSigningOrder,
|
||||
} from '../fixtures/envelope-editor';
|
||||
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
||||
|
||||
type RecipientFlowResult = {
|
||||
externalId: string;
|
||||
expectedRecipientsBySigningOrder: Array<{
|
||||
email: string;
|
||||
name: string;
|
||||
role: RecipientRole;
|
||||
signingOrder: number;
|
||||
}>;
|
||||
removedRecipientEmail: string;
|
||||
};
|
||||
|
||||
const TEST_RECIPIENT_VALUES = {
|
||||
secondRecipient: {
|
||||
email: 'recipient-two@example.com',
|
||||
name: 'Recipient Two',
|
||||
},
|
||||
thirdRecipient: {
|
||||
email: 'recipient-three@example.com',
|
||||
name: 'Recipient Three',
|
||||
},
|
||||
embeddedPrimaryRecipient: {
|
||||
email: 'embedded-primary@example.com',
|
||||
name: 'Embedded Primary',
|
||||
},
|
||||
};
|
||||
|
||||
const openSettingsDialog = async (root: Page) => {
|
||||
await getEnvelopeEditorSettingsTrigger(root).click();
|
||||
await expect(root.getByRole('heading', { name: 'Document Settings' })).toBeVisible();
|
||||
};
|
||||
|
||||
const updateExternalId = async (surface: TEnvelopeEditorSurface, externalId: string) => {
|
||||
await openSettingsDialog(surface.root);
|
||||
await surface.root.locator('input[name="externalId"]').fill(externalId);
|
||||
await surface.root.getByRole('button', { name: 'Update' }).click();
|
||||
|
||||
if (!surface.isEmbedded) {
|
||||
await expectToastTextToBeVisible(surface.root, 'Envelope updated');
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToAddFieldsAndBack = async (root: Page) => {
|
||||
await clickEnvelopeEditorStep(root, 'addFields');
|
||||
await expect(root.getByText('Selected Recipient')).toBeVisible();
|
||||
|
||||
await clickEnvelopeEditorStep(root, 'upload');
|
||||
await expect(root.getByRole('heading', { name: 'Recipients' })).toBeVisible();
|
||||
};
|
||||
|
||||
const runRecipientFlow = async (surface: TEnvelopeEditorSurface): Promise<RecipientFlowResult> => {
|
||||
const externalId = `e2e-recipients-${nanoid()}`;
|
||||
|
||||
await updateExternalId(surface, externalId);
|
||||
|
||||
let primaryRecipient = TEST_RECIPIENT_VALUES.embeddedPrimaryRecipient;
|
||||
|
||||
if (surface.isEmbedded) {
|
||||
await expect(surface.root.getByRole('button', { name: 'Add Myself' })).toHaveCount(0);
|
||||
await setRecipientEmail(surface.root, 0, primaryRecipient.email);
|
||||
await setRecipientName(surface.root, 0, primaryRecipient.name);
|
||||
} else {
|
||||
await expect(surface.root.getByRole('button', { name: 'Add Myself' })).toBeVisible();
|
||||
await clickAddMyselfButton(surface.root);
|
||||
|
||||
primaryRecipient = {
|
||||
email: surface.userEmail,
|
||||
name: surface.userName,
|
||||
};
|
||||
|
||||
await expect(getRecipientEmailInputs(surface.root).nth(0)).toHaveValue(surface.userEmail);
|
||||
}
|
||||
|
||||
await clickAddSignerButton(surface.root);
|
||||
await clickAddSignerButton(surface.root);
|
||||
|
||||
await setRecipientEmail(surface.root, 1, TEST_RECIPIENT_VALUES.secondRecipient.email);
|
||||
await setRecipientName(surface.root, 1, TEST_RECIPIENT_VALUES.secondRecipient.name);
|
||||
|
||||
await setRecipientEmail(surface.root, 2, TEST_RECIPIENT_VALUES.thirdRecipient.email);
|
||||
await setRecipientName(surface.root, 2, TEST_RECIPIENT_VALUES.thirdRecipient.name);
|
||||
|
||||
await setRecipientRole(surface.root, 1, 'Needs to approve');
|
||||
await setRecipientRole(surface.root, 2, 'Receives copy');
|
||||
|
||||
await getRecipientRemoveButtons(surface.root).nth(2).click();
|
||||
await expect(getRecipientEmailInputs(surface.root)).toHaveCount(2);
|
||||
|
||||
await toggleSigningOrder(surface.root, true);
|
||||
await expect(getSigningOrderInputs(surface.root)).toHaveCount(2);
|
||||
await setSigningOrderValue(surface.root, 0, 2);
|
||||
|
||||
await toggleAllowDictateSigners(surface.root, true);
|
||||
|
||||
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(1)).toHaveValue(primaryRecipient.email);
|
||||
|
||||
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(getSigningOrderInputs(surface.root).nth(0)).toHaveValue('1');
|
||||
await expect(getSigningOrderInputs(surface.root).nth(1)).toHaveValue('2');
|
||||
|
||||
return {
|
||||
externalId,
|
||||
removedRecipientEmail: TEST_RECIPIENT_VALUES.thirdRecipient.email,
|
||||
expectedRecipientsBySigningOrder: [
|
||||
{
|
||||
email: TEST_RECIPIENT_VALUES.secondRecipient.email,
|
||||
name: TEST_RECIPIENT_VALUES.secondRecipient.name,
|
||||
role: RecipientRole.APPROVER,
|
||||
signingOrder: 1,
|
||||
},
|
||||
{
|
||||
email: primaryRecipient.email,
|
||||
name: primaryRecipient.name,
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const assertRecipientsPersistedInDatabase = async ({
|
||||
surface,
|
||||
externalId,
|
||||
expectedRecipientsBySigningOrder,
|
||||
removedRecipientEmail,
|
||||
}: {
|
||||
surface: TEnvelopeEditorSurface;
|
||||
externalId: string;
|
||||
expectedRecipientsBySigningOrder: RecipientFlowResult['expectedRecipientsBySigningOrder'];
|
||||
removedRecipientEmail: string;
|
||||
}) => {
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
externalId,
|
||||
userId: surface.userId,
|
||||
teamId: surface.teamId,
|
||||
type: surface.envelopeType,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
recipients: {
|
||||
orderBy: {
|
||||
signingOrder: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
expect(envelope.recipients).toHaveLength(expectedRecipientsBySigningOrder.length);
|
||||
expect(envelope.documentMeta.signingOrder).toBe(DocumentSigningOrder.SEQUENTIAL);
|
||||
expect(envelope.documentMeta.allowDictateNextSigner).toBe(true);
|
||||
|
||||
expectedRecipientsBySigningOrder.forEach((expectedRecipient, index) => {
|
||||
const recipient = envelope.recipients[index];
|
||||
|
||||
expect(recipient.email).toBe(expectedRecipient.email);
|
||||
expect(recipient.name).toBe(expectedRecipient.name);
|
||||
expect(recipient.role).toBe(expectedRecipient.role);
|
||||
expect(recipient.signingOrder).toBe(expectedRecipient.signingOrder);
|
||||
});
|
||||
|
||||
expect(envelope.recipients.some((recipient) => recipient.email === removedRecipientEmail)).toBe(
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
test.describe('Envelope Editor V2 - Recipients', () => {
|
||||
test('documents/<id>: add myself, CRUD, roles, signing order and dictate signers', async ({
|
||||
page,
|
||||
}) => {
|
||||
const surface = await openDocumentEnvelopeEditor(page);
|
||||
const result = await runRecipientFlow(surface);
|
||||
|
||||
await assertRecipientsPersistedInDatabase({
|
||||
surface,
|
||||
...result,
|
||||
});
|
||||
});
|
||||
|
||||
test('templates/<id>: add myself, CRUD, roles, signing order and dictate signers', async ({
|
||||
page,
|
||||
}) => {
|
||||
const surface = await openTemplateEnvelopeEditor(page);
|
||||
const result = await runRecipientFlow(surface);
|
||||
|
||||
await assertRecipientsPersistedInDatabase({
|
||||
surface,
|
||||
...result,
|
||||
});
|
||||
});
|
||||
|
||||
test('/embed/v2/authoring/envelope/create DOCUMENT: recipients settings persist after create', async ({
|
||||
page,
|
||||
}) => {
|
||||
const surface = await openEmbeddedEnvelopeEditor(page, {
|
||||
envelopeType: 'DOCUMENT',
|
||||
tokenNamePrefix: 'e2e-embed-recipients',
|
||||
});
|
||||
|
||||
await addEnvelopeItemPdf(surface.root, 'embedded-document-recipients.pdf');
|
||||
|
||||
const result = await runRecipientFlow(surface);
|
||||
await persistEmbeddedEnvelope(surface);
|
||||
|
||||
await assertRecipientsPersistedInDatabase({
|
||||
surface,
|
||||
...result,
|
||||
});
|
||||
});
|
||||
|
||||
test('/embed/v2/authoring/envelope/edit/<id> TEMPLATE: recipients settings persist after update', async ({
|
||||
page,
|
||||
}) => {
|
||||
const surface = await openEmbeddedEnvelopeEditor(page, {
|
||||
envelopeType: 'TEMPLATE',
|
||||
mode: 'edit',
|
||||
tokenNamePrefix: 'e2e-embed-recipients',
|
||||
});
|
||||
|
||||
const result = await runRecipientFlow(surface);
|
||||
await persistEmbeddedEnvelope(surface);
|
||||
|
||||
await assertRecipientsPersistedInDatabase({
|
||||
surface,
|
||||
...result,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,359 @@
|
||||
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 {
|
||||
type TEnvelopeEditorSurface,
|
||||
getEnvelopeEditorSettingsTrigger,
|
||||
openDocumentEnvelopeEditor,
|
||||
openEmbeddedEnvelopeEditor,
|
||||
openTemplateEnvelopeEditor,
|
||||
persistEmbeddedEnvelope,
|
||||
} from '../fixtures/envelope-editor';
|
||||
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
||||
|
||||
type SettingsFlowData = {
|
||||
externalId: string;
|
||||
isEmbedded: boolean;
|
||||
};
|
||||
|
||||
const TEST_SETTINGS_VALUES = {
|
||||
replyTo: 'e2e-settings@example.com',
|
||||
redirectUrl: 'https://example.com/e2e-settings-complete',
|
||||
subject: 'E2E settings subject',
|
||||
message: 'E2E settings message',
|
||||
language: 'French',
|
||||
dateFormat: 'DD/MM/YYYY',
|
||||
timezone: 'Europe/London',
|
||||
distributionMethod: 'None',
|
||||
accessAuth: 'Require account',
|
||||
actionAuth: 'Require password',
|
||||
visibility: 'Managers and above',
|
||||
};
|
||||
|
||||
const DB_EXPECTED_VALUES = {
|
||||
language: 'fr',
|
||||
dateFormat: 'dd/MM/yyyy',
|
||||
timezone: 'Europe/London',
|
||||
distributionMethod: DocumentDistributionMethod.NONE,
|
||||
visibility: DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
globalAccessAuth: ['ACCOUNT'],
|
||||
globalActionAuth: ['PASSWORD'],
|
||||
emailSettings: {
|
||||
recipientSigned: false,
|
||||
recipientSigningRequest: false,
|
||||
recipientRemoved: false,
|
||||
documentPending: false,
|
||||
documentCompleted: false,
|
||||
documentDeleted: false,
|
||||
ownerDocumentCompleted: false,
|
||||
},
|
||||
};
|
||||
|
||||
const openSettingsDialog = async (root: Page) => {
|
||||
await getEnvelopeEditorSettingsTrigger(root).click();
|
||||
await expect(root.getByRole('heading', { name: 'Document Settings' })).toBeVisible();
|
||||
};
|
||||
|
||||
const clickSettingsDialogHeader = async (root: Page) => {
|
||||
await root.locator('[data-testid="envelope-editor-settings-dialog-header"]').click();
|
||||
};
|
||||
|
||||
const getComboboxByLabel = (root: Page, label: string) =>
|
||||
root
|
||||
.locator(`label:has-text("${label}")`)
|
||||
.locator('xpath=..')
|
||||
.locator('[role="combobox"]')
|
||||
.first();
|
||||
|
||||
const selectMultiSelectOption = async (
|
||||
root: Page,
|
||||
dataTestId: 'documentAccessSelectValue' | 'documentActionSelectValue',
|
||||
optionLabel: string,
|
||||
) => {
|
||||
const select = root.locator(`[data-testid="${dataTestId}"]`);
|
||||
|
||||
await select.click();
|
||||
await root.locator('[cmdk-item]').filter({ hasText: optionLabel }).first().click();
|
||||
await clickSettingsDialogHeader(root);
|
||||
};
|
||||
|
||||
const runSettingsFlow = async (
|
||||
{ root }: TEnvelopeEditorSurface,
|
||||
{ externalId, isEmbedded }: SettingsFlowData,
|
||||
) => {
|
||||
await openSettingsDialog(root);
|
||||
|
||||
await getComboboxByLabel(root, 'Language').click();
|
||||
await root.getByRole('option', { name: TEST_SETTINGS_VALUES.language }).click();
|
||||
await clickSettingsDialogHeader(root);
|
||||
|
||||
const signatureTypesCombobox = getComboboxByLabel(root, 'Allowed Signature Types');
|
||||
|
||||
await signatureTypesCombobox.click();
|
||||
await root.getByRole('option', { name: 'Upload' }).click();
|
||||
await clickSettingsDialogHeader(root);
|
||||
|
||||
await getComboboxByLabel(root, 'Date Format').click();
|
||||
await root.getByRole('option', { name: TEST_SETTINGS_VALUES.dateFormat, exact: true }).click();
|
||||
await clickSettingsDialogHeader(root);
|
||||
|
||||
await getComboboxByLabel(root, 'Time Zone').click();
|
||||
await root.locator('[cmdk-input]').last().fill(TEST_SETTINGS_VALUES.timezone);
|
||||
await root.getByRole('option', { name: TEST_SETTINGS_VALUES.timezone }).click();
|
||||
await clickSettingsDialogHeader(root);
|
||||
|
||||
await root.locator('input[name="externalId"]').fill(externalId);
|
||||
await root.locator('input[name="meta.redirectUrl"]').fill(TEST_SETTINGS_VALUES.redirectUrl);
|
||||
|
||||
await root.locator('[data-testid="documentDistributionMethodSelectValue"]').click();
|
||||
await root.getByRole('option', { name: TEST_SETTINGS_VALUES.distributionMethod }).click();
|
||||
await clickSettingsDialogHeader(root);
|
||||
|
||||
await root.getByRole('button', { name: 'Email' }).click();
|
||||
await root.locator('#recipientSigned').click();
|
||||
await root.locator('#recipientSigningRequest').click();
|
||||
await root.locator('#recipientRemoved').click();
|
||||
await root.locator('#documentPending').click();
|
||||
await root.locator('#documentCompleted').click();
|
||||
await root.locator('#documentDeleted').click();
|
||||
await root.locator('#ownerDocumentCompleted').click();
|
||||
await root.locator('input[name="meta.emailReplyTo"]').fill(TEST_SETTINGS_VALUES.replyTo);
|
||||
await root.locator('input[name="meta.subject"]').fill(TEST_SETTINGS_VALUES.subject);
|
||||
await root.locator('textarea[name="meta.message"]').fill(TEST_SETTINGS_VALUES.message);
|
||||
|
||||
await root.getByRole('button', { name: 'Security' }).click();
|
||||
await selectMultiSelectOption(root, 'documentAccessSelectValue', TEST_SETTINGS_VALUES.accessAuth);
|
||||
|
||||
const actionAuthSelect = root.locator('[data-testid="documentActionSelectValue"]');
|
||||
const hasActionAuthSelect = (await actionAuthSelect.count()) > 0;
|
||||
|
||||
if (hasActionAuthSelect) {
|
||||
await selectMultiSelectOption(
|
||||
root,
|
||||
'documentActionSelectValue',
|
||||
TEST_SETTINGS_VALUES.actionAuth,
|
||||
);
|
||||
}
|
||||
|
||||
await root.locator('[data-testid="documentVisibilitySelectValue"]').click();
|
||||
await root.getByRole('option', { name: TEST_SETTINGS_VALUES.visibility }).click();
|
||||
await clickSettingsDialogHeader(root);
|
||||
|
||||
await root.getByRole('button', { name: 'Update' }).click();
|
||||
|
||||
if (!isEmbedded) {
|
||||
await expectToastTextToBeVisible(root, 'Envelope updated');
|
||||
}
|
||||
|
||||
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(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, 'Time Zone')).toContainText(TEST_SETTINGS_VALUES.timezone);
|
||||
await expect(root.locator('[data-testid="documentDistributionMethodSelectValue"]')).toContainText(
|
||||
TEST_SETTINGS_VALUES.distributionMethod,
|
||||
);
|
||||
|
||||
await root.getByRole('button', { name: 'Email' }).click();
|
||||
await expect(root.locator('#recipientSigned')).toHaveAttribute('aria-checked', 'false');
|
||||
await expect(root.locator('#recipientSigningRequest')).toHaveAttribute('aria-checked', 'false');
|
||||
await expect(root.locator('#recipientRemoved')).toHaveAttribute('aria-checked', 'false');
|
||||
await expect(root.locator('#documentPending')).toHaveAttribute('aria-checked', 'false');
|
||||
await expect(root.locator('#documentCompleted')).toHaveAttribute('aria-checked', 'false');
|
||||
await expect(root.locator('#documentDeleted')).toHaveAttribute('aria-checked', 'false');
|
||||
await expect(root.locator('#ownerDocumentCompleted')).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 root.getByRole('button', { name: 'Security' }).click();
|
||||
await expect(root.locator('[data-testid="documentAccessSelectValue"]')).toContainText(
|
||||
TEST_SETTINGS_VALUES.accessAuth,
|
||||
);
|
||||
|
||||
if (hasActionAuthSelect) {
|
||||
await expect(root.locator('[data-testid="documentActionSelectValue"]')).toContainText(
|
||||
TEST_SETTINGS_VALUES.actionAuth,
|
||||
);
|
||||
}
|
||||
|
||||
await expect(root.locator('[data-testid="documentVisibilitySelectValue"]')).toContainText(
|
||||
TEST_SETTINGS_VALUES.visibility,
|
||||
);
|
||||
|
||||
await root.getByRole('button', { name: 'Update' }).click();
|
||||
|
||||
if (!isEmbedded) {
|
||||
await expectToastTextToBeVisible(root, 'Envelope updated');
|
||||
}
|
||||
|
||||
return {
|
||||
hasActionAuthSelect,
|
||||
};
|
||||
};
|
||||
|
||||
const assertEnvelopeSettingsPersistedInDatabase = async ({
|
||||
externalId,
|
||||
surface,
|
||||
hasActionAuthSelect,
|
||||
}: {
|
||||
externalId: string;
|
||||
surface: TEnvelopeEditorSurface;
|
||||
hasActionAuthSelect: boolean;
|
||||
}) => {
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
externalId,
|
||||
userId: surface.userId,
|
||||
teamId: surface.teamId,
|
||||
type: surface.envelopeType,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(envelope.externalId).toBe(externalId);
|
||||
expect(envelope.visibility).toBe(DB_EXPECTED_VALUES.visibility);
|
||||
expect(envelope.documentMeta.language).toBe(DB_EXPECTED_VALUES.language);
|
||||
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.redirectUrl).toBe(TEST_SETTINGS_VALUES.redirectUrl);
|
||||
expect(envelope.documentMeta.emailReplyTo).toBe(TEST_SETTINGS_VALUES.replyTo);
|
||||
expect(envelope.documentMeta.subject).toBe(TEST_SETTINGS_VALUES.subject);
|
||||
expect(envelope.documentMeta.message).toBe(TEST_SETTINGS_VALUES.message);
|
||||
expect(envelope.documentMeta.drawSignatureEnabled).toBe(true);
|
||||
expect(envelope.documentMeta.typedSignatureEnabled).toBe(true);
|
||||
expect(envelope.documentMeta.uploadSignatureEnabled).toBe(false);
|
||||
expect(envelope.documentMeta.emailSettings).toMatchObject(DB_EXPECTED_VALUES.emailSettings);
|
||||
|
||||
const authOptions = parseAuthOptions(envelope.authOptions);
|
||||
|
||||
expect(authOptions.globalAccessAuth ?? []).toEqual(DB_EXPECTED_VALUES.globalAccessAuth);
|
||||
|
||||
if (hasActionAuthSelect) {
|
||||
expect(authOptions.globalActionAuth ?? []).toEqual(DB_EXPECTED_VALUES.globalActionAuth);
|
||||
}
|
||||
};
|
||||
|
||||
const parseAuthOptions = (
|
||||
authOptions: unknown,
|
||||
): { globalAccessAuth: string[]; globalActionAuth: string[] } => {
|
||||
if (!isRecord(authOptions)) {
|
||||
return {
|
||||
globalAccessAuth: [],
|
||||
globalActionAuth: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
globalAccessAuth: Array.isArray(authOptions.globalAccessAuth)
|
||||
? authOptions.globalAccessAuth.filter((entry): entry is string => typeof entry === 'string')
|
||||
: [],
|
||||
globalActionAuth: Array.isArray(authOptions.globalActionAuth)
|
||||
? authOptions.globalActionAuth.filter((entry): entry is string => typeof entry === 'string')
|
||||
: [],
|
||||
};
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null;
|
||||
|
||||
test.describe('Envelope Editor V2 - Envelope settings dialog', () => {
|
||||
test('documents/<id>: update and persist settings', async ({ page }) => {
|
||||
const surface = await openDocumentEnvelopeEditor(page);
|
||||
const externalId = `e2e-settings-${nanoid()}`;
|
||||
|
||||
const { hasActionAuthSelect } = await runSettingsFlow(surface, {
|
||||
externalId,
|
||||
isEmbedded: false,
|
||||
});
|
||||
|
||||
await assertEnvelopeSettingsPersistedInDatabase({
|
||||
externalId,
|
||||
surface,
|
||||
hasActionAuthSelect,
|
||||
});
|
||||
});
|
||||
|
||||
test('templates/<id>: update and persist settings', async ({ page }) => {
|
||||
const surface = await openTemplateEnvelopeEditor(page);
|
||||
const externalId = `e2e-settings-${nanoid()}`;
|
||||
|
||||
const { hasActionAuthSelect } = await runSettingsFlow(surface, {
|
||||
externalId,
|
||||
isEmbedded: false,
|
||||
});
|
||||
|
||||
await assertEnvelopeSettingsPersistedInDatabase({
|
||||
externalId,
|
||||
surface,
|
||||
hasActionAuthSelect,
|
||||
});
|
||||
});
|
||||
|
||||
test('/embed/v2/authoring/envelope/create DOCUMENT: update and persist settings', async ({
|
||||
page,
|
||||
}) => {
|
||||
const surface = await openEmbeddedEnvelopeEditor(page, {
|
||||
envelopeType: 'DOCUMENT',
|
||||
tokenNamePrefix: 'e2e-embed-settings',
|
||||
});
|
||||
const externalId = `e2e-settings-${nanoid()}`;
|
||||
|
||||
const { hasActionAuthSelect } = await runSettingsFlow(surface, {
|
||||
externalId,
|
||||
isEmbedded: true,
|
||||
});
|
||||
|
||||
await persistEmbeddedEnvelope(surface);
|
||||
|
||||
await assertEnvelopeSettingsPersistedInDatabase({
|
||||
externalId,
|
||||
surface,
|
||||
hasActionAuthSelect,
|
||||
});
|
||||
});
|
||||
|
||||
test('/embed/v2/authoring/envelope/edit/<id> TEMPLATE: update and persist settings', async ({
|
||||
page,
|
||||
}) => {
|
||||
const surface = await openEmbeddedEnvelopeEditor(page, {
|
||||
envelopeType: 'TEMPLATE',
|
||||
mode: 'edit',
|
||||
tokenNamePrefix: 'e2e-embed-settings',
|
||||
});
|
||||
const externalId = `e2e-settings-${nanoid()}`;
|
||||
|
||||
const { hasActionAuthSelect } = await runSettingsFlow(surface, {
|
||||
externalId,
|
||||
isEmbedded: true,
|
||||
});
|
||||
|
||||
await persistEmbeddedEnvelope(surface);
|
||||
|
||||
await assertEnvelopeSettingsPersistedInDatabase({
|
||||
externalId,
|
||||
surface,
|
||||
hasActionAuthSelect,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,429 @@
|
||||
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 { apiSignin } from './authentication';
|
||||
|
||||
const examplePdfBuffer = fs.readFileSync(path.join(__dirname, '../../../../assets/example.pdf'));
|
||||
|
||||
export type TEnvelopeEditorSurface = {
|
||||
root: Page;
|
||||
isEmbedded: boolean;
|
||||
envelopeId?: string;
|
||||
envelopeType: TEnvelopeEditorType;
|
||||
userId: number;
|
||||
userEmail: string;
|
||||
userName: string;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export type TEnvelopeEditorType = 'DOCUMENT' | 'TEMPLATE';
|
||||
|
||||
type TEmbeddedHashCommonOptions = {
|
||||
externalId?: string;
|
||||
features?: typeof DEFAULT_EMBEDDED_EDITOR_CONFIG;
|
||||
css?: string;
|
||||
cssVars?: Record<string, string>;
|
||||
darkModeDisabled?: boolean;
|
||||
};
|
||||
|
||||
const encodeEmbeddedOptions = (options: Record<string, unknown>) => {
|
||||
const encodedPayload = encodeURIComponent(JSON.stringify(options));
|
||||
|
||||
if (typeof btoa === 'function') {
|
||||
return btoa(encodedPayload);
|
||||
}
|
||||
|
||||
return Buffer.from(encodedPayload, 'utf8').toString('base64');
|
||||
};
|
||||
|
||||
export const createEmbeddedEnvelopeCreateHash = ({
|
||||
envelopeType,
|
||||
externalId,
|
||||
features = DEFAULT_EMBEDDED_EDITOR_CONFIG,
|
||||
css,
|
||||
cssVars,
|
||||
darkModeDisabled,
|
||||
}: { envelopeType: TEnvelopeEditorType } & TEmbeddedHashCommonOptions) => {
|
||||
return encodeEmbeddedOptions({
|
||||
externalId,
|
||||
type: envelopeType,
|
||||
features,
|
||||
css,
|
||||
cssVars,
|
||||
darkModeDisabled,
|
||||
});
|
||||
};
|
||||
|
||||
export const createEmbeddedEnvelopeEditHash = ({
|
||||
externalId,
|
||||
features = DEFAULT_EMBEDDED_EDITOR_CONFIG,
|
||||
css,
|
||||
cssVars,
|
||||
darkModeDisabled,
|
||||
}: TEmbeddedHashCommonOptions) => {
|
||||
return encodeEmbeddedOptions({
|
||||
externalId,
|
||||
features,
|
||||
css,
|
||||
cssVars,
|
||||
darkModeDisabled,
|
||||
});
|
||||
};
|
||||
|
||||
export const openDocumentEnvelopeEditor = async (page: Page): Promise<TEnvelopeEditorSurface> => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const document = await seedBlankDocument(user, team.id, {
|
||||
internalVersion: 2,
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/documents/${document.id}/edit?step=uploadAndRecipients`,
|
||||
});
|
||||
|
||||
return {
|
||||
root: page,
|
||||
isEmbedded: false,
|
||||
envelopeId: document.id,
|
||||
envelopeType: 'DOCUMENT',
|
||||
userId: user.id,
|
||||
userEmail: user.email,
|
||||
userName: user.name ?? '',
|
||||
teamId: team.id,
|
||||
};
|
||||
};
|
||||
|
||||
export const openTemplateEnvelopeEditor = async (page: Page): Promise<TEnvelopeEditorSurface> => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const template = await seedBlankTemplate(user, team.id, {
|
||||
createTemplateOptions: {
|
||||
title: `E2E Template ${Date.now()}`,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
internalVersion: 2,
|
||||
},
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/templates/${template.id}/edit?step=uploadAndRecipients`,
|
||||
});
|
||||
|
||||
return {
|
||||
root: page,
|
||||
isEmbedded: false,
|
||||
envelopeId: template.id,
|
||||
envelopeType: 'TEMPLATE',
|
||||
userId: user.id,
|
||||
userEmail: user.email,
|
||||
userName: user.name ?? '',
|
||||
teamId: team.id,
|
||||
};
|
||||
};
|
||||
|
||||
type OpenEmbeddedEnvelopeEditorOptions = {
|
||||
envelopeType: TEnvelopeEditorType;
|
||||
mode?: 'create' | 'edit';
|
||||
tokenNamePrefix?: string;
|
||||
externalId?: string;
|
||||
features?: typeof DEFAULT_EMBEDDED_EDITOR_CONFIG;
|
||||
css?: string;
|
||||
cssVars?: Record<string, string>;
|
||||
darkModeDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const openEmbeddedEnvelopeEditor = async (
|
||||
page: Page,
|
||||
{
|
||||
envelopeType,
|
||||
mode = 'create',
|
||||
tokenNamePrefix = 'e2e-embed',
|
||||
externalId,
|
||||
features,
|
||||
css,
|
||||
cssVars,
|
||||
darkModeDisabled,
|
||||
}: OpenEmbeddedEnvelopeEditorOptions,
|
||||
): Promise<TEnvelopeEditorSurface> => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const envelopeToEdit =
|
||||
mode === 'edit'
|
||||
? envelopeType === 'DOCUMENT'
|
||||
? await seedBlankDocument(user, team.id, {
|
||||
internalVersion: 2,
|
||||
})
|
||||
: await seedBlankTemplate(user, team.id, {
|
||||
createTemplateOptions: {
|
||||
title: `E2E Template ${Date.now()}`,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
internalVersion: 2,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
const { token } = await createApiToken({
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
tokenName: `${tokenNamePrefix}-${envelopeType.toLowerCase()}`,
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
const embeddedToken = await resolveEmbeddingToken(
|
||||
page,
|
||||
token,
|
||||
envelopeToEdit ? `envelopeId:${envelopeToEdit.id}` : undefined,
|
||||
);
|
||||
|
||||
if (envelopeToEdit) {
|
||||
const hash = createEmbeddedEnvelopeEditHash({
|
||||
externalId,
|
||||
features: features ?? DEFAULT_EMBEDDED_EDITOR_CONFIG,
|
||||
css,
|
||||
cssVars,
|
||||
darkModeDisabled,
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
`/embed/v2/authoring/envelope/edit/${envelopeToEdit.id}?token=${encodeURIComponent(embeddedToken)}#${hash}`,
|
||||
);
|
||||
} else {
|
||||
const hash = createEmbeddedEnvelopeCreateHash({
|
||||
envelopeType,
|
||||
externalId,
|
||||
features,
|
||||
css,
|
||||
cssVars,
|
||||
darkModeDisabled,
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
`/embed/v2/authoring/envelope/create?token=${encodeURIComponent(embeddedToken)}#${hash}`,
|
||||
);
|
||||
}
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();
|
||||
|
||||
return {
|
||||
root: page,
|
||||
isEmbedded: true,
|
||||
envelopeId: envelopeToEdit?.id,
|
||||
envelopeType,
|
||||
userId: user.id,
|
||||
userEmail: user.email,
|
||||
userName: user.name ?? '',
|
||||
teamId: team.id,
|
||||
};
|
||||
};
|
||||
|
||||
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 getEnvelopeItemDragHandles = (root: Page) =>
|
||||
root.locator('[data-testid^="envelope-item-drag-handle-"]');
|
||||
|
||||
export const getEnvelopeItemRemoveButtons = (root: Page) =>
|
||||
root.locator('[data-testid^="envelope-item-remove-button-"]');
|
||||
|
||||
export const getEnvelopeItemDropzoneInput = (root: Page) =>
|
||||
root.locator('[data-testid="envelope-item-dropzone"] input[type="file"]');
|
||||
|
||||
export const addEnvelopeItemPdf = async (root: Page, fileName = 'embedded-envelope-item.pdf') => {
|
||||
await getEnvelopeItemDropzoneInput(root).setInputFiles({
|
||||
name: fileName,
|
||||
mimeType: 'application/pdf',
|
||||
buffer: examplePdfBuffer,
|
||||
});
|
||||
};
|
||||
|
||||
export const getRecipientEmailInputs = (root: Page) =>
|
||||
root.locator('[data-testid="signer-email-input"]');
|
||||
|
||||
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 getSigningOrderInputs = (root: Page) =>
|
||||
root.locator('[data-testid="signing-order-input"]');
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
export const clickAddMyselfButton = async (root: Page) => {
|
||||
await root.getByRole('button', { name: 'Add Myself' }).click();
|
||||
};
|
||||
|
||||
export const clickAddSignerButton = async (root: Page) => {
|
||||
await root.getByRole('button', { name: 'Add Signer' }).click();
|
||||
};
|
||||
|
||||
export const setRecipientEmail = async (root: Page, index: number, email: string) => {
|
||||
await getRecipientEmailInputs(root).nth(index).fill(email);
|
||||
};
|
||||
|
||||
export const setRecipientName = async (root: Page, index: number, name: string) => {
|
||||
await getRecipientNameInputs(root).nth(index).fill(name);
|
||||
};
|
||||
|
||||
export const setRecipientRole = async (
|
||||
root: Page,
|
||||
index: number,
|
||||
roleLabel:
|
||||
| 'Needs to sign'
|
||||
| 'Needs to approve'
|
||||
| 'Needs to view'
|
||||
| 'Receives copy'
|
||||
| 'Can prepare',
|
||||
) => {
|
||||
const row = getRecipientRows(root).nth(index);
|
||||
|
||||
await row.locator('button[role="combobox"]').first().click();
|
||||
await root.getByRole('option', { name: roleLabel }).click();
|
||||
};
|
||||
|
||||
export const assertRecipientRole = async (
|
||||
root: Page,
|
||||
index: number,
|
||||
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> = {
|
||||
'Needs to sign': 'SIGNER',
|
||||
'Needs to approve': 'APPROVER',
|
||||
'Needs to view': 'VIEWER',
|
||||
'Receives copy': 'CC',
|
||||
'Can prepare': 'ASSISTANT',
|
||||
};
|
||||
|
||||
await expect(row.locator('button[role="combobox"]').first()).toHaveAttribute(
|
||||
'title',
|
||||
roleValueByLabel[roleLabel],
|
||||
);
|
||||
};
|
||||
|
||||
export const toggleSigningOrder = async (root: Page, enabled: boolean) => {
|
||||
const checkbox = root.locator('#signingOrder');
|
||||
const currentState = await checkbox.getAttribute('aria-checked');
|
||||
const isEnabled = currentState === 'true';
|
||||
|
||||
if (isEnabled !== enabled) {
|
||||
await checkbox.click();
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleAllowDictateSigners = async (root: Page, enabled: boolean) => {
|
||||
const checkbox = root.locator('#allowDictateNextSigner');
|
||||
const currentState = await checkbox.getAttribute('aria-checked');
|
||||
const isEnabled = currentState === 'true';
|
||||
|
||||
if (isEnabled !== enabled) {
|
||||
await checkbox.click();
|
||||
}
|
||||
};
|
||||
|
||||
export const setSigningOrderValue = async (root: Page, index: number, value: number) => {
|
||||
const input = getSigningOrderInputs(root).nth(index);
|
||||
await input.fill(value.toString());
|
||||
await input.blur();
|
||||
};
|
||||
|
||||
export const persistEmbeddedEnvelope = async (surface: TEnvelopeEditorSurface) => {
|
||||
if (!surface.isEmbedded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isUpdateFlow =
|
||||
(await surface.root.getByRole('button', { name: 'Update Document' }).count()) > 0 ||
|
||||
(await surface.root.getByRole('button', { name: 'Update Template' }).count()) > 0;
|
||||
|
||||
const actionButtonName = isUpdateFlow
|
||||
? surface.envelopeType === 'DOCUMENT'
|
||||
? 'Update Document'
|
||||
: 'Update Template'
|
||||
: surface.envelopeType === 'DOCUMENT'
|
||||
? 'Create Document'
|
||||
: 'Create Template';
|
||||
|
||||
await surface.root.getByRole('button', { name: actionButtonName }).click();
|
||||
|
||||
const completionHeading = isUpdateFlow
|
||||
? surface.envelopeType === 'DOCUMENT'
|
||||
? 'Document Updated'
|
||||
: 'Template Updated'
|
||||
: surface.envelopeType === 'DOCUMENT'
|
||||
? 'Document Created'
|
||||
: 'Template Created';
|
||||
|
||||
await expect(surface.root.getByRole('heading', { name: completionHeading })).toBeVisible();
|
||||
};
|
||||
|
||||
const resolveEmbeddingToken = async (
|
||||
page: Page,
|
||||
inputToken: string,
|
||||
scope?: string,
|
||||
): Promise<string> => {
|
||||
if (!inputToken.startsWith('api_')) {
|
||||
return inputToken;
|
||||
}
|
||||
|
||||
const response = await page
|
||||
.context()
|
||||
.request.post(`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2/embedding/create-presign-token`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${inputToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: scope ? { scope } : {},
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Failed to exchange API token (${response.status()}): ${text}`);
|
||||
}
|
||||
|
||||
const data: unknown = await response.json();
|
||||
|
||||
if (typeof data !== 'object' || data === null || !('token' in data)) {
|
||||
throw new Error(`Unexpected response shape: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
const token = data.token;
|
||||
|
||||
if (typeof token !== 'string' || token.length === 0) {
|
||||
throw new Error(`Unexpected response shape: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
return token;
|
||||
};
|
||||
Reference in New Issue
Block a user