test: drive recipient grouping e2e through drag and drop

This commit is contained in:
David Nguyen
2026-08-04 20:11:22 +10:00
parent 6a1236cac2
commit 34f6102a3f
4 changed files with 197 additions and 56 deletions
@@ -11,7 +11,7 @@ import {
assertRecipientRole,
getRecipientEmailInputs,
getRecipientRows,
getSigningOrderInputs,
getRecipientStepCards,
openDocumentEnvelopeEditor,
setRecipientEmail,
setRecipientName,
@@ -34,14 +34,14 @@ const assertCcDisplayedLastWithNoOrderInput = async (root: Page) => {
await assertRecipientRole(root, 1, 'Needs to sign');
await assertRecipientRole(root, 2, 'Receives copy');
// Only the two signers have signing order inputs, showing 1 and 2.
await expect(getSigningOrderInputs(root)).toHaveCount(2);
await expect(getSigningOrderInputs(root).nth(0)).toHaveValue('1');
await expect(getSigningOrderInputs(root).nth(1)).toHaveValue('2');
// Only the two signers render as ordered group cards, showing groups 1 and 2.
await expect(getRecipientStepCards(root)).toHaveCount(2);
await expect(root.getByText('Group 1', { exact: true })).toBeVisible();
await expect(root.getByText('Group 2', { exact: true })).toBeVisible();
// The CC row itself renders no signing order input (placeholder div instead).
// The CC row itself renders outside the group cards with no drag handle.
const ccRow = getRecipientRows(root).nth(2);
await expect(ccRow.locator('[data-testid="signing-order-input"]')).toHaveCount(0);
await expect(ccRow.locator('[data-testid="recipient-row-drag-handle"]')).toHaveCount(0);
};
test.describe('document editor', () => {
@@ -61,8 +61,8 @@ test.describe('document editor', () => {
await setRecipientName(root, 1, CC_RECIPIENT.name);
await setRecipientRole(root, 1, 'Receives copy');
// Once the row becomes CC, its signing order input disappears.
await expect(getSigningOrderInputs(root)).toHaveCount(1);
// Once the row becomes CC, it drops out of the ordered group cards.
await expect(getRecipientStepCards(root)).toHaveCount(1);
// Add signer B third. The new row is inserted before the CC recipient,
// which is kept last by the client-side sorting.
@@ -3,12 +3,12 @@ import { expect, test } from '@playwright/test';
import {
clickAddSignerButton,
getSigningOrderInputs,
dragGroupCardOntoCard,
dragRecipientRowToGap,
openDocumentEnvelopeEditor,
openTemplateEnvelopeEditor,
setRecipientEmail,
setRecipientName,
setSigningOrderValue,
type TEnvelopeEditorSurface,
toggleSigningOrder,
} from '../fixtures/envelope-editor';
@@ -48,20 +48,16 @@ const runGroupingFlow = async (surface: TEnvelopeEditorSurface) => {
await toggleSigningOrder(root, true);
// Three standalone steps.
await expect(root.getByText('Step 1', { exact: true })).toBeVisible();
await expect(root.getByText('Step 3', { exact: true })).toBeVisible();
// Three standalone groups.
await expect(root.getByText('Group 1', { exact: true })).toBeVisible();
await expect(root.getByText('Group 3', { exact: true })).toBeVisible();
// Type-to-join: carol (step 3) joins bob (step 2).
await setSigningOrderValue(root, 2, 2);
// Drag carol's card onto bob's card to merge them into one group.
await dragGroupCardOntoCard(root, 2, 1);
await expect(root.getByText('2 signers · any order')).toBeVisible();
await expect(root.getByTestId('ungroup-step-button')).toBeVisible();
await expect(root.getByText('Step 3', { exact: true })).not.toBeVisible();
const orderInputs = getSigningOrderInputs(root);
await expect(orderInputs.nth(1)).toHaveValue('2');
await expect(orderInputs.nth(2)).toHaveValue('2');
await expect(root.getByText('Group 3', { exact: true })).not.toBeVisible();
await expectRecipientOrders(surface, [
['alice@example.com', 1],
@@ -73,11 +69,11 @@ const runGroupingFlow = async (surface: TEnvelopeEditorSurface) => {
await root.reload();
await expect(root.getByText('2 signers · any order')).toBeVisible();
// Ungroup dissolves back into sequential steps.
// Ungroup dissolves back into sequential groups.
await root.getByTestId('ungroup-step-button').click();
await expect(root.getByText('2 signers · any order')).not.toBeVisible();
await expect(root.getByText('Step 3', { exact: true })).toBeVisible();
await expect(root.getByText('Group 3', { exact: true })).toBeVisible();
await expectRecipientOrders(surface, [
['alice@example.com', 1],
@@ -85,9 +81,8 @@ const runGroupingFlow = async (surface: TEnvelopeEditorSurface) => {
['carol@example.com', 3],
]);
// Out-of-bounds extraction: bob (step 2) types 4 (> 3 steps) and becomes the
// last standalone step.
await setSigningOrderValue(root, 1, 4);
// Drag bob's row into the gap after the last group, moving him to the end.
await dragRecipientRowToGap(root, 1, 3);
await expectRecipientOrders(surface, [
['alice@example.com', 1],
@@ -97,7 +92,7 @@ const runGroupingFlow = async (surface: TEnvelopeEditorSurface) => {
};
test.describe('document editor', () => {
test('documents: group recipients via signing order input and ungroup', async ({ page }) => {
test('documents: group recipients via drag and drop and ungroup', async ({ page }) => {
const surface = await openDocumentEnvelopeEditor(page);
await runGroupingFlow(surface);
@@ -105,7 +100,7 @@ test.describe('document editor', () => {
});
test.describe('template editor', () => {
test('templates: group recipients via signing order input and ungroup', async ({ page }) => {
test('templates: group recipients via drag and drop and ungroup', async ({ page }) => {
const surface = await openTemplateEnvelopeEditor(page);
await runGroupingFlow(surface);
@@ -9,11 +9,12 @@ import {
clickAddMyselfButton,
clickAddSignerButton,
clickEnvelopeEditorStep,
dragRecipientRowToGap,
getEnvelopeEditorSettingsTrigger,
getRecipientEmailInputs,
getRecipientNameInputs,
getRecipientRemoveButtons,
getSigningOrderInputs,
getRecipientStepCards,
openDocumentEnvelopeEditor,
openEmbeddedEnvelopeEditor,
openTemplateEnvelopeEditor,
@@ -21,7 +22,6 @@ import {
setRecipientEmail,
setRecipientName,
setRecipientRole,
setSigningOrderValue,
type TEnvelopeEditorSurface,
toggleAllowDictateSigners,
toggleSigningOrder,
@@ -116,46 +116,61 @@ const runRecipientFlow = async (surface: TEnvelopeEditorSurface): Promise<Recipi
await expect(getRecipientEmailInputs(surface.root)).toHaveCount(2);
await toggleSigningOrder(surface.root, true);
await expect(getSigningOrderInputs(surface.root)).toHaveCount(2);
await expect(getRecipientStepCards(surface.root)).toHaveCount(2);
// Typing an existing step number would group the recipients together, so an
// out-of-bounds value (> step count) is used to move the first recipient to
// the end as their own step, swapping the two.
await setSigningOrderValue(surface.root, 0, 3);
// Reordering is drag-only. Pointer-emulated drags are unreliable inside the
// embedded authoring surface (its inner scroll container auto-scrolls and
// cancels the emulated drag), so the drag-swap is exercised on the native
// surfaces only — the same component drives all surfaces.
const shouldSwapViaDrag = !surface.isEmbedded;
if (shouldSwapViaDrag) {
// Let the debounced autosave from the edits above land before dragging —
// the editor re-rendering mid-drag would cancel the drag.
await surface.root.waitForTimeout(1500);
// Drag the first recipient's row into the gap after the last group,
// swapping the two.
await dragRecipientRowToGap(surface.root, 0, 2);
}
await toggleAllowDictateSigners(surface.root, true);
await navigateToAddFieldsAndBack(surface.root);
const [firstRecipient, secondRecipient] = shouldSwapViaDrag
? [TEST_RECIPIENT_VALUES.secondRecipient, primaryRecipient]
: [primaryRecipient, TEST_RECIPIENT_VALUES.secondRecipient];
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(getRecipientEmailInputs(surface.root).nth(0)).toHaveValue(firstRecipient.email);
await expect(getRecipientEmailInputs(surface.root).nth(1)).toHaveValue(secondRecipient.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 expect(getRecipientNameInputs(surface.root).nth(0)).toHaveValue(firstRecipient.name);
await expect(getRecipientNameInputs(surface.root).nth(1)).toHaveValue(secondRecipient.name);
await assertRecipientRole(surface.root, 0, 'Needs to approve');
await assertRecipientRole(surface.root, 1, 'Needs to sign');
await assertRecipientRole(surface.root, 0, shouldSwapViaDrag ? 'Needs to approve' : 'Needs to sign');
await assertRecipientRole(surface.root, 1, shouldSwapViaDrag ? 'Needs to sign' : 'Needs to approve');
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');
await expect(surface.root.getByText('Group 1', { exact: true })).toBeVisible();
await expect(surface.root.getByText('Group 2', { exact: true })).toBeVisible();
return {
externalId,
removedRecipientEmail: TEST_RECIPIENT_VALUES.thirdRecipient.email,
expectedRecipientsBySigningOrder: [
{
email: TEST_RECIPIENT_VALUES.secondRecipient.email,
name: TEST_RECIPIENT_VALUES.secondRecipient.name,
role: RecipientRole.APPROVER,
email: firstRecipient.email,
name: firstRecipient.name,
role: shouldSwapViaDrag ? RecipientRole.APPROVER : RecipientRole.SIGNER,
signingOrder: 1,
},
{
email: primaryRecipient.email,
name: primaryRecipient.name,
role: RecipientRole.SIGNER,
email: secondRecipient.email,
name: secondRecipient.name,
role: shouldSwapViaDrag ? RecipientRole.SIGNER : RecipientRole.APPROVER,
signingOrder: 2,
},
],