mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 09:25:08 +10:00
fix: render error messages for invalid templates
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { seedDirectTemplate } from '@documenso/prisma/seed/templates';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
import { expect, type Page, test } from '@playwright/test';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
import { clickEnvelopeEditorStep } from '../fixtures/envelope-editor';
|
||||
|
||||
const INVALID_DIRECT_TEMPLATE_ALERT_TITLE = 'Invalid direct link template';
|
||||
|
||||
/**
|
||||
* Place a field on the PDF canvas in the envelope editor.
|
||||
*/
|
||||
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 });
|
||||
};
|
||||
|
||||
/**
|
||||
* Seed a V2 direct template and open it in the native template editor.
|
||||
*
|
||||
* Only the native template editor is covered here: direct links only exist
|
||||
* for templates and are not part of the embedded editor surfaces.
|
||||
*/
|
||||
const openDirectTemplateEditor = async (page: Page, options: { createDirectRecipientSignatureField: boolean }) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const template = await seedDirectTemplate({
|
||||
title: `E2E Direct Template Validation ${Date.now()}`,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
internalVersion: 2,
|
||||
createDirectRecipientSignatureField: options.createDirectRecipientSignatureField,
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
|
||||
});
|
||||
|
||||
return { user, team, template };
|
||||
};
|
||||
|
||||
test.describe('template editor', () => {
|
||||
test('shows invalid direct template warning when a signer has no signature field', async ({ page }) => {
|
||||
await openDirectTemplateEditor(page, { createDirectRecipientSignatureField: false });
|
||||
|
||||
await expect(page.getByText(INVALID_DIRECT_TEMPLATE_ALERT_TITLE)).toBeVisible();
|
||||
await expect(page.getByText('are missing a signature field')).toBeVisible();
|
||||
});
|
||||
|
||||
test('does not show the warning when all signers have signature fields', async ({ page }) => {
|
||||
await openDirectTemplateEditor(page, { createDirectRecipientSignatureField: true });
|
||||
|
||||
// Wait for the editor to render before asserting the banner is absent.
|
||||
await expect(page.getByTestId('envelope-editor-step-upload')).toBeVisible();
|
||||
await expect(page.getByText(INVALID_DIRECT_TEMPLATE_ALERT_TITLE)).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('warning disappears after placing a signature field', async ({ page }) => {
|
||||
await openDirectTemplateEditor(page, { createDirectRecipientSignatureField: false });
|
||||
|
||||
await expect(page.getByText(INVALID_DIRECT_TEMPLATE_ALERT_TITLE)).toBeVisible();
|
||||
|
||||
// Place a signature field for the direct recipient (auto-selected single recipient).
|
||||
await clickEnvelopeEditorStep(page, 'addFields');
|
||||
await expect(page.locator('.konva-container canvas').first()).toBeVisible();
|
||||
await placeFieldOnPdf(page, 'Signature', { x: 120, y: 140 });
|
||||
|
||||
// The banner clears once the field is autosaved and the envelope state updates.
|
||||
await expect(page.getByText(INVALID_DIRECT_TEMPLATE_ALERT_TITLE)).not.toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
||||
import { signSignaturePad } from '../fixtures/signature';
|
||||
|
||||
test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
||||
const { user, team } = await seedUser();
|
||||
@@ -73,8 +74,19 @@ test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
||||
await expect(page.locator('body')).toContainText('public-direct-template-title');
|
||||
await expect(page.locator('body')).toContainText('public-direct-template-description');
|
||||
|
||||
const directSignatureField = directTemplate.fields[0];
|
||||
|
||||
if (!directSignatureField) {
|
||||
throw new Error('Expected seeded direct template signature field to exist');
|
||||
}
|
||||
|
||||
await page.getByRole('link', { name: 'Sign' }).click();
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await signSignaturePad(page);
|
||||
await page.locator(`#field-${directSignatureField.id}`).getByRole('button').click();
|
||||
await expect(page.locator(`#field-${directSignatureField.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
|
||||
|
||||
@@ -197,7 +197,18 @@ test('[DIRECT_TEMPLATES]: V1 direct template link auth access', async ({ page })
|
||||
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
||||
await expect(page.getByLabel('Email')).toBeDisabled();
|
||||
|
||||
const directSignatureField = directTemplateWithAuth.fields[0];
|
||||
|
||||
if (!directSignatureField) {
|
||||
throw new Error('Expected seeded direct template signature field to exist');
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await signSignaturePad(page);
|
||||
await page.locator(`#field-${directSignatureField.id}`).getByRole('button').click();
|
||||
await expect(page.locator(`#field-${directSignatureField.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
@@ -235,6 +246,37 @@ test('[DIRECT_TEMPLATES]: V2 direct template link auth access', async ({ page })
|
||||
await page.goto(directTemplatePath);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Personal direct template link' })).toBeVisible();
|
||||
|
||||
const directSignatureField = directTemplateWithAuth.fields[0];
|
||||
|
||||
if (!directSignatureField) {
|
||||
throw new Error('Expected seeded direct template signature field to exist');
|
||||
}
|
||||
|
||||
// Wait for the PDF and the Konva canvas overlay to be ready.
|
||||
await expect(page.locator('img[data-page-number]').first()).toBeVisible({ timeout: 30_000 });
|
||||
const canvas = page.locator('.konva-container canvas').first();
|
||||
await expect(canvas).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// Sign the direct template recipient's signature field via the canvas-based V2 UI.
|
||||
await signSignaturePad(page);
|
||||
|
||||
const canvasBox = await canvas.boundingBox();
|
||||
|
||||
if (!canvasBox) {
|
||||
throw new Error('Canvas bounding box not found');
|
||||
}
|
||||
|
||||
const x =
|
||||
(Number(directSignatureField.positionX) / 100) * canvasBox.width +
|
||||
((Number(directSignatureField.width) / 100) * canvasBox.width) / 2;
|
||||
const y =
|
||||
(Number(directSignatureField.positionY) / 100) * canvasBox.height +
|
||||
((Number(directSignatureField.height) / 100) * canvasBox.height) / 2;
|
||||
|
||||
await canvas.click({ position: { x, y } });
|
||||
await expect(page.getByText('0 Fields Remaining').first()).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await expect(page.getByLabel('Your Email')).not.toBeVisible();
|
||||
|
||||
@@ -266,6 +308,16 @@ test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ p
|
||||
|
||||
await expect(page.getByText('Next Recipient Name')).not.toBeVisible();
|
||||
|
||||
const directSignatureField = template.fields[0];
|
||||
|
||||
if (!directSignatureField) {
|
||||
throw new Error('Expected seeded direct template signature field to exist');
|
||||
}
|
||||
|
||||
await signSignaturePad(page);
|
||||
await page.locator(`#field-${directSignatureField.id}`).getByRole('button').click();
|
||||
await expect(page.locator(`#field-${directSignatureField.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
await page.waitForURL(/\/sign/);
|
||||
@@ -299,19 +351,13 @@ test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with nex
|
||||
},
|
||||
});
|
||||
|
||||
const directTemplateRecipient = template.recipients[0];
|
||||
// The seeded direct template already includes a signature field for the direct recipient.
|
||||
const directSignatureField = template.fields[0];
|
||||
|
||||
if (!directTemplateRecipient) {
|
||||
throw new Error('Expected direct template recipient to exist');
|
||||
if (!directSignatureField) {
|
||||
throw new Error('Expected seeded direct template signature field to exist');
|
||||
}
|
||||
|
||||
// All SIGNER recipients need a signature field for sendDocument to dispatch emails.
|
||||
const directSignatureField = await seedSignatureFieldForRecipient({
|
||||
envelopeId: template.id,
|
||||
recipientId: directTemplateRecipient.id,
|
||||
positionY: 10,
|
||||
});
|
||||
|
||||
const originalName = 'Signer 2';
|
||||
const originalSecondSignerEmail = seedTestEmail();
|
||||
|
||||
@@ -413,19 +459,13 @@ test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with nex
|
||||
},
|
||||
});
|
||||
|
||||
const directTemplateRecipient = template.recipients[0];
|
||||
// The seeded direct template already includes a signature field for the direct recipient.
|
||||
const directSignatureField = template.fields[0];
|
||||
|
||||
if (!directTemplateRecipient) {
|
||||
throw new Error('Expected direct template recipient to exist');
|
||||
if (!directSignatureField) {
|
||||
throw new Error('Expected seeded direct template signature field to exist');
|
||||
}
|
||||
|
||||
// All SIGNER recipients need a signature field for sendDocument to dispatch emails.
|
||||
const directSignatureField = await seedSignatureFieldForRecipient({
|
||||
envelopeId: template.id,
|
||||
recipientId: directTemplateRecipient.id,
|
||||
positionY: 10,
|
||||
});
|
||||
|
||||
const originalName = 'Signer 2';
|
||||
const originalSecondSignerEmail = seedTestEmail();
|
||||
|
||||
@@ -521,3 +561,48 @@ test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with nex
|
||||
expect(updatedSecondRecipient.email).toBe(newSecondSignerEmail);
|
||||
await expectSigningRequestJobForRecipient(updatedSecondRecipient.id);
|
||||
});
|
||||
|
||||
test('[DIRECT_TEMPLATES]: V1 direct template without signature fields shows invalid template page', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const template = await seedDirectTemplate({
|
||||
title: 'V1 invalid direct template',
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
createDirectRecipientSignatureField: false,
|
||||
});
|
||||
|
||||
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Invalid direct link template' })).toBeVisible();
|
||||
await expect(page.getByText('This direct link template cannot be used because one or more signers')).toBeVisible();
|
||||
|
||||
// The signing flow must not render.
|
||||
await expect(page.getByRole('heading', { name: 'General' })).not.toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Continue' })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('[DIRECT_TEMPLATES]: V2 direct template without signature fields shows invalid template page', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const template = await seedDirectTemplate({
|
||||
title: 'V2 invalid direct template',
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
internalVersion: 2,
|
||||
createDirectRecipientSignatureField: false,
|
||||
});
|
||||
|
||||
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Invalid direct link template' })).toBeVisible();
|
||||
await expect(page.getByText('This direct link template cannot be used because one or more signers')).toBeVisible();
|
||||
|
||||
// The signing flow (PDF canvas) must not render.
|
||||
await expect(page.locator('.konva-container canvas')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: 'Complete' })).not.toBeVisible();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { FIELD_SIGNATURE_META_DEFAULT_VALUES } from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedTemplate } from '@documenso/prisma/seed/templates';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { DocumentStatus, FieldType } from '@prisma/client';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
||||
|
||||
const seedSignatureFieldForRecipient = async (options: { envelopeId: string; recipientId: number }) => {
|
||||
const envelopeItem = await prisma.envelopeItem.findFirstOrThrow({
|
||||
where: { envelopeId: options.envelopeId },
|
||||
});
|
||||
|
||||
return await prisma.field.create({
|
||||
data: {
|
||||
envelopeId: options.envelopeId,
|
||||
envelopeItemId: envelopeItem.id,
|
||||
recipientId: options.recipientId,
|
||||
type: FieldType.SIGNATURE,
|
||||
page: 1,
|
||||
positionX: 5,
|
||||
positionY: 10,
|
||||
width: 20,
|
||||
height: 5,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: FIELD_SIGNATURE_META_DEFAULT_VALUES,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
test('[TEMPLATE_USE]: shows missing signature fields error when sending a template without signature fields', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
// seedTemplate creates one SIGNER recipient and no fields.
|
||||
await seedTemplate({
|
||||
title: 'Template missing signature fields',
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/templates`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Create document from template' })).toBeVisible();
|
||||
|
||||
// Enable distribution so the document is sent on creation.
|
||||
await page.locator('#distributeDocument').click();
|
||||
await page.getByRole('button', { name: 'Create and send' }).click();
|
||||
|
||||
await expectToastTextToBeVisible(page, 'Missing signature fields');
|
||||
await expectToastTextToBeVisible(
|
||||
page,
|
||||
'The document could not be sent because some signers do not have a signature field',
|
||||
);
|
||||
});
|
||||
|
||||
test('[TEMPLATE_USE]: creates and sends a document when signers have signature fields', async ({ page }) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const template = await seedTemplate({
|
||||
title: 'Template with signature fields',
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
await seedSignatureFieldForRecipient({
|
||||
envelopeId: template.id,
|
||||
recipientId: template.recipients[0].id,
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/templates`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Create document from template' })).toBeVisible();
|
||||
|
||||
await page.locator('#distributeDocument').click();
|
||||
await page.getByRole('button', { name: 'Create and send' }).click();
|
||||
|
||||
await page.waitForURL(new RegExp(`/t/${team.url}/documents/envelope_.*`));
|
||||
|
||||
const envelopeId = page.url().split('/').pop()?.split('?')[0];
|
||||
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: { id: envelopeId },
|
||||
});
|
||||
|
||||
expect(envelope.status).toBe(DocumentStatus.PENDING);
|
||||
});
|
||||
Reference in New Issue
Block a user