15 KiB
name, description
| name | description |
|---|---|
| envelope-editor-v2-e2e | Writing and maintaining Playwright E2E tests for the Envelope Editor V2. Use when the user needs to create, modify, debug, or extend E2E tests in packages/app-tests/e2e/envelope-editor-v2/. Triggers include requests to "write an e2e test", "add a test for the envelope editor", "test envelope settings/recipients/fields/items/attachments", "fix a failing envelope test", or any task involving Playwright tests for the envelope editor feature. |
Envelope Editor V2 E2E Tests
Overview
The Envelope Editor V2 E2E test suite lives in packages/app-tests/e2e/envelope-editor-v2/. Each test file covers a distinct feature area of the envelope editor and follows a strict architectural pattern that tests the same flow across four surfaces:
- Document (
documents/<id>) - Native document editor - Template (
templates/<id>) - Native template editor - Embedded Create (
/embed/v2/authoring/envelope/create) - Embedded editor creating a new envelope - Embedded Edit (
/embed/v2/authoring/envelope/edit/<id>) - Embedded editor updating an existing envelope
Project Structure
packages/app-tests/
e2e/
envelope-editor-v2/
envelope-attachments.spec.ts # Attachment CRUD
envelope-fields.spec.ts # Field placement on PDF canvas
envelope-items.spec.ts # PDF document item CRUD
envelope-recipients.spec.ts # Recipient management
envelope-settings.spec.ts # Settings dialog
fixtures/
authentication.ts # apiSignin, apiSignout
documents.ts # Document tab helpers
envelope-editor.ts # Core fixture: surface openers + locator/action helpers
generic.ts # Toast assertions, text visibility
signature.ts # Signature pad helpers
playwright.config.ts # Test configuration
Core Abstraction: TEnvelopeEditorSurface
Every test revolves around the TEnvelopeEditorSurface type from fixtures/envelope-editor.ts. This is the central abstraction that normalizes differences between the four surfaces:
type TEnvelopeEditorSurface = {
root: Page; // The Playwright page
isEmbedded: boolean; // true for embed surfaces
envelopeId?: string; // Set for document/template/embed-edit, undefined for embed-create
envelopeType: 'DOCUMENT' | 'TEMPLATE';
userId: number; // Seeded user ID
userEmail: string; // Seeded user email
userName: string; // Seeded user name
teamId: number; // Seeded team ID
};
Surface Openers (from fixtures/envelope-editor.ts)
// Native surfaces - seed user + document/template, sign in, navigate
const surface = await openDocumentEnvelopeEditor(page);
const surface = await openTemplateEnvelopeEditor(page);
// Embedded surfaces - seed user, create API token, get presign token, navigate
const surface = await openEmbeddedEnvelopeEditor(page, {
envelopeType: 'DOCUMENT' | 'TEMPLATE',
mode?: 'create' | 'edit', // default: 'create'
tokenNamePrefix?: string, // for unique API token names
externalId?: string, // optional external ID in hash
features?: EmbeddedEditorConfig, // feature flags
});
Test Architecture Pattern
Every test file follows this structure, with four test.describe blocks grouping tests by editor surface:
1. Imports
import { type Page, expect, test } from '@playwright/test';
// Prisma enums if needed for DB assertions
import { SomePrismaEnum } from '@prisma/client';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import {
type TEnvelopeEditorSurface, // Import needed helpers from the fixture
openDocumentEnvelopeEditor,
openEmbeddedEnvelopeEditor,
openTemplateEnvelopeEditor,
persistEmbeddedEnvelope, // ... other helpers
} from '../fixtures/envelope-editor';
import { expectToastTextToBeVisible } from '../fixtures/generic';
2. Type definitions and constants
type FlowResult = {
externalId: string;
// ... other data needed for DB assertions
};
const TEST_VALUES = {
// Centralized test data constants
};
3. Local helper functions
// Common: open settings and set external ID for DB lookup
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');
}
};
4. The flow function
A single runXxxFlow function that works across ALL surfaces. It handles embedded vs non-embedded differences internally:
const runMyFeatureFlow = async (surface: TEnvelopeEditorSurface): Promise<FlowResult> => {
const externalId = `e2e-feature-${nanoid()}`;
// For embedded create, may need to add a PDF first
if (surface.isEmbedded && !surface.envelopeId) {
await addEnvelopeItemPdf(surface.root, 'embedded-feature.pdf');
}
await updateExternalId(surface, externalId);
// Handle embedded vs native differences
if (surface.isEmbedded) {
// No "Add Myself" button in embedded mode
await setRecipientEmail(surface.root, 0, 'embedded@example.com');
} else {
await clickAddMyselfButton(surface.root);
}
// ... perform feature-specific actions ...
// Navigate away and back to verify UI persistence
await clickEnvelopeEditorStep(surface.root, 'addFields');
await clickEnvelopeEditorStep(surface.root, 'upload');
// ... assert UI state after navigation ...
return { externalId /* ... */ };
};
5. Database assertion function
Uses Prisma directly to verify data was persisted correctly:
const assertFeaturePersistedInDatabase = async ({
surface,
externalId,
// ... expected values
}: {
surface: TEnvelopeEditorSurface;
externalId: string;
// ...
}) => {
const envelope = await prisma.envelope.findFirstOrThrow({
where: {
externalId,
userId: surface.userId,
teamId: surface.teamId,
type: surface.envelopeType,
},
include: {
// Include related data as needed
documentMeta: true,
recipients: true,
fields: true,
envelopeAttachments: true,
},
orderBy: { createdAt: 'desc' },
});
// Assert expected values
expect(envelope.someField).toBe(expectedValue);
};
6. The four test.describe blocks
Tests are organized into four test.describe blocks, one per editor surface. Each describe block contains the tests relevant to that surface. This structure allows adding multiple tests per surface while keeping them grouped:
test.describe('document editor', () => {
test('description of what is tested', async ({ page }) => {
const surface = await openDocumentEnvelopeEditor(page);
const result = await runMyFeatureFlow(surface);
await assertFeaturePersistedInDatabase({
surface,
...result,
});
});
// Additional document-editor-specific tests here...
});
test.describe('template editor', () => {
test('description of what is tested', async ({ page }) => {
const surface = await openTemplateEnvelopeEditor(page);
const result = await runMyFeatureFlow(surface);
await assertFeaturePersistedInDatabase({
surface,
...result,
});
});
// Additional template-editor-specific tests here...
});
test.describe('embedded create', () => {
test('description of what is tested', async ({ page }) => {
const surface = await openEmbeddedEnvelopeEditor(page, {
envelopeType: 'DOCUMENT',
tokenNamePrefix: 'e2e-embed-feature',
});
const result = await runMyFeatureFlow(surface);
// IMPORTANT: Must persist before DB assertions for embedded
await persistEmbeddedEnvelope(surface);
await assertFeaturePersistedInDatabase({
surface,
...result,
});
});
// Additional embedded-create-specific tests here...
});
test.describe('embedded edit', () => {
test('description of what is tested', async ({ page }) => {
const surface = await openEmbeddedEnvelopeEditor(page, {
envelopeType: 'TEMPLATE',
mode: 'edit',
tokenNamePrefix: 'e2e-embed-feature',
});
const result = await runMyFeatureFlow(surface);
// IMPORTANT: Must persist before DB assertions for embedded
await persistEmbeddedEnvelope(surface);
await assertFeaturePersistedInDatabase({
surface,
...result,
});
});
// Additional embedded-edit-specific tests here...
});
When a test only applies to specific surfaces (e.g., a document-only action like "send document"), only include it in the relevant describe block(s). Not every describe block needs the same tests -- the structure groups tests by surface, not by requiring symmetry.
Key Differences Between Surfaces
| Behavior | Document/Template | Embedded Create | Embedded Edit |
|---|---|---|---|
| User seeding | Seed + sign in | Seed + API token | Seed + API token + seed envelope |
| "Add Myself" button | Available | Not available | Not available |
| Toast on settings update | Yes ('Envelope updated') |
No | No |
| PDF already attached | Yes (1 item) | No (0 items, must upload) | Yes (1 item) |
| Delete confirmation dialog | Yes ('Delete' button) |
No (immediate) | No (immediate) |
| DB persistence timing | Immediate (autosaved) | After persistEmbeddedEnvelope() |
After persistEmbeddedEnvelope() |
| Persist button label | N/A | 'Create Document' / 'Create Template' |
'Update Document' / 'Update Template' |
Available Fixture Helpers
From fixtures/envelope-editor.ts
Locator helpers (return Playwright Locators):
getEnvelopeEditorSettingsTrigger(root)- Settings gear buttongetEnvelopeItemTitleInputs(root)- Title inputs for envelope itemsgetEnvelopeItemDragHandles(root)- Drag handles for reordering itemsgetEnvelopeItemRemoveButtons(root)- Remove buttons for itemsgetEnvelopeItemDropzoneInput(root)- File input for PDF uploadgetRecipientEmailInputs(root)- Email inputs for recipientsgetRecipientNameInputs(root)- Name inputs for recipientsgetRecipientRows(root)- Full recipient row fieldsetsgetRecipientRemoveButtons(root)- Remove buttons for recipientsgetSigningOrderInputs(root)- Signing order number inputs
Action helpers:
addEnvelopeItemPdf(root, fileName?)- Upload a PDF to the dropzoneclickEnvelopeEditorStep(root, stepId)- Navigate to a step:'upload','addFields','preview'clickAddMyselfButton(root)- Click "Add Myself" (native only)clickAddSignerButton(root)- Click "Add Signer"setRecipientEmail(root, index, email)- Fill recipient emailsetRecipientName(root, index, name)- Fill recipient namesetRecipientRole(root, index, roleLabel)- Set role via comboboxassertRecipientRole(root, index, roleLabel)- Assert role valuetoggleSigningOrder(root, enabled)- Toggle signing order switchtoggleAllowDictateSigners(root, enabled)- Toggle dictate signers switchsetSigningOrderValue(root, index, value)- Set signing order numberpersistEmbeddedEnvelope(surface)- Click Create/Update button for embedded flows
From fixtures/generic.ts
expectTextToBeVisible(page, text)- Assert text visible on pageexpectTextToNotBeVisible(page, text)- Assert text not visibleexpectToastTextToBeVisible(page, text)- Assert toast message visible
External ID Pattern
Every test uses an externalId (e.g., e2e-feature-${nanoid()}) set via the settings dialog. This unique ID is then used in Prisma queries to reliably locate the envelope in the database for assertions. This is critical because multiple tests run in parallel.
Running Tests
# Run all envelope editor tests
npm run test:dev -w @documenso/app-tests -- --grep "Envelope Editor V2"
# Run a specific test file
npm run test:dev -w @documenso/app-tests -- e2e/envelope-editor-v2/envelope-recipients.spec.ts
# Run with UI
npm run test-ui:dev -w @documenso/app-tests -- e2e/envelope-editor-v2/
# Run specific test by name
npm run test:dev -w @documenso/app-tests -- --grep "documents/<id>: add myself"
Checklist When Writing a New Test
- Create the spec file in
packages/app-tests/e2e/envelope-editor-v2/ - Import
TEnvelopeEditorSurfaceand the three opener functions - Import
persistEmbeddedEnvelopeif you need DB assertions for embedded flows - Define a
FlowResulttype for data passed between flow and assertion - Define
TEST_VALUESconstants for test data - Write
updateExternalIdhelper (or reuse the pattern) - Write the
runXxxFlowfunction handling embedded vs native differences - Write the
assertXxxPersistedInDatabasefunction using Prisma - Create four
test.describeblocks:'document editor','template editor','embedded create','embedded edit' - Place tests inside the appropriate describe block for each surface
- For embedded create tests, add a PDF via
addEnvelopeItemPdfbefore the flow - For embedded tests, call
persistEmbeddedEnvelope(surface)before DB assertions - Use
surface.isEmbeddedto branch on behavioral differences (toasts, "Add Myself", etc.)
Common Pitfalls
- Missing
persistEmbeddedEnvelope: Embedded flows don't autosave. You MUST call this before any DB assertions. - PDF required for embedded create: Embedded create starts with 0 items. Upload a PDF before navigating to fields.
- Toast assertions in embedded: Don't assert toasts for settings updates in embedded mode (they don't appear).
- Parallel test isolation: Always use a unique
externalIdviananoid()so parallel tests don't collide. - Navigation verification: Navigate away from and back to the current step to verify UI state persistence (the editor may re-render).
- Delete confirmation: Native surfaces show a confirmation dialog for item deletion; embedded surfaces delete immediately.