mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
The form learns server ids only on a step change, so each autosave after the first sent the recipients it had just created without an id: the server deleted them and created them again, with new ids and signing tokens. The provider now keeps the id it got back for each row's formId, sent as clientId and echoed by envelope.recipient.set.
381 lines
13 KiB
TypeScript
381 lines
13 KiB
TypeScript
import { type Page, expect, test } from '@playwright/test';
|
|
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
|
|
|
|
import { nanoid } from '@hanzo/esign-lib/universal/id';
|
|
import { prisma } from '@hanzo/esign-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: '[email protected]',
|
|
name: 'Recipient Two',
|
|
},
|
|
thirdRecipient: {
|
|
email: '[email protected]',
|
|
name: 'Recipient Three',
|
|
},
|
|
embeddedPrimaryRecipient: {
|
|
email: '[email protected]',
|
|
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('document editor', () => {
|
|
test('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.describe('document editor, several recipients in one visit', () => {
|
|
// The form learns server ids only on a step change, so every autosave after
|
|
// the first used to send the new signers id-less: the server deleted and
|
|
// recreated them, and their ids, signing tokens and fields went with them.
|
|
test('recipients keep their ids and fields while more are added, then send', async ({ page }) => {
|
|
const surface = await openDocumentEnvelopeEditor(page);
|
|
const { root } = surface;
|
|
|
|
const recipientsInDatabase = async () =>
|
|
prisma.recipient.findMany({
|
|
where: { envelopeId: surface.envelopeId },
|
|
orderBy: { id: 'asc' },
|
|
});
|
|
|
|
await setRecipientEmail(root, 0, '[email protected]');
|
|
await setRecipientName(root, 0, 'First Signer');
|
|
await expect.poll(async () => (await recipientsInDatabase()).length).toBe(1);
|
|
|
|
const [first] = await recipientsInDatabase();
|
|
|
|
await clickAddSignerButton(root);
|
|
await setRecipientEmail(root, 1, '[email protected]');
|
|
await setRecipientName(root, 1, 'The Approver');
|
|
await setRecipientRole(root, 1, 'Needs to approve');
|
|
|
|
await clickAddSignerButton(root);
|
|
await setRecipientEmail(root, 2, '[email protected]');
|
|
await setRecipientName(root, 2, 'Gets A Copy');
|
|
await setRecipientRole(root, 2, 'Receives copy');
|
|
|
|
await expect
|
|
.poll(async () => (await recipientsInDatabase()).map((r) => r.role))
|
|
.toEqual([RecipientRole.SIGNER, RecipientRole.APPROVER, RecipientRole.CC]);
|
|
|
|
const afterAdding = await recipientsInDatabase();
|
|
expect(afterAdding[0].id).toBe(first.id);
|
|
expect(afterAdding[0].token).toBe(first.token);
|
|
|
|
// A field for each of the two who act.
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
await expect(root.locator('.konva-container canvas').first()).toBeVisible();
|
|
await root.getByRole('button', { name: 'Signature', exact: true }).click();
|
|
await root
|
|
.locator('.konva-container canvas')
|
|
.first()
|
|
.click({ position: { x: 120, y: 140 } });
|
|
await root.locator('button[role="combobox"]').click();
|
|
await root.getByText('[email protected]').click();
|
|
await root.getByRole('button', { name: 'Signature', exact: true }).click();
|
|
await root
|
|
.locator('.konva-container canvas')
|
|
.first()
|
|
.click({ position: { x: 120, y: 300 } });
|
|
await expect
|
|
.poll(async () => prisma.field.count({ where: { envelopeId: surface.envelopeId } }))
|
|
.toBe(2);
|
|
|
|
// An edit after the fields are placed keeps everyone and every field.
|
|
await clickEnvelopeEditorStep(root, 'upload');
|
|
await setRecipientName(root, 1, 'The Approver, Esq.');
|
|
await clickAddSignerButton(root);
|
|
await setRecipientEmail(root, 3, '[email protected]');
|
|
await setRecipientName(root, 3, 'Late Signer');
|
|
await setRecipientName(root, 3, 'Late Signer Two');
|
|
await expect.poll(async () => (await recipientsInDatabase()).length).toBe(4);
|
|
await expect
|
|
.poll(async () => (await recipientsInDatabase())[1].name)
|
|
.toBe('The Approver, Esq.');
|
|
|
|
const afterEditing = await recipientsInDatabase();
|
|
expect(afterEditing.slice(0, 3).map((r) => r.id)).toEqual(afterAdding.map((r) => r.id));
|
|
expect(await prisma.field.count({ where: { envelopeId: surface.envelopeId } })).toBe(2);
|
|
|
|
// The late signer needs a field before the document can go out.
|
|
await clickEnvelopeEditorStep(root, 'addFields');
|
|
await expect(root.locator('.konva-container canvas').first()).toBeVisible();
|
|
await root.locator('button[role="combobox"]').click();
|
|
await root.getByText('[email protected]').click();
|
|
await root.getByRole('button', { name: 'Signature', exact: true }).click();
|
|
await root
|
|
.locator('.konva-container canvas')
|
|
.first()
|
|
.click({ position: { x: 300, y: 140 } });
|
|
await expect
|
|
.poll(async () => prisma.field.count({ where: { envelopeId: surface.envelopeId } }))
|
|
.toBe(3);
|
|
await clickEnvelopeEditorStep(root, 'upload');
|
|
|
|
await page.locator('button[title="Send Envelope"]').click();
|
|
await expect(page.getByRole('heading', { name: 'Send Document' })).toBeVisible();
|
|
await page.getByRole('button', { name: 'Send' }).click();
|
|
await expectToastTextToBeVisible(page, 'Envelope distributed');
|
|
|
|
const sent = await prisma.envelope.findUniqueOrThrow({
|
|
where: { id: surface.envelopeId },
|
|
include: { recipients: { orderBy: { id: 'asc' } } },
|
|
});
|
|
|
|
expect(sent.status).toBe('PENDING');
|
|
expect(sent.recipients.map((r) => r.id)).toEqual(afterEditing.map((r) => r.id));
|
|
});
|
|
});
|
|
|
|
test.describe('template editor', () => {
|
|
test('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.describe('embedded create', () => {
|
|
test('CRUD, roles, signing order and dictate signers', 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.describe('embedded edit', () => {
|
|
test('CRUD, roles, signing order and dictate signers', 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,
|
|
});
|
|
});
|
|
});
|