mirror of
https://github.com/documenso/documenso.git
synced 2026-07-17 16:50:13 +10:00
488 lines
17 KiB
TypeScript
488 lines
17 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
|
import { prisma } from '@documenso/prisma';
|
|
import { seedUser } from '@documenso/prisma/seed/users';
|
|
import type {
|
|
TCreateEnvelopePayload,
|
|
TCreateEnvelopeResponse,
|
|
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
|
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
|
|
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
|
import { expect, type Page, test } from '@playwright/test';
|
|
import { EnvelopeType, FieldType, RecipientRole } from '@prisma/client';
|
|
|
|
import { apiSignin } from '../fixtures/authentication';
|
|
import {
|
|
clickAddMyselfButton,
|
|
clickEnvelopeEditorStep,
|
|
getRecipientEmailInputs,
|
|
openDocumentEnvelopeEditor,
|
|
} from '../fixtures/envelope-editor';
|
|
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
|
|
|
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
|
const V2_API_BASE_URL = `${WEBAPP_BASE_URL}/api/v2-beta`;
|
|
|
|
const examplePdfBuffer = fs.readFileSync(path.join(__dirname, '../../../../assets/example.pdf'));
|
|
|
|
/**
|
|
* 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 });
|
|
};
|
|
|
|
/**
|
|
* Create a V2 document envelope via the API with a single SIGNER recipient
|
|
* and a SIGNATURE field, suitable for testing save-as-template with data.
|
|
*
|
|
* Returns the envelope ID, user, team, and recipient email.
|
|
*/
|
|
const createDocumentWithRecipientAndField = async () => {
|
|
const { user, team } = await seedUser();
|
|
|
|
const { token } = await createApiToken({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
tokenName: `e2e-save-as-template-${Date.now()}`,
|
|
expiresIn: null,
|
|
});
|
|
|
|
const recipientEmail = `save-template-${Date.now()}@test.documenso.com`;
|
|
|
|
// 1. Create envelope with a PDF.
|
|
const payload = {
|
|
type: EnvelopeType.DOCUMENT,
|
|
title: `E2E Save as Template ${Date.now()}`,
|
|
} satisfies TCreateEnvelopePayload;
|
|
|
|
const formData = new FormData();
|
|
formData.append('payload', JSON.stringify(payload));
|
|
formData.append('files', new File([examplePdfBuffer], 'example.pdf', { type: 'application/pdf' }));
|
|
|
|
const createRes = await fetch(`${V2_API_BASE_URL}/envelope/create`, {
|
|
method: 'POST',
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
body: formData,
|
|
});
|
|
|
|
expect(createRes.ok).toBeTruthy();
|
|
const createResponse = (await createRes.json()) as TCreateEnvelopeResponse;
|
|
|
|
// 2. Create a SIGNER recipient.
|
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
|
envelopeId: createResponse.id,
|
|
data: [
|
|
{
|
|
email: recipientEmail,
|
|
name: 'Template Test Signer',
|
|
role: RecipientRole.SIGNER,
|
|
accessAuth: [],
|
|
actionAuth: [],
|
|
},
|
|
],
|
|
};
|
|
|
|
const recipientsRes = await fetch(`${V2_API_BASE_URL}/envelope/recipient/create-many`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(createRecipientsRequest),
|
|
});
|
|
|
|
expect(recipientsRes.ok).toBeTruthy();
|
|
const recipientsResponse = await recipientsRes.json();
|
|
const recipients = recipientsResponse.data;
|
|
|
|
// 3. Get envelope to find the envelope item ID.
|
|
const getRes = await fetch(`${V2_API_BASE_URL}/envelope/${createResponse.id}`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
|
|
const envelope = (await getRes.json()) as TGetEnvelopeResponse;
|
|
const envelopeItem = envelope.envelopeItems[0];
|
|
|
|
// 4. Create a SIGNATURE field for the recipient.
|
|
const createFieldsRequest = {
|
|
envelopeId: createResponse.id,
|
|
data: [
|
|
{
|
|
recipientId: recipients[0].id,
|
|
envelopeItemId: envelopeItem.id,
|
|
type: FieldType.SIGNATURE,
|
|
page: 1,
|
|
positionX: 100,
|
|
positionY: 100,
|
|
width: 50,
|
|
height: 50,
|
|
},
|
|
],
|
|
};
|
|
|
|
const fieldsRes = await fetch(`${V2_API_BASE_URL}/envelope/field/create-many`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(createFieldsRequest),
|
|
});
|
|
|
|
expect(fieldsRes.ok).toBeTruthy();
|
|
|
|
return {
|
|
user,
|
|
team,
|
|
envelopeId: createResponse.id,
|
|
recipientEmail,
|
|
};
|
|
};
|
|
|
|
test.describe('document editor', () => {
|
|
test('save document as template from editor sidebar', async ({ page }) => {
|
|
const surface = await openDocumentEnvelopeEditor(page);
|
|
|
|
// Add the current user as a recipient via the UI.
|
|
await clickAddMyselfButton(surface.root);
|
|
await expect(getRecipientEmailInputs(surface.root).first()).toHaveValue(surface.userEmail);
|
|
|
|
// Navigate to the add fields step and place a signature field.
|
|
await clickEnvelopeEditorStep(surface.root, 'addFields');
|
|
await expect(surface.root.locator('.konva-container canvas').first()).toBeVisible();
|
|
await placeFieldOnPdf(surface.root, 'Signature', { x: 120, y: 140 });
|
|
|
|
// Navigate back to the upload step so the sidebar actions are available.
|
|
await clickEnvelopeEditorStep(surface.root, 'upload');
|
|
|
|
// Click the "Save as Template" sidebar action.
|
|
await page.locator('button[title="Save as Template"]').click();
|
|
|
|
// The save as template dialog should appear.
|
|
await expect(page.getByRole('heading', { name: 'Save as Template' })).toBeVisible();
|
|
|
|
// Both checkboxes should be checked by default.
|
|
await expect(page.locator('#envelopeIncludeRecipients')).toBeChecked();
|
|
await expect(page.locator('#envelopeIncludeFields')).toBeChecked();
|
|
|
|
// Click "Save as Template".
|
|
await page.getByRole('button', { name: 'Save as Template' }).click();
|
|
|
|
// Assert toast appears.
|
|
await expectToastTextToBeVisible(page, 'Template Created');
|
|
|
|
// The page should have navigated to the new template's edit page.
|
|
await expect(page).toHaveURL(/\/templates\/.*\/edit/);
|
|
|
|
// Verify the new template envelope was created in the database with correct type and secondaryId.
|
|
const templateEnvelopes = await prisma.envelope.findMany({
|
|
where: {
|
|
userId: surface.userId,
|
|
teamId: surface.teamId,
|
|
type: 'TEMPLATE',
|
|
},
|
|
include: {
|
|
recipients: {
|
|
include: {
|
|
fields: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(templateEnvelopes.length).toBeGreaterThanOrEqual(1);
|
|
|
|
const createdTemplate = templateEnvelopes.find((e) => e.title.includes('(copy)'));
|
|
expect(createdTemplate).toBeDefined();
|
|
|
|
// CRITICAL: Verify the secondaryId uses the template_ prefix, not document_.
|
|
// This confirms incrementTemplateId was called, not incrementDocumentId.
|
|
expect(createdTemplate!.secondaryId).toMatch(/^template_\d+$/);
|
|
expect(createdTemplate!.type).toBe(EnvelopeType.TEMPLATE);
|
|
|
|
// Verify recipients were included.
|
|
expect(createdTemplate!.recipients.length).toBe(1);
|
|
expect(createdTemplate!.recipients[0].email).toBe(surface.userEmail);
|
|
|
|
// Verify fields were included.
|
|
expect(createdTemplate!.recipients[0].fields.length).toBe(1);
|
|
expect(createdTemplate!.recipients[0].fields[0].type).toBe(FieldType.SIGNATURE);
|
|
});
|
|
|
|
test('save document as template without recipients', async ({ page }) => {
|
|
const { user, team, envelopeId, recipientEmail } = await createDocumentWithRecipientAndField();
|
|
|
|
await apiSignin({
|
|
page,
|
|
email: user.email,
|
|
redirectPath: `/t/${team.url}/documents/${envelopeId}/edit`,
|
|
});
|
|
|
|
await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();
|
|
|
|
// Click the "Save as Template" sidebar action.
|
|
await page.locator('button[title="Save as Template"]').click();
|
|
|
|
// The dialog should appear.
|
|
await expect(page.getByRole('heading', { name: 'Save as Template' })).toBeVisible();
|
|
|
|
// Uncheck "Include Recipients" - "Include Fields" should auto-disable.
|
|
await page.locator('#envelopeIncludeRecipients').click();
|
|
await expect(page.locator('#envelopeIncludeRecipients')).not.toBeChecked();
|
|
await expect(page.locator('#envelopeIncludeFields')).not.toBeChecked();
|
|
await expect(page.locator('#envelopeIncludeFields')).toBeDisabled();
|
|
|
|
// Click "Save as Template".
|
|
await page.getByRole('button', { name: 'Save as Template' }).click();
|
|
|
|
// Assert toast appears.
|
|
await expectToastTextToBeVisible(page, 'Template Created');
|
|
|
|
// The page should have navigated to the new template's edit page.
|
|
await expect(page).toHaveURL(/\/templates\/.*\/edit/);
|
|
|
|
// Verify the template was created without recipients.
|
|
const templateEnvelopes = await prisma.envelope.findMany({
|
|
where: {
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
type: 'TEMPLATE',
|
|
},
|
|
include: {
|
|
recipients: true,
|
|
},
|
|
});
|
|
|
|
const createdTemplate = templateEnvelopes.find((e) => e.title.includes('(copy)'));
|
|
expect(createdTemplate).toBeDefined();
|
|
|
|
// CRITICAL: Verify the secondaryId uses the template_ prefix.
|
|
expect(createdTemplate!.secondaryId).toMatch(/^template_\d+$/);
|
|
|
|
// Verify no recipients were included.
|
|
expect(createdTemplate!.recipients.length).toBe(0);
|
|
});
|
|
|
|
test('save document as template without fields but with recipients', async ({ page }) => {
|
|
const { user, team, envelopeId, recipientEmail } = await createDocumentWithRecipientAndField();
|
|
|
|
await apiSignin({
|
|
page,
|
|
email: user.email,
|
|
redirectPath: `/t/${team.url}/documents/${envelopeId}/edit`,
|
|
});
|
|
|
|
await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();
|
|
|
|
// Click the "Save as Template" sidebar action.
|
|
await page.locator('button[title="Save as Template"]').click();
|
|
|
|
// The dialog should appear.
|
|
await expect(page.getByRole('heading', { name: 'Save as Template' })).toBeVisible();
|
|
|
|
// Uncheck "Include Fields" but keep "Include Recipients".
|
|
await page.locator('#envelopeIncludeFields').click();
|
|
await expect(page.locator('#envelopeIncludeRecipients')).toBeChecked();
|
|
await expect(page.locator('#envelopeIncludeFields')).not.toBeChecked();
|
|
|
|
// Click "Save as Template".
|
|
await page.getByRole('button', { name: 'Save as Template' }).click();
|
|
|
|
// Assert toast appears.
|
|
await expectToastTextToBeVisible(page, 'Template Created');
|
|
|
|
// The page should have navigated to the new template's edit page.
|
|
await expect(page).toHaveURL(/\/templates\/.*\/edit/);
|
|
|
|
// Verify the template was created with recipients but no fields.
|
|
const templateEnvelopes = await prisma.envelope.findMany({
|
|
where: {
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
type: 'TEMPLATE',
|
|
},
|
|
include: {
|
|
recipients: {
|
|
include: {
|
|
fields: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const createdTemplate = templateEnvelopes.find((e) => e.title.includes('(copy)'));
|
|
expect(createdTemplate).toBeDefined();
|
|
|
|
// CRITICAL: Verify the secondaryId uses the template_ prefix.
|
|
expect(createdTemplate!.secondaryId).toMatch(/^template_\d+$/);
|
|
|
|
// Verify recipients were included.
|
|
expect(createdTemplate!.recipients.length).toBe(1);
|
|
expect(createdTemplate!.recipients[0].email).toBe(recipientEmail);
|
|
|
|
// Verify no fields were included.
|
|
expect(createdTemplate!.recipients[0].fields.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
test.describe('documents table', () => {
|
|
test('save as template from document row dropdown', async ({ page }) => {
|
|
const { user, team, envelopeId, recipientEmail } = await createDocumentWithRecipientAndField();
|
|
|
|
await apiSignin({
|
|
page,
|
|
email: user.email,
|
|
redirectPath: `/t/${team.url}/documents`,
|
|
});
|
|
|
|
// Wait for the documents table to load.
|
|
await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();
|
|
|
|
// Click the actions dropdown for the document row.
|
|
await page.getByTestId('document-table-action-btn').first().click();
|
|
|
|
// Click "Save as Template" in the dropdown.
|
|
await page.getByRole('menuitem', { name: 'Save as Template' }).click();
|
|
|
|
// The dialog should appear.
|
|
await expect(page.getByRole('heading', { name: 'Save as Template' })).toBeVisible();
|
|
|
|
// Click "Save as Template".
|
|
await page.getByRole('button', { name: 'Save as Template' }).click();
|
|
|
|
// Assert toast appears.
|
|
await expectToastTextToBeVisible(page, 'Template Created');
|
|
|
|
// The page should have navigated to the new template's edit page.
|
|
await expect(page).toHaveURL(/\/templates\/.*\/edit/);
|
|
|
|
// Verify the template was created with correct type and secondaryId.
|
|
const templateEnvelopes = await prisma.envelope.findMany({
|
|
where: {
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
type: 'TEMPLATE',
|
|
},
|
|
include: {
|
|
recipients: {
|
|
include: {
|
|
fields: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const createdTemplate = templateEnvelopes.find((e) => e.title.includes('(copy)'));
|
|
expect(createdTemplate).toBeDefined();
|
|
|
|
// CRITICAL: Verify the secondaryId uses the template_ prefix.
|
|
expect(createdTemplate!.secondaryId).toMatch(/^template_\d+$/);
|
|
expect(createdTemplate!.type).toBe(EnvelopeType.TEMPLATE);
|
|
|
|
// Verify recipients and fields were included (defaults are both checked).
|
|
expect(createdTemplate!.recipients.length).toBe(1);
|
|
expect(createdTemplate!.recipients[0].fields.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
test.describe('document index page', () => {
|
|
test('save as template from document page dropdown', async ({ page }) => {
|
|
const { user, team, envelopeId, recipientEmail } = await createDocumentWithRecipientAndField();
|
|
|
|
await apiSignin({
|
|
page,
|
|
email: user.email,
|
|
redirectPath: `/t/${team.url}/documents/${envelopeId}`,
|
|
});
|
|
|
|
// Wait for the document page to load.
|
|
await expect(page.getByRole('heading', { name: /E2E Save as Template/ })).toBeVisible();
|
|
|
|
// Click the more actions dropdown trigger.
|
|
await page.getByTestId('document-page-view-action-btn').click();
|
|
|
|
// Click "Save as Template" in the dropdown.
|
|
await page.getByRole('menuitem', { name: 'Save as Template' }).click();
|
|
|
|
// The dialog should appear.
|
|
await expect(page.getByRole('heading', { name: 'Save as Template' })).toBeVisible();
|
|
|
|
// Click "Save as Template".
|
|
await page.getByRole('button', { name: 'Save as Template' }).click();
|
|
|
|
// Assert toast appears.
|
|
await expectToastTextToBeVisible(page, 'Template Created');
|
|
|
|
// The page should have navigated to the new template's edit page.
|
|
await expect(page).toHaveURL(/\/templates\/.*\/edit/);
|
|
|
|
// Verify the template was created with correct type and secondaryId.
|
|
const templateEnvelopes = await prisma.envelope.findMany({
|
|
where: {
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
type: 'TEMPLATE',
|
|
},
|
|
});
|
|
|
|
const createdTemplate = templateEnvelopes.find((e) => e.title.includes('(copy)'));
|
|
expect(createdTemplate).toBeDefined();
|
|
|
|
// CRITICAL: Verify the secondaryId uses the template_ prefix.
|
|
expect(createdTemplate!.secondaryId).toMatch(/^template_\d+$/);
|
|
expect(createdTemplate!.type).toBe(EnvelopeType.TEMPLATE);
|
|
});
|
|
});
|
|
|
|
test.describe('legacy ID correctness', () => {
|
|
test('save as template uses template counter, not document counter', async ({ page }) => {
|
|
// Record the current counter values before the operation.
|
|
const [documentCounterBefore, templateCounterBefore] = await Promise.all([
|
|
prisma.counter.findUnique({ where: { id: 'document' } }),
|
|
prisma.counter.findUnique({ where: { id: 'template' } }),
|
|
]);
|
|
|
|
const surface = await openDocumentEnvelopeEditor(page);
|
|
|
|
// Click the "Save as Template" sidebar action.
|
|
await page.locator('button[title="Save as Template"]').click();
|
|
await expect(page.getByRole('heading', { name: 'Save as Template' })).toBeVisible();
|
|
|
|
// Click "Save as Template".
|
|
await page.getByRole('button', { name: 'Save as Template' }).click();
|
|
await expectToastTextToBeVisible(page, 'Template Created');
|
|
await expect(page).toHaveURL(/\/templates\/.*\/edit/);
|
|
|
|
// Record the counter values after the operation.
|
|
const [documentCounterAfter, templateCounterAfter] = await Promise.all([
|
|
prisma.counter.findUnique({ where: { id: 'document' } }),
|
|
prisma.counter.findUnique({ where: { id: 'template' } }),
|
|
]);
|
|
|
|
// The template counter MUST have incremented (at least once - could be more due to
|
|
// the seedBlankDocument call in openDocumentEnvelopeEditor seeding other templates).
|
|
expect(templateCounterAfter!.value).toBeGreaterThan(templateCounterBefore!.value);
|
|
|
|
// Verify the created template's secondaryId matches the template counter.
|
|
const createdTemplate = await prisma.envelope.findFirst({
|
|
where: {
|
|
userId: surface.userId,
|
|
teamId: surface.teamId,
|
|
type: 'TEMPLATE',
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
|
|
expect(createdTemplate).not.toBeNull();
|
|
expect(createdTemplate!.secondaryId).toBe(`template_${templateCounterAfter!.value}`);
|
|
expect(createdTemplate!.secondaryId).not.toMatch(/^document_/);
|
|
});
|
|
});
|