mirror of
https://github.com/documenso/documenso.git
synced 2026-09-29 16:24:30 +10:00
Ctrl+C copies all the fields in the selection. Before this change, it copied a field only when one field was selected. The editor keeps the ids of the selected fields. When the author selects fields on one page, the editor clears the selection on the other pages.
1326 lines
46 KiB
TypeScript
1326 lines
46 KiB
TypeScript
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 {
|
|
addEnvelopeItemPdf,
|
|
clickAddMyselfButton,
|
|
clickAddSignerButton,
|
|
clickEnvelopeEditorStep,
|
|
getEnvelopeEditorSettingsTrigger,
|
|
getRecipientEmailInputs,
|
|
getRecipientRemoveButtons,
|
|
openDocumentEnvelopeEditor,
|
|
openEmbeddedEnvelopeEditor,
|
|
openTemplateEnvelopeEditor,
|
|
persistEmbeddedEnvelope,
|
|
setRecipientEmail,
|
|
setRecipientName,
|
|
type TEnvelopeEditorSurface,
|
|
} from '../fixtures/envelope-editor';
|
|
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
|
import { getKonvaElementCountForPage, getKonvaTransformerNodeCountForPage } from '../fixtures/konva';
|
|
|
|
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');
|
|
await surface.root.getByTestId('toast-close').click();
|
|
}
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
type FieldButtonName =
|
|
| 'Signature'
|
|
| 'Email'
|
|
| 'Name'
|
|
| 'Initials'
|
|
| 'Date'
|
|
| 'Text'
|
|
| 'Number'
|
|
| 'Radio'
|
|
| 'Checkbox'
|
|
| 'Dropdown';
|
|
|
|
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();
|
|
await expect(canvas).toBeVisible();
|
|
await canvas.click({ position });
|
|
};
|
|
|
|
const selectRecipientInFieldsStep = async (root: Page, recipientIdentifier: string) => {
|
|
await root.locator('button[role="combobox"]').click();
|
|
await root.getByText(recipientIdentifier).click();
|
|
};
|
|
|
|
const selectFieldOnCanvas = async (root: Page, position: { x: number; y: number }) => {
|
|
const canvas = root.locator('.konva-container canvas').first();
|
|
await expect(canvas).toBeVisible();
|
|
await root.waitForTimeout(300);
|
|
// Use force:true to bypass any floating action toolbar buttons that may intercept clicks.
|
|
await canvas.click({ position, force: true });
|
|
};
|
|
|
|
/**
|
|
* Shift+click a field on the canvas to toggle it in/out of the current multi-selection.
|
|
*/
|
|
const shiftClickFieldOnCanvas = async (root: Page, position: { x: number; y: number }) => {
|
|
const canvas = root.locator('.konva-container canvas').first();
|
|
await expect(canvas).toBeVisible();
|
|
await root.waitForTimeout(300);
|
|
// Use force:true to bypass any floating action toolbar buttons that may intercept clicks.
|
|
await canvas.click({ position, modifiers: ['Shift'], force: true });
|
|
};
|
|
|
|
const runAddAndPersistSignatureTextFields = 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 });
|
|
let fieldCount = await getKonvaElementCountForPage(surface.root, 1, '.field-group');
|
|
expect(fieldCount).toBe(1);
|
|
|
|
await placeFieldOnPdf(surface.root, 'Text', { x: 220, y: 240 });
|
|
fieldCount = await getKonvaElementCountForPage(surface.root, 1, '.field-group');
|
|
expect(fieldCount).toBe(2);
|
|
|
|
await clickEnvelopeEditorStep(surface.root, 'upload');
|
|
await expect(surface.root.getByRole('heading', { name: 'Recipients' })).toBeVisible();
|
|
|
|
await clickEnvelopeEditorStep(surface.root, 'addFields');
|
|
await surface.root.locator('.konva-container canvas').first().waitFor({ state: 'visible' });
|
|
await expect(surface.root.getByText('Selected Recipient')).toBeVisible();
|
|
fieldCount = await getKonvaElementCountForPage(surface.root, 1, '.field-group');
|
|
expect(fieldCount).toBe(2);
|
|
|
|
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');
|
|
};
|
|
|
|
// --- Multi-recipient field flow ---
|
|
|
|
type TMultiRecipientFlowResult = {
|
|
externalId: string;
|
|
firstRecipientEmail: string;
|
|
secondRecipientEmail: string;
|
|
};
|
|
|
|
const MULTI_RECIPIENT_VALUES = {
|
|
secondSigner: {
|
|
email: 'second-signer@test.documenso.com',
|
|
name: 'Second Signer',
|
|
},
|
|
};
|
|
|
|
const runMultiRecipientFieldFlow = async (surface: TEnvelopeEditorSurface): Promise<TMultiRecipientFlowResult> => {
|
|
const externalId = `e2e-multi-recip-${nanoid()}`;
|
|
const root = surface.root;
|
|
|
|
if (surface.isEmbedded && !surface.envelopeId) {
|
|
await addEnvelopeItemPdf(root, 'embedded-fields.pdf');
|
|
}
|
|
|
|
await updateExternalId(surface, externalId);
|
|
|
|
// Add two recipients.
|
|
let firstRecipientEmail: string;
|
|
|
|
if (surface.isEmbedded) {
|
|
await setRecipientEmail(root, 0, TEST_FIELD_VALUES.embeddedRecipient.email);
|
|
await setRecipientName(root, 0, TEST_FIELD_VALUES.embeddedRecipient.name);
|
|
firstRecipientEmail = TEST_FIELD_VALUES.embeddedRecipient.email;
|
|
} else {
|
|
await clickAddMyselfButton(root);
|
|
firstRecipientEmail = surface.userEmail;
|
|
}
|
|
|
|
await clickAddSignerButton(root);
|
|
await setRecipientEmail(root, 1, MULTI_RECIPIENT_VALUES.secondSigner.email);
|
|
await setRecipientName(root, 1, MULTI_RECIPIENT_VALUES.secondSigner.name);
|
|
|
|
// Navigate to fields step.
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
await expect(root.getByText('Selected Recipient')).toBeVisible();
|
|
await expect(root.locator('.konva-container canvas').first()).toBeVisible();
|
|
|
|
let fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(0);
|
|
|
|
// Place Signature for recipient #1 (auto-selected).
|
|
await placeFieldOnPdf(root, 'Signature', { x: 120, y: 140 });
|
|
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(1);
|
|
|
|
// Switch recipient and place text field for recipient #2.
|
|
await selectRecipientInFieldsStep(root, MULTI_RECIPIENT_VALUES.secondSigner.email);
|
|
await placeFieldOnPdf(root, 'Text', { x: 220, y: 240 });
|
|
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(2);
|
|
|
|
// Navigate away and back to ensure fields are persisted in the UI.
|
|
await clickEnvelopeEditorStep(root, 'upload');
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(2);
|
|
|
|
// Phase 2: cascade deletion — go back to recipients and remove the second one.
|
|
await clickEnvelopeEditorStep(root, 'upload');
|
|
await expect(getRecipientEmailInputs(root)).toHaveCount(2);
|
|
|
|
await getRecipientRemoveButtons(root).nth(1).click();
|
|
await expect(getRecipientEmailInputs(root)).toHaveCount(1);
|
|
|
|
// Go back to fields and verify cascade removal.
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(1);
|
|
|
|
return {
|
|
externalId,
|
|
firstRecipientEmail,
|
|
secondRecipientEmail: MULTI_RECIPIENT_VALUES.secondSigner.email,
|
|
};
|
|
};
|
|
|
|
const assertMultiRecipientCascadePersistedInDatabase = async ({
|
|
surface,
|
|
externalId,
|
|
firstRecipientEmail,
|
|
}: {
|
|
surface: TEnvelopeEditorSurface;
|
|
externalId: string;
|
|
firstRecipientEmail: string;
|
|
secondRecipientEmail: 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 },
|
|
});
|
|
|
|
// After cascade deletion, only one recipient and one field should remain.
|
|
expect(envelope.recipients).toHaveLength(1);
|
|
expect(envelope.recipients[0].email).toBe(firstRecipientEmail);
|
|
|
|
expect(envelope.fields).toHaveLength(1);
|
|
expect(envelope.fields[0].type).toBe(FieldType.SIGNATURE);
|
|
expect(envelope.fields[0].recipientId).toBe(envelope.recipients[0].id);
|
|
};
|
|
|
|
// --- All 10 field types flow ---
|
|
|
|
type TAllFieldTypesFlowResult = {
|
|
externalId: string;
|
|
};
|
|
|
|
const runAllFieldTypesFlow = async (surface: TEnvelopeEditorSurface): Promise<TAllFieldTypesFlowResult> => {
|
|
const externalId = `e2e-all-fields-${nanoid()}`;
|
|
const root = surface.root;
|
|
|
|
if (surface.isEmbedded && !surface.envelopeId) {
|
|
await addEnvelopeItemPdf(root, 'embedded-fields.pdf');
|
|
}
|
|
|
|
await updateExternalId(surface, externalId);
|
|
await setupRecipientsForFieldPlacement(surface);
|
|
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
await expect(root.locator('.konva-container canvas').first()).toBeVisible();
|
|
|
|
// Place and configure each field type immediately after placement.
|
|
// After placeFieldOnPdf, the sidebar shows the field's config form (field is selected in React state).
|
|
|
|
// 1. Signature: place and set fontSize to 24.
|
|
await placeFieldOnPdf(root, 'Signature', { x: 120, y: 50 });
|
|
await root.locator('[data-testid="field-form-fontSize"]').fill('24');
|
|
|
|
// 2. Email: place and set textAlign to center.
|
|
await placeFieldOnPdf(root, 'Email', { x: 120, y: 100 });
|
|
await root.locator('[data-testid="field-form-textAlign"]').click();
|
|
await root.getByRole('option', { name: 'Center' }).click();
|
|
|
|
// 3. Name: place and set textAlign to right.
|
|
await placeFieldOnPdf(root, 'Name', { x: 120, y: 150 });
|
|
await root.locator('[data-testid="field-form-textAlign"]').click();
|
|
await root.getByRole('option', { name: 'Right' }).click();
|
|
|
|
// 4. Initials: place and set fontSize to 16.
|
|
await placeFieldOnPdf(root, 'Initials', { x: 120, y: 200 });
|
|
await root.locator('[data-testid="field-form-fontSize"]').fill('16');
|
|
|
|
// 5. Date: place and set textAlign to center.
|
|
await placeFieldOnPdf(root, 'Date', { x: 120, y: 250 });
|
|
await root.locator('[data-testid="field-form-textAlign"]').click();
|
|
await root.getByRole('option', { name: 'Center' }).click();
|
|
|
|
// 6. Text: place and configure label, placeholder, text, characterLimit, required.
|
|
await placeFieldOnPdf(root, 'Text', { x: 120, y: 300 });
|
|
await root.locator('[data-testid="field-form-label"]').fill('Test Label');
|
|
await root.locator('[data-testid="field-form-placeholder"]').fill('Enter text here');
|
|
await root.locator('[data-testid="field-form-text"]').fill('Default text value');
|
|
await root.locator('[data-testid="field-form-characterLimit"]').fill('100');
|
|
await root.locator('[data-testid="field-form-required"]').click();
|
|
|
|
// 7. Number: place and configure label, placeholder, numberFormat, minValue, maxValue, required.
|
|
await placeFieldOnPdf(root, 'Number', { x: 120, y: 350 });
|
|
await root.locator('[data-testid="field-form-label"]').fill('Amount');
|
|
await root.locator('[data-testid="field-form-placeholder"]').fill('0.00');
|
|
await root.locator('[data-testid="field-form-numberFormat"]').click();
|
|
await root.getByRole('option', { name: '123,456,789.00' }).click();
|
|
await root.locator('[data-testid="field-form-minValue"]').fill('0');
|
|
await root.locator('[data-testid="field-form-maxValue"]').fill('1000');
|
|
await root.locator('[data-testid="field-form-required"]').click();
|
|
|
|
// 8. Radio: place and configure two options, pre-select first, set direction to horizontal.
|
|
await placeFieldOnPdf(root, 'Radio', { x: 120, y: 400 });
|
|
|
|
// The first option already exists with default value "Default value". Fill it.
|
|
await root.locator('[data-testid="field-form-values-0-value"]').fill('Option A');
|
|
|
|
// Add a second option.
|
|
await root.locator('[data-testid="field-form-values-add"]').click();
|
|
await root.locator('[data-testid="field-form-values-1-value"]').fill('Option B');
|
|
|
|
// Pre-select the first option (click its checkbox).
|
|
await root.locator('[data-testid="field-form-values-0-checked"]').click();
|
|
|
|
// Set direction to horizontal.
|
|
await root.locator('[data-testid="field-form-direction"]').click();
|
|
await root.getByRole('option', { name: 'Horizontal' }).click();
|
|
|
|
// 9. Checkbox: place and configure two options, check both, set validation rule.
|
|
await placeFieldOnPdf(root, 'Checkbox', { x: 120, y: 450 });
|
|
|
|
// Fill first option value.
|
|
await root.locator('[data-testid="field-form-values-0-value"]').fill('Check A');
|
|
|
|
// Add a second option.
|
|
await root.locator('[data-testid="field-form-values-add"]').click();
|
|
await root.locator('[data-testid="field-form-values-1-value"]').fill('Check B');
|
|
|
|
// Check both options (click their checkboxes).
|
|
await root.locator('[data-testid="field-form-values-0-checked"]').click();
|
|
await root.locator('[data-testid="field-form-values-1-checked"]').click();
|
|
|
|
// Set validation: "Select at least" 1.
|
|
await root.locator('[data-testid="field-form-validationRule"]').click();
|
|
await root.getByRole('option', { name: 'Select at least' }).click();
|
|
|
|
// Set validation length to 1.
|
|
await root.locator('[data-testid="field-form-validationLength"]').click();
|
|
await root.getByRole('option', { name: '1', exact: true }).click();
|
|
|
|
// 10. Dropdown: place and configure two options, set default value.
|
|
await placeFieldOnPdf(root, 'Dropdown', { x: 120, y: 500 });
|
|
|
|
// First option already has "Option 1". Change it to "Red".
|
|
await root.locator('[data-testid="field-form-values-0-value"]').fill('Red');
|
|
|
|
// Add a second option.
|
|
await root.locator('[data-testid="field-form-values-add"]').click();
|
|
await root.locator('[data-testid="field-form-values-1-value"]').clear();
|
|
await root.locator('[data-testid="field-form-values-1-value"]').fill('Blue');
|
|
|
|
// Set default value to "Red".
|
|
await root.locator('[data-testid="field-form-defaultValue"]').click();
|
|
await root.getByRole('option', { name: 'Red' }).click();
|
|
|
|
let fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(10);
|
|
|
|
// Wait briefly for auto-save to fire on the last configured field.
|
|
await root.waitForTimeout(500);
|
|
|
|
// Navigate away and back to verify persistence.
|
|
await clickEnvelopeEditorStep(root, 'upload');
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(10);
|
|
|
|
return { externalId };
|
|
};
|
|
|
|
const assertAllFieldTypesPersistedInDatabase = async ({
|
|
surface,
|
|
externalId,
|
|
}: {
|
|
surface: TEnvelopeEditorSurface;
|
|
externalId: string;
|
|
}) => {
|
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
|
where: {
|
|
externalId,
|
|
userId: surface.userId,
|
|
teamId: surface.teamId,
|
|
type: surface.envelopeType,
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
include: { fields: true },
|
|
});
|
|
|
|
expect(envelope.fields).toHaveLength(10);
|
|
|
|
const fieldsByType = new Map(envelope.fields.map((f) => [f.type, f]));
|
|
|
|
// Helper to safely access fieldMeta as a record.
|
|
const meta = (type: FieldType): Record<string, unknown> => {
|
|
const field = fieldsByType.get(type);
|
|
expect(field).toBeDefined();
|
|
const fieldMeta = field!.fieldMeta;
|
|
expect(typeof fieldMeta).toBe('object');
|
|
expect(fieldMeta).not.toBeNull();
|
|
return fieldMeta as Record<string, unknown>;
|
|
};
|
|
|
|
// SIGNATURE
|
|
expect(meta(FieldType.SIGNATURE).type).toBe('signature');
|
|
expect(meta(FieldType.SIGNATURE).fontSize).toBe(24);
|
|
|
|
// EMAIL
|
|
expect(meta(FieldType.EMAIL).type).toBe('email');
|
|
expect(meta(FieldType.EMAIL).textAlign).toBe('center');
|
|
|
|
// NAME
|
|
expect(meta(FieldType.NAME).type).toBe('name');
|
|
expect(meta(FieldType.NAME).textAlign).toBe('right');
|
|
|
|
// INITIALS
|
|
expect(meta(FieldType.INITIALS).type).toBe('initials');
|
|
expect(meta(FieldType.INITIALS).fontSize).toBe(16);
|
|
|
|
// DATE
|
|
expect(meta(FieldType.DATE).type).toBe('date');
|
|
expect(meta(FieldType.DATE).textAlign).toBe('center');
|
|
|
|
// TEXT
|
|
expect(meta(FieldType.TEXT).type).toBe('text');
|
|
expect(meta(FieldType.TEXT).label).toBe('Test Label');
|
|
expect(meta(FieldType.TEXT).placeholder).toBe('Enter text here');
|
|
expect(meta(FieldType.TEXT).text).toBe('Default text value');
|
|
expect(meta(FieldType.TEXT).characterLimit).toBe(100);
|
|
expect(meta(FieldType.TEXT).required).toBe(true);
|
|
|
|
// NUMBER
|
|
expect(meta(FieldType.NUMBER).type).toBe('number');
|
|
expect(meta(FieldType.NUMBER).label).toBe('Amount');
|
|
expect(meta(FieldType.NUMBER).placeholder).toBe('0.00');
|
|
expect(meta(FieldType.NUMBER).numberFormat).toBe('123,456,789.00');
|
|
expect(meta(FieldType.NUMBER).minValue).toBe(0);
|
|
expect(meta(FieldType.NUMBER).maxValue).toBe(1000);
|
|
expect(meta(FieldType.NUMBER).required).toBe(true);
|
|
|
|
// RADIO
|
|
expect(meta(FieldType.RADIO).type).toBe('radio');
|
|
expect(meta(FieldType.RADIO).direction).toBe('horizontal');
|
|
const radioValues = meta(FieldType.RADIO).values as Array<{
|
|
value: string;
|
|
checked: boolean;
|
|
}>;
|
|
expect(radioValues).toHaveLength(2);
|
|
expect(radioValues[0].value).toBe('Option A');
|
|
expect(radioValues[0].checked).toBe(true);
|
|
expect(radioValues[1].value).toBe('Option B');
|
|
expect(radioValues[1].checked).toBe(false);
|
|
|
|
// CHECKBOX
|
|
expect(meta(FieldType.CHECKBOX).type).toBe('checkbox');
|
|
expect(meta(FieldType.CHECKBOX).validationRule).toBe('Select at least');
|
|
expect(meta(FieldType.CHECKBOX).validationLength).toBe(1);
|
|
const checkboxValues = meta(FieldType.CHECKBOX).values as Array<{
|
|
value: string;
|
|
checked: boolean;
|
|
}>;
|
|
expect(checkboxValues).toHaveLength(2);
|
|
expect(checkboxValues[0].value).toBe('Check A');
|
|
expect(checkboxValues[0].checked).toBe(true);
|
|
expect(checkboxValues[1].value).toBe('Check B');
|
|
expect(checkboxValues[1].checked).toBe(true);
|
|
|
|
// DROPDOWN
|
|
expect(meta(FieldType.DROPDOWN).type).toBe('dropdown');
|
|
expect(meta(FieldType.DROPDOWN).defaultValue).toBe('Red');
|
|
const dropdownValues = meta(FieldType.DROPDOWN).values as Array<{ value: string }>;
|
|
expect(dropdownValues).toHaveLength(2);
|
|
expect(dropdownValues[0].value).toBe('Red');
|
|
expect(dropdownValues[1].value).toBe('Blue');
|
|
};
|
|
|
|
// --- Duplicate and delete fields flow ---
|
|
|
|
type TDuplicateDeleteFlowResult = {
|
|
externalId: string;
|
|
};
|
|
|
|
const runDuplicateDeleteFieldFlow = async (surface: TEnvelopeEditorSurface): Promise<TDuplicateDeleteFlowResult> => {
|
|
const externalId = `e2e-dup-del-${nanoid()}`;
|
|
const root = surface.root;
|
|
|
|
if (surface.isEmbedded && !surface.envelopeId) {
|
|
await addEnvelopeItemPdf(root, 'embedded-fields.pdf');
|
|
}
|
|
|
|
await updateExternalId(surface, externalId);
|
|
await setupRecipientsForFieldPlacement(surface);
|
|
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
await expect(root.locator('.konva-container canvas').first()).toBeVisible();
|
|
|
|
// Place a Signature field.
|
|
await placeFieldOnPdf(root, 'Signature', { x: 150, y: 150 });
|
|
let fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(1);
|
|
|
|
// Select the field on canvas to show the action toolbar.
|
|
await selectFieldOnCanvas(root, { x: 150, y: 150 });
|
|
await expect(root.locator('button[title="Duplicate"]')).toBeVisible();
|
|
|
|
// Duplicate the field.
|
|
await root.locator('button[title="Duplicate"]').click();
|
|
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(2);
|
|
|
|
// Navigate away and back to persist changes.
|
|
await clickEnvelopeEditorStep(root, 'upload');
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(2);
|
|
|
|
// Select a field and delete it via the Remove button.
|
|
await selectFieldOnCanvas(root, { x: 150, y: 150 });
|
|
await expect(root.locator('button[title="Remove"]')).toBeVisible();
|
|
await root.locator('button[title="Remove"]').click();
|
|
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(1);
|
|
|
|
// Navigate away and back to verify persistence.
|
|
await clickEnvelopeEditorStep(root, 'upload');
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(1);
|
|
|
|
return { externalId };
|
|
};
|
|
|
|
const assertDuplicateDeleteFieldPersistedInDatabase = async ({
|
|
surface,
|
|
externalId,
|
|
}: {
|
|
surface: TEnvelopeEditorSurface;
|
|
externalId: string;
|
|
}) => {
|
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
|
where: {
|
|
externalId,
|
|
userId: surface.userId,
|
|
teamId: surface.teamId,
|
|
type: surface.envelopeType,
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
include: { fields: true },
|
|
});
|
|
|
|
// After duplicating (2 fields) then deleting one, exactly 1 SIGNATURE field should remain.
|
|
expect(envelope.fields).toHaveLength(1);
|
|
expect(envelope.fields[0].type).toBe(FieldType.SIGNATURE);
|
|
};
|
|
|
|
// --- Copy and paste fields flow ---
|
|
|
|
type TCopyPasteFlowResult = {
|
|
externalId: string;
|
|
};
|
|
|
|
const runCopyPasteFieldFlow = async (surface: TEnvelopeEditorSurface): Promise<TCopyPasteFlowResult> => {
|
|
const externalId = `e2e-copy-paste-${nanoid()}`;
|
|
const root = surface.root;
|
|
|
|
await updateExternalId(surface, externalId);
|
|
await setupRecipientsForFieldPlacement(surface);
|
|
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
await expect(root.locator('.konva-container canvas').first()).toBeVisible();
|
|
|
|
await placeFieldOnPdf(root, 'Signature', { x: 150, y: 150 });
|
|
await selectFieldOnCanvas(root, { x: 150, y: 150 });
|
|
|
|
await root.keyboard.press('ControlOrMeta+c');
|
|
await root.keyboard.press('ControlOrMeta+v');
|
|
await root.keyboard.press('ControlOrMeta+v');
|
|
|
|
await expect.poll(async () => getKonvaElementCountForPage(root, 1, '.field-group')).toBe(3);
|
|
|
|
// Navigate away and back to persist changes.
|
|
await clickEnvelopeEditorStep(root, 'upload');
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
await expect.poll(async () => getKonvaElementCountForPage(root, 1, '.field-group')).toBe(3);
|
|
|
|
return { externalId };
|
|
};
|
|
|
|
const assertCopyPasteFieldPersistedInDatabase = async ({
|
|
surface,
|
|
externalId,
|
|
}: {
|
|
surface: TEnvelopeEditorSurface;
|
|
externalId: string;
|
|
}) => {
|
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
|
where: {
|
|
externalId,
|
|
userId: surface.userId,
|
|
teamId: surface.teamId,
|
|
type: surface.envelopeType,
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
include: { fields: true },
|
|
});
|
|
|
|
expect(envelope.fields).toHaveLength(3);
|
|
expect(envelope.fields.every((field) => field.type === FieldType.SIGNATURE)).toBe(true);
|
|
|
|
// Each paste is offset from the previous one, so no two fields share a position.
|
|
const positions = new Set(envelope.fields.map((field) => `${field.positionX}:${field.positionY}`));
|
|
expect(positions.size).toBe(3);
|
|
};
|
|
|
|
const runMultiCopyPasteFieldFlow = async (surface: TEnvelopeEditorSurface): Promise<TCopyPasteFlowResult> => {
|
|
const externalId = `e2e-multi-copy-paste-${nanoid()}`;
|
|
const root = surface.root;
|
|
|
|
await updateExternalId(surface, externalId);
|
|
await setupRecipientsForFieldPlacement(surface);
|
|
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
await expect(root.locator('.konva-container canvas').first()).toBeVisible();
|
|
|
|
await placeFieldOnPdf(root, 'Signature', FIELD_A_POSITION);
|
|
await placeFieldOnPdf(root, 'Name', FIELD_B_POSITION);
|
|
await marqueeSelectFieldsOnCanvas(root, { x: 50, y: 100 }, { x: 260, y: 290 });
|
|
await expect.poll(async () => getKonvaTransformerNodeCountForPage(root, 1)).toBe(2);
|
|
|
|
await root.keyboard.press('ControlOrMeta+c');
|
|
await root.keyboard.press('ControlOrMeta+v');
|
|
|
|
await expect.poll(async () => getKonvaElementCountForPage(root, 1, '.field-group')).toBe(4);
|
|
|
|
// Navigate away and back to persist changes.
|
|
await clickEnvelopeEditorStep(root, 'upload');
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
await expect.poll(async () => getKonvaElementCountForPage(root, 1, '.field-group')).toBe(4);
|
|
|
|
return { externalId };
|
|
};
|
|
|
|
const assertMultiCopyPasteFieldPersistedInDatabase = async ({
|
|
surface,
|
|
externalId,
|
|
}: {
|
|
surface: TEnvelopeEditorSurface;
|
|
externalId: string;
|
|
}) => {
|
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
|
where: {
|
|
externalId,
|
|
userId: surface.userId,
|
|
teamId: surface.teamId,
|
|
type: surface.envelopeType,
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
include: { fields: true },
|
|
});
|
|
|
|
const types = envelope.fields.map((field) => field.type).sort();
|
|
expect(types).toEqual([FieldType.NAME, FieldType.NAME, FieldType.SIGNATURE, FieldType.SIGNATURE]);
|
|
};
|
|
|
|
// --- Change field type flow ---
|
|
|
|
type TChangeFieldTypeFlowResult = {
|
|
externalId: string;
|
|
};
|
|
|
|
const FIELD_A_POSITION = { x: 150, y: 150 };
|
|
const FIELD_B_POSITION = { x: 150, y: 250 };
|
|
|
|
const changeFieldTypeViaToolbar = async (root: Page, newTypeLabel: FieldButtonName) => {
|
|
await expect(root.locator('button[title="Change Field Type"]')).toBeVisible();
|
|
await root.locator('button[title="Change Field Type"]').click();
|
|
|
|
// The CommandDialog uses role="option" for items; sidebar palette buttons use role="button".
|
|
const option = root.getByRole('option', { name: newTypeLabel, exact: true });
|
|
await expect(option).toBeVisible();
|
|
await option.click();
|
|
|
|
// Wait for the CommandDialog to close (selection persists so the toolbar remains).
|
|
await expect(root.getByRole('dialog')).toHaveCount(0);
|
|
};
|
|
|
|
/**
|
|
* Multi-select fields on the konva canvas by drawing a marquee selection rectangle.
|
|
*
|
|
* The editor's stage mousedown/mousemove/mouseup handlers create a Konva selection
|
|
* rectangle when the user drags on empty stage area. All field groups that intersect
|
|
* the rectangle are selected at once. This is the canonical multi-select gesture.
|
|
*/
|
|
const marqueeSelectFieldsOnCanvas = async (
|
|
root: Page,
|
|
start: { x: number; y: number },
|
|
end: { x: number; y: number },
|
|
) => {
|
|
const canvas = root.locator('.konva-container canvas').first();
|
|
await expect(canvas).toBeVisible();
|
|
|
|
const box = await canvas.boundingBox();
|
|
|
|
if (!box) {
|
|
throw new Error('Canvas bounding box not available for marquee selection.');
|
|
}
|
|
|
|
// The marquee gesture must start on empty stage (not on a field) and pass through
|
|
// intermediate points so the editor's mousemove handler can grow the rectangle.
|
|
await root.mouse.move(box.x + start.x, box.y + start.y);
|
|
await root.mouse.down();
|
|
await root.mouse.move(box.x + (start.x + end.x) / 2, box.y + (start.y + end.y) / 2, { steps: 5 });
|
|
await root.mouse.move(box.x + end.x, box.y + end.y, { steps: 5 });
|
|
await root.mouse.up();
|
|
};
|
|
|
|
const runChangeFieldTypeFlow = async (surface: TEnvelopeEditorSurface): Promise<TChangeFieldTypeFlowResult> => {
|
|
const externalId = `e2e-change-type-${nanoid()}`;
|
|
const root = surface.root;
|
|
|
|
if (surface.isEmbedded && !surface.envelopeId) {
|
|
await addEnvelopeItemPdf(root, 'embedded-fields.pdf');
|
|
}
|
|
|
|
await updateExternalId(surface, externalId);
|
|
await setupRecipientsForFieldPlacement(surface);
|
|
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
await expect(root.locator('.konva-container canvas').first()).toBeVisible();
|
|
|
|
// Place two fields of different types: Signature (A) and Name (B).
|
|
await placeFieldOnPdf(root, 'Signature', FIELD_A_POSITION);
|
|
await placeFieldOnPdf(root, 'Name', FIELD_B_POSITION);
|
|
let fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(2);
|
|
|
|
// --- Phase 1: single field type change ---
|
|
// Select field A (Signature) and change it to Text via the toolbar.
|
|
await selectFieldOnCanvas(root, FIELD_A_POSITION);
|
|
await changeFieldTypeViaToolbar(root, 'Text');
|
|
|
|
// Field count must remain stable -- changing type doesn't add/remove fields.
|
|
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(2);
|
|
|
|
// Navigate away and back to verify the change is persisted in local state.
|
|
await clickEnvelopeEditorStep(root, 'upload');
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(2);
|
|
|
|
// --- Phase 2: multi-field type change ---
|
|
// Use a marquee drag-selection rectangle to capture both fields at once.
|
|
// Fields are at (150, 150) and (150, 250) with default dims ~90x30; drag from
|
|
// (50, 100) to (260, 290) encloses both with margin.
|
|
await marqueeSelectFieldsOnCanvas(root, { x: 50, y: 100 }, { x: 260, y: 290 });
|
|
|
|
// With mixed-type selection (Text + Name), change both to Date.
|
|
await changeFieldTypeViaToolbar(root, 'Date');
|
|
|
|
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(2);
|
|
|
|
// Navigate away and back to verify persistence.
|
|
await clickEnvelopeEditorStep(root, 'upload');
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
|
|
expect(fieldCount).toBe(2);
|
|
|
|
return { externalId };
|
|
};
|
|
|
|
const assertChangeFieldTypePersistedInDatabase = async ({
|
|
surface,
|
|
externalId,
|
|
}: {
|
|
surface: TEnvelopeEditorSurface;
|
|
externalId: string;
|
|
}) => {
|
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
|
where: {
|
|
externalId,
|
|
userId: surface.userId,
|
|
teamId: surface.teamId,
|
|
type: surface.envelopeType,
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
include: { fields: true },
|
|
});
|
|
|
|
// Started with Signature + Name, then both were converted to Date.
|
|
// Use sorted .map() in the assertion so any failure prints which types were found.
|
|
const actualTypes = envelope.fields.map((field) => field.type).sort();
|
|
const expectedTypes = [FieldType.DATE, FieldType.DATE];
|
|
|
|
expect(envelope.fields).toHaveLength(2);
|
|
expect(actualTypes).toEqual(expectedTypes);
|
|
|
|
// Each field's meta must have been reset to the new type's defaults.
|
|
const actualMetaTypes = envelope.fields.map((field) => getFieldMetaType(field.fieldMeta)).sort();
|
|
expect(actualMetaTypes).toEqual(['date', 'date']);
|
|
};
|
|
|
|
// --- Shift+click multi-select flow ---
|
|
|
|
type TShiftClickFlowResult = {
|
|
externalId: string;
|
|
};
|
|
|
|
const SHIFT_CLICK_FIELD_POSITIONS = {
|
|
signature: { x: 150, y: 120 },
|
|
text: { x: 150, y: 260 },
|
|
name: { x: 150, y: 400 },
|
|
};
|
|
|
|
const runShiftClickMultiSelectFlow = async (surface: TEnvelopeEditorSurface): Promise<TShiftClickFlowResult> => {
|
|
const externalId = `e2e-shift-click-${nanoid()}`;
|
|
const root = surface.root;
|
|
|
|
if (surface.isEmbedded && !surface.envelopeId) {
|
|
await addEnvelopeItemPdf(root, 'embedded-fields.pdf');
|
|
}
|
|
|
|
await updateExternalId(surface, externalId);
|
|
await setupRecipientsForFieldPlacement(surface);
|
|
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
await expect(root.locator('.konva-container canvas').first()).toBeVisible();
|
|
|
|
// Place three fields, spaced far enough apart that their action toolbars don't
|
|
// overlap a neighbouring field's click target.
|
|
await placeFieldOnPdf(root, 'Signature', SHIFT_CLICK_FIELD_POSITIONS.signature);
|
|
await placeFieldOnPdf(root, 'Text', SHIFT_CLICK_FIELD_POSITIONS.text);
|
|
await placeFieldOnPdf(root, 'Name', SHIFT_CLICK_FIELD_POSITIONS.name);
|
|
expect(await getKonvaElementCountForPage(root, 1, '.field-group')).toBe(3);
|
|
|
|
// A plain click selects exactly one field.
|
|
await selectFieldOnCanvas(root, SHIFT_CLICK_FIELD_POSITIONS.signature);
|
|
await expect.poll(() => getKonvaTransformerNodeCountForPage(root, 1)).toBe(1);
|
|
|
|
// Shift+click a second field ADDS it to the selection (the new behaviour).
|
|
await shiftClickFieldOnCanvas(root, SHIFT_CLICK_FIELD_POSITIONS.text);
|
|
await expect.poll(() => getKonvaTransformerNodeCountForPage(root, 1)).toBe(2);
|
|
|
|
// Shift+click an already-selected field REMOVES it from the selection.
|
|
await shiftClickFieldOnCanvas(root, SHIFT_CLICK_FIELD_POSITIONS.signature);
|
|
await expect.poll(() => getKonvaTransformerNodeCountForPage(root, 1)).toBe(1);
|
|
|
|
// Shift+click it again RE-ADDS it, leaving Signature + Text selected and Name excluded.
|
|
await shiftClickFieldOnCanvas(root, SHIFT_CLICK_FIELD_POSITIONS.signature);
|
|
await expect.poll(() => getKonvaTransformerNodeCountForPage(root, 1)).toBe(2);
|
|
|
|
// Delete the two selected fields via the floating action toolbar. Only the
|
|
// un-selected Name field should remain -- proving the multi-selection contained
|
|
// exactly the two Shift-clicked fields.
|
|
await expect(root.locator('button[title="Remove"]')).toBeVisible();
|
|
await root.locator('button[title="Remove"]').click();
|
|
expect(await getKonvaElementCountForPage(root, 1, '.field-group')).toBe(1);
|
|
|
|
// Navigate away and back to verify persistence.
|
|
await clickEnvelopeEditorStep(root, 'upload');
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
expect(await getKonvaElementCountForPage(root, 1, '.field-group')).toBe(1);
|
|
|
|
return { externalId };
|
|
};
|
|
|
|
const assertShiftClickMultiSelectPersistedInDatabase = async ({
|
|
surface,
|
|
externalId,
|
|
}: {
|
|
surface: TEnvelopeEditorSurface;
|
|
externalId: string;
|
|
}) => {
|
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
|
where: {
|
|
externalId,
|
|
userId: surface.userId,
|
|
teamId: surface.teamId,
|
|
type: surface.envelopeType,
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
include: { fields: true },
|
|
});
|
|
|
|
// Signature + Text were multi-selected via Shift+click and deleted; only Name remains.
|
|
expect(envelope.fields).toHaveLength(1);
|
|
expect(envelope.fields[0].type).toBe(FieldType.NAME);
|
|
};
|
|
|
|
// --- Test describe blocks ---
|
|
|
|
test.describe('document editor', () => {
|
|
test('shift+click adds and removes fields from the selection', async ({ page }) => {
|
|
const surface = await openDocumentEnvelopeEditor(page);
|
|
const result = await runShiftClickMultiSelectFlow(surface);
|
|
|
|
await assertShiftClickMultiSelectPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('add and persist signature/text fields', async ({ page }) => {
|
|
const surface = await openDocumentEnvelopeEditor(page);
|
|
const result = await runAddAndPersistSignatureTextFields(surface);
|
|
|
|
await assertFieldsPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('multi-recipient field placement, switching, and cascade deletion', async ({ page }) => {
|
|
const surface = await openDocumentEnvelopeEditor(page);
|
|
const result = await runMultiRecipientFieldFlow(surface);
|
|
|
|
await assertMultiRecipientCascadePersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('duplicate and delete fields via canvas action toolbar', async ({ page }) => {
|
|
const surface = await openDocumentEnvelopeEditor(page);
|
|
const result = await runDuplicateDeleteFieldFlow(surface);
|
|
|
|
await assertDuplicateDeleteFieldPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('copy and paste fields with keyboard shortcuts', async ({ page }) => {
|
|
const surface = await openDocumentEnvelopeEditor(page);
|
|
const result = await runCopyPasteFieldFlow(surface);
|
|
|
|
await assertCopyPasteFieldPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('copy and paste multiple selected fields with keyboard shortcuts', async ({ page }) => {
|
|
const surface = await openDocumentEnvelopeEditor(page);
|
|
const result = await runMultiCopyPasteFieldFlow(surface);
|
|
|
|
await assertMultiCopyPasteFieldPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('place and configure all 10 field types', async ({ page }) => {
|
|
const surface = await openDocumentEnvelopeEditor(page);
|
|
const result = await runAllFieldTypesFlow(surface);
|
|
|
|
await assertAllFieldTypesPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('change field type via canvas action toolbar (single and multi-select)', async ({ page }) => {
|
|
const surface = await openDocumentEnvelopeEditor(page);
|
|
const result = await runChangeFieldTypeFlow(surface);
|
|
|
|
await assertChangeFieldTypePersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
});
|
|
|
|
test.describe('template editor', () => {
|
|
test('shift+click adds and removes fields from the selection', async ({ page }) => {
|
|
const surface = await openTemplateEnvelopeEditor(page);
|
|
const result = await runShiftClickMultiSelectFlow(surface);
|
|
|
|
await assertShiftClickMultiSelectPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('add and persist signature/text fields', async ({ page }) => {
|
|
const surface = await openTemplateEnvelopeEditor(page);
|
|
const result = await runAddAndPersistSignatureTextFields(surface);
|
|
|
|
await assertFieldsPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('multi-recipient field placement, switching, and cascade deletion', async ({ page }) => {
|
|
const surface = await openTemplateEnvelopeEditor(page);
|
|
const result = await runMultiRecipientFieldFlow(surface);
|
|
|
|
await assertMultiRecipientCascadePersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('duplicate and delete fields via canvas action toolbar', async ({ page }) => {
|
|
const surface = await openTemplateEnvelopeEditor(page);
|
|
const result = await runDuplicateDeleteFieldFlow(surface);
|
|
|
|
await assertDuplicateDeleteFieldPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('copy and paste fields with keyboard shortcuts', async ({ page }) => {
|
|
const surface = await openTemplateEnvelopeEditor(page);
|
|
const result = await runCopyPasteFieldFlow(surface);
|
|
|
|
await assertCopyPasteFieldPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('copy and paste multiple selected fields with keyboard shortcuts', async ({ page }) => {
|
|
const surface = await openTemplateEnvelopeEditor(page);
|
|
const result = await runMultiCopyPasteFieldFlow(surface);
|
|
|
|
await assertMultiCopyPasteFieldPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('place and configure all 10 field types', async ({ page }) => {
|
|
const surface = await openTemplateEnvelopeEditor(page);
|
|
const result = await runAllFieldTypesFlow(surface);
|
|
|
|
await assertAllFieldTypesPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('change field type via canvas action toolbar (single and multi-select)', async ({ page }) => {
|
|
const surface = await openTemplateEnvelopeEditor(page);
|
|
const result = await runChangeFieldTypeFlow(surface);
|
|
|
|
await assertChangeFieldTypePersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
});
|
|
|
|
test.describe('embedded create', () => {
|
|
test('shift+click adds and removes fields from the selection', async ({ page }) => {
|
|
const surface = await openEmbeddedEnvelopeEditor(page, {
|
|
envelopeType: 'DOCUMENT',
|
|
tokenNamePrefix: 'e2e-embed-shift-click',
|
|
});
|
|
const result = await runShiftClickMultiSelectFlow(surface);
|
|
|
|
await persistEmbeddedEnvelope(surface);
|
|
|
|
await assertShiftClickMultiSelectPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('add and persist signature/text fields', async ({ page }) => {
|
|
const surface = await openEmbeddedEnvelopeEditor(page, {
|
|
envelopeType: 'DOCUMENT',
|
|
tokenNamePrefix: 'e2e-embed-fields',
|
|
});
|
|
const result = await runAddAndPersistSignatureTextFields(surface);
|
|
|
|
await persistEmbeddedEnvelope(surface);
|
|
|
|
await assertFieldsPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('multi-recipient field placement, switching, and cascade deletion', async ({ page }) => {
|
|
const surface = await openEmbeddedEnvelopeEditor(page, {
|
|
envelopeType: 'DOCUMENT',
|
|
tokenNamePrefix: 'e2e-embed-multi-recip',
|
|
});
|
|
const result = await runMultiRecipientFieldFlow(surface);
|
|
|
|
await persistEmbeddedEnvelope(surface);
|
|
|
|
await assertMultiRecipientCascadePersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('duplicate and delete fields via canvas action toolbar', async ({ page }) => {
|
|
const surface = await openEmbeddedEnvelopeEditor(page, {
|
|
envelopeType: 'DOCUMENT',
|
|
tokenNamePrefix: 'e2e-embed-dup-del',
|
|
});
|
|
const result = await runDuplicateDeleteFieldFlow(surface);
|
|
|
|
await persistEmbeddedEnvelope(surface);
|
|
|
|
await assertDuplicateDeleteFieldPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('place and configure all 10 field types', async ({ page }) => {
|
|
const surface = await openEmbeddedEnvelopeEditor(page, {
|
|
envelopeType: 'DOCUMENT',
|
|
tokenNamePrefix: 'e2e-embed-all-fields',
|
|
});
|
|
const result = await runAllFieldTypesFlow(surface);
|
|
|
|
await persistEmbeddedEnvelope(surface);
|
|
|
|
await assertAllFieldTypesPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('change field type via canvas action toolbar (single and multi-select)', async ({ page }) => {
|
|
const surface = await openEmbeddedEnvelopeEditor(page, {
|
|
envelopeType: 'DOCUMENT',
|
|
tokenNamePrefix: 'e2e-embed-change-type',
|
|
});
|
|
const result = await runChangeFieldTypeFlow(surface);
|
|
|
|
await persistEmbeddedEnvelope(surface);
|
|
|
|
await assertChangeFieldTypePersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
});
|
|
|
|
test.describe('embedded edit', () => {
|
|
test('shift+click adds and removes fields from the selection', async ({ page }) => {
|
|
const surface = await openEmbeddedEnvelopeEditor(page, {
|
|
envelopeType: 'TEMPLATE',
|
|
mode: 'edit',
|
|
tokenNamePrefix: 'e2e-embed-shift-click',
|
|
});
|
|
const result = await runShiftClickMultiSelectFlow(surface);
|
|
|
|
await persistEmbeddedEnvelope(surface);
|
|
|
|
await assertShiftClickMultiSelectPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('add and persist signature/text fields', async ({ page }) => {
|
|
const surface = await openEmbeddedEnvelopeEditor(page, {
|
|
envelopeType: 'TEMPLATE',
|
|
mode: 'edit',
|
|
tokenNamePrefix: 'e2e-embed-fields',
|
|
});
|
|
const result = await runAddAndPersistSignatureTextFields(surface);
|
|
|
|
await persistEmbeddedEnvelope(surface);
|
|
|
|
await assertFieldsPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('multi-recipient field placement, switching, and cascade deletion', async ({ page }) => {
|
|
const surface = await openEmbeddedEnvelopeEditor(page, {
|
|
envelopeType: 'TEMPLATE',
|
|
mode: 'edit',
|
|
tokenNamePrefix: 'e2e-embed-multi-recip',
|
|
});
|
|
const result = await runMultiRecipientFieldFlow(surface);
|
|
|
|
await persistEmbeddedEnvelope(surface);
|
|
|
|
await assertMultiRecipientCascadePersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('duplicate and delete fields via canvas action toolbar', async ({ page }) => {
|
|
const surface = await openEmbeddedEnvelopeEditor(page, {
|
|
envelopeType: 'TEMPLATE',
|
|
mode: 'edit',
|
|
tokenNamePrefix: 'e2e-embed-dup-del',
|
|
});
|
|
const result = await runDuplicateDeleteFieldFlow(surface);
|
|
|
|
await persistEmbeddedEnvelope(surface);
|
|
|
|
await assertDuplicateDeleteFieldPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('place and configure all 10 field types', async ({ page }) => {
|
|
const surface = await openEmbeddedEnvelopeEditor(page, {
|
|
envelopeType: 'TEMPLATE',
|
|
mode: 'edit',
|
|
tokenNamePrefix: 'e2e-embed-all-fields',
|
|
});
|
|
const result = await runAllFieldTypesFlow(surface);
|
|
|
|
await persistEmbeddedEnvelope(surface);
|
|
|
|
await assertAllFieldTypesPersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
|
|
test('change field type via canvas action toolbar (single and multi-select)', async ({ page }) => {
|
|
const surface = await openEmbeddedEnvelopeEditor(page, {
|
|
envelopeType: 'TEMPLATE',
|
|
mode: 'edit',
|
|
tokenNamePrefix: 'e2e-embed-change-type',
|
|
});
|
|
const result = await runChangeFieldTypeFlow(surface);
|
|
|
|
await persistEmbeddedEnvelope(surface);
|
|
|
|
await assertChangeFieldTypePersistedInDatabase({
|
|
surface,
|
|
...result,
|
|
});
|
|
});
|
|
});
|