mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
feat: add signature configurations (#1710)
Add ability to enable or disable allowed signature types: - Drawn - Typed - Uploaded **Tabbed style signature dialog**  **Document settings**  **Team preferences**  - Add multiselect to select allowed signatures in document and templates settings tab - Add multiselect to select allowed signatures in teams preferences - Removed "Enable typed signatures" from document/template edit page - Refactored signature pad to use tabs instead of an all in one signature pad Added E2E tests to check settings are applied correctly for documents and templates
This commit is contained in:
@ -246,7 +246,9 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
||||
});
|
||||
}
|
||||
|
||||
await signSignaturePad(page);
|
||||
if (fields.some((field) => field.type === FieldType.SIGNATURE)) {
|
||||
await signSignaturePad(page);
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
@ -349,7 +351,9 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
||||
});
|
||||
}
|
||||
|
||||
await signSignaturePad(page);
|
||||
if (fields.some((field) => field.type === FieldType.SIGNATURE)) {
|
||||
await signSignaturePad(page);
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
@ -11,7 +11,7 @@ import { prisma } from '@documenso/prisma';
|
||||
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { signSignaturePad } from '../fixtures/signature';
|
||||
import { signDirectSignaturePad, signSignaturePad } from '../fixtures/signature';
|
||||
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should allow updating next recipient when dictation is enabled', async ({
|
||||
page,
|
||||
@ -322,7 +322,7 @@ test('[NEXT_RECIPIENT_DICTATION]: should allow assistant to dictate next signer'
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
if (field.type === FieldType.SIGNATURE) {
|
||||
await signSignaturePad(page);
|
||||
await signDirectSignaturePad(page);
|
||||
await page.getByRole('button', { name: 'Sign', exact: true }).click();
|
||||
}
|
||||
|
||||
|
||||
@ -222,7 +222,10 @@ test.describe('Signing Certificate Tests', () => {
|
||||
|
||||
// Toggle signing certificate setting
|
||||
await page.getByLabel('Include the Signing Certificate in the Document').click();
|
||||
await page.getByRole('button', { name: /Save/ }).first().click();
|
||||
await page
|
||||
.getByRole('button', { name: /Update/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
@ -236,7 +239,10 @@ test.describe('Signing Certificate Tests', () => {
|
||||
|
||||
// Toggle the setting back to true
|
||||
await page.getByLabel('Include the Signing Certificate in the Document').click();
|
||||
await page.getByRole('button', { name: /Save/ }).first().click();
|
||||
await page
|
||||
.getByRole('button', { name: /Update/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
|
||||
@ -1,40 +1,28 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Will open the signature pad dialog and sign it.
|
||||
*/
|
||||
export const signSignaturePad = async (page: Page) => {
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const canvas = page.getByTestId('signature-pad');
|
||||
await page.getByTestId('signature-pad-dialog-button').click();
|
||||
|
||||
const box = await canvas.boundingBox();
|
||||
// Click type tab
|
||||
await page.getByRole('tab', { name: 'Type' }).click();
|
||||
await page.getByTestId('signature-pad-type-input').fill('Signature');
|
||||
|
||||
if (!box) {
|
||||
throw new Error('Signature pad not found');
|
||||
}
|
||||
|
||||
// Calculate center point
|
||||
const centerX = box.x + box.width / 2;
|
||||
const centerY = box.y + box.height / 2;
|
||||
|
||||
// Calculate square size (making it slightly smaller than the canvas)
|
||||
const squareSize = Math.min(box.width, box.height) * 0.4; // 40% of the smallest dimension
|
||||
|
||||
// Move to center
|
||||
await page.mouse.move(centerX, centerY);
|
||||
await page.mouse.down();
|
||||
|
||||
// Draw square clockwise from center
|
||||
// Move right
|
||||
await page.mouse.move(centerX + squareSize, centerY, { steps: 10 });
|
||||
// Move down
|
||||
await page.mouse.move(centerX + squareSize, centerY + squareSize, { steps: 10 });
|
||||
// Move left
|
||||
await page.mouse.move(centerX - squareSize, centerY + squareSize, { steps: 10 });
|
||||
// Move up
|
||||
await page.mouse.move(centerX - squareSize, centerY - squareSize, { steps: 10 });
|
||||
// Move right
|
||||
await page.mouse.move(centerX + squareSize, centerY - squareSize, { steps: 10 });
|
||||
// Move down to close the square
|
||||
await page.mouse.move(centerX + squareSize, centerY, { steps: 10 });
|
||||
|
||||
await page.mouse.up();
|
||||
// Click Next button
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
};
|
||||
|
||||
/**
|
||||
* For when the signature pad is already open.
|
||||
*/
|
||||
export const signDirectSignaturePad = async (page: Page) => {
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Click type tab
|
||||
await page.getByRole('tab', { name: 'Type' }).click();
|
||||
await page.getByTestId('signature-pad-type-input').fill('Signature');
|
||||
};
|
||||
|
||||
@ -23,7 +23,7 @@ test('[TEAMS]: update the default document visibility in the team global setting
|
||||
// !: Brittle selector
|
||||
await page.getByRole('combobox').first().click();
|
||||
await page.getByRole('option', { name: 'Admin' }).click();
|
||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||
await page.getByRole('button', { name: 'Update' }).first().click();
|
||||
|
||||
const toast = page.locator('li[role="status"][data-state="open"]').first();
|
||||
await expect(toast).toBeVisible();
|
||||
@ -47,7 +47,7 @@ test('[TEAMS]: update the sender details in the team global settings', async ({
|
||||
|
||||
await expect(checkbox).toBeChecked();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||
await page.getByRole('button', { name: 'Update' }).first().click();
|
||||
|
||||
const toast = page.locator('li[role="status"][data-state="open"]').first();
|
||||
await expect(toast).toBeVisible();
|
||||
|
||||
182
packages/app-tests/e2e/teams/team-signature-settings.spec.ts
Normal file
182
packages/app-tests/e2e/teams/team-signature-settings.spec.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
seedTeamDocumentWithMeta,
|
||||
seedTeamDocuments,
|
||||
seedTeamTemplateWithMeta,
|
||||
} from '@documenso/prisma/seed/documents';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test('[TEAMS]: check that default team signature settings are all enabled', async ({ page }) => {
|
||||
const { team } = await seedTeamDocuments();
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: team.owner.email,
|
||||
password: 'password',
|
||||
redirectPath: `/t/${team.url}/settings/preferences`,
|
||||
});
|
||||
|
||||
// Verify that the default created team settings has all signatures enabled
|
||||
await expect(page.getByRole('combobox').filter({ hasText: 'Type' })).toBeVisible();
|
||||
await expect(page.getByRole('combobox').filter({ hasText: 'Upload' })).toBeVisible();
|
||||
await expect(page.getByRole('combobox').filter({ hasText: 'Draw' })).toBeVisible();
|
||||
|
||||
const document = await seedTeamDocumentWithMeta(team);
|
||||
|
||||
// Create a document and check the settings
|
||||
await page.goto(`/t/${team.url}/documents/${document.id}/edit`);
|
||||
|
||||
// Verify that the settings match
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
await expect(page.getByRole('combobox').filter({ hasText: 'Type' })).toBeVisible();
|
||||
await expect(page.getByRole('combobox').filter({ hasText: 'Upload' })).toBeVisible();
|
||||
await expect(page.getByRole('combobox').filter({ hasText: 'Draw' })).toBeVisible();
|
||||
|
||||
// Go to document and check that the signatured tabs are correct.
|
||||
await page.goto(`/sign/${document.recipients[0].token}`);
|
||||
await page.getByTestId('signature-pad-dialog-button').click();
|
||||
|
||||
// Check the tab values
|
||||
await expect(page.getByRole('tab', { name: 'Type' })).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: 'Upload' })).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: 'Draw' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('[TEAMS]: check signature modes can be disabled', async ({ page }) => {
|
||||
const { team } = await seedTeamDocuments();
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: team.owner.email,
|
||||
password: 'password',
|
||||
redirectPath: `/t/${team.url}/settings/preferences`,
|
||||
});
|
||||
|
||||
const allTabs = ['Type', 'Upload', 'Draw'];
|
||||
const tabTest = [['Type', 'Upload', 'Draw'], ['Type', 'Upload'], ['Type']];
|
||||
|
||||
for (const tabs of tabTest) {
|
||||
await page.goto(`/t/${team.url}/settings/preferences`);
|
||||
|
||||
// Update combobox to have the correct tabs
|
||||
await page.getByTestId('signature-types-combobox').click();
|
||||
|
||||
await expect(page.getByRole('option', { name: 'Type' })).toBeVisible();
|
||||
await expect(page.getByRole('option', { name: 'Upload' })).toBeVisible();
|
||||
await expect(page.getByRole('option', { name: 'Draw' })).toBeVisible();
|
||||
|
||||
// Clear all selected items.
|
||||
for (const tab of allTabs) {
|
||||
const item = page.getByRole('option', { name: tab });
|
||||
|
||||
const isSelected = (await item.innerHTML()).includes('opacity-100');
|
||||
|
||||
if (isSelected) {
|
||||
await item.click();
|
||||
}
|
||||
}
|
||||
|
||||
// Selected wanted items.
|
||||
for (const tab of tabs) {
|
||||
const item = page.getByRole('option', { name: tab });
|
||||
await item.click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Update' }).first().click();
|
||||
|
||||
const document = await seedTeamDocumentWithMeta(team);
|
||||
|
||||
// Go to document and check that the signatured tabs are correct.
|
||||
await page.goto(`/sign/${document.recipients[0].token}`);
|
||||
await page.getByTestId('signature-pad-dialog-button').click();
|
||||
|
||||
// Check the tab values
|
||||
for (const tab of allTabs) {
|
||||
if (tabs.includes(tab)) {
|
||||
await expect(page.getByRole('tab', { name: tab })).toBeVisible();
|
||||
} else {
|
||||
await expect(page.getByRole('tab', { name: tab })).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('[TEAMS]: check signature modes work for templates', async ({ page }) => {
|
||||
const { team } = await seedTeamDocuments();
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: team.owner.email,
|
||||
password: 'password',
|
||||
redirectPath: `/t/${team.url}/settings/preferences`,
|
||||
});
|
||||
|
||||
const allTabs = ['Type', 'Upload', 'Draw'];
|
||||
const tabTest = [['Type', 'Upload', 'Draw'], ['Type', 'Upload'], ['Type']];
|
||||
|
||||
for (const tabs of tabTest) {
|
||||
await page.goto(`/t/${team.url}/settings/preferences`);
|
||||
|
||||
// Update combobox to have the correct tabs
|
||||
await page.getByTestId('signature-types-combobox').click();
|
||||
|
||||
await expect(page.getByRole('option', { name: 'Type' })).toBeVisible();
|
||||
await expect(page.getByRole('option', { name: 'Upload' })).toBeVisible();
|
||||
await expect(page.getByRole('option', { name: 'Draw' })).toBeVisible();
|
||||
|
||||
// Clear all selected items.
|
||||
for (const tab of allTabs) {
|
||||
const item = page.getByRole('option', { name: tab });
|
||||
|
||||
const isSelected = (await item.innerHTML()).includes('opacity-100');
|
||||
|
||||
if (isSelected) {
|
||||
await item.click();
|
||||
}
|
||||
}
|
||||
|
||||
// Selected wanted items.
|
||||
for (const tab of tabs) {
|
||||
const item = page.getByRole('option', { name: tab });
|
||||
await item.click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Update' }).first().click();
|
||||
|
||||
const template = await seedTeamTemplateWithMeta(team);
|
||||
|
||||
await page.goto(`/t/${team.url}/templates/${template.id}`);
|
||||
await page.getByRole('button', { name: 'Use' }).click();
|
||||
|
||||
// Check the send document checkbox to true
|
||||
await page.getByLabel('Send document').click();
|
||||
await page.getByRole('button', { name: 'Create and send' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
templateId: template.id,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Test kinda flaky, debug here.
|
||||
// console.log({
|
||||
// tabs,
|
||||
// typedSignatureEnabled: document?.documentMeta?.typedSignatureEnabled,
|
||||
// uploadSignatureEnabled: document?.documentMeta?.uploadSignatureEnabled,
|
||||
// drawSignatureEnabled: document?.documentMeta?.drawSignatureEnabled,
|
||||
// });
|
||||
|
||||
expect(document?.documentMeta?.typedSignatureEnabled).toEqual(tabs.includes('Type'));
|
||||
expect(document?.documentMeta?.uploadSignatureEnabled).toEqual(tabs.includes('Upload'));
|
||||
expect(document?.documentMeta?.drawSignatureEnabled).toEqual(tabs.includes('Draw'));
|
||||
}
|
||||
});
|
||||
@ -298,6 +298,7 @@ test('[DIRECT_TEMPLATES]: use direct template link with 2 recipients', async ({
|
||||
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
|
||||
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await page.getByPlaceholder('recipient@documenso.com').fill(seedTestEmail());
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
@ -4,6 +4,7 @@ import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-emai
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
import { signSignaturePad } from '../fixtures/signature';
|
||||
|
||||
test('[USER] update full name', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
@ -12,7 +13,7 @@ test('[USER] update full name', async ({ page }) => {
|
||||
|
||||
await page.getByLabel('Full Name').fill('John Doe');
|
||||
|
||||
await page.getByPlaceholder('Type your signature').fill('John Doe');
|
||||
await signSignaturePad(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Update profile' }).click();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user