mirror of
https://github.com/documenso/documenso.git
synced 2026-08-24 15:22:26 +10:00
test: drive recipient grouping e2e through drag and drop
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
|||||||
assertRecipientRole,
|
assertRecipientRole,
|
||||||
getRecipientEmailInputs,
|
getRecipientEmailInputs,
|
||||||
getRecipientRows,
|
getRecipientRows,
|
||||||
getSigningOrderInputs,
|
getRecipientStepCards,
|
||||||
openDocumentEnvelopeEditor,
|
openDocumentEnvelopeEditor,
|
||||||
setRecipientEmail,
|
setRecipientEmail,
|
||||||
setRecipientName,
|
setRecipientName,
|
||||||
@@ -34,14 +34,14 @@ const assertCcDisplayedLastWithNoOrderInput = async (root: Page) => {
|
|||||||
await assertRecipientRole(root, 1, 'Needs to sign');
|
await assertRecipientRole(root, 1, 'Needs to sign');
|
||||||
await assertRecipientRole(root, 2, 'Receives copy');
|
await assertRecipientRole(root, 2, 'Receives copy');
|
||||||
|
|
||||||
// Only the two signers have signing order inputs, showing 1 and 2.
|
// Only the two signers render as ordered group cards, showing groups 1 and 2.
|
||||||
await expect(getSigningOrderInputs(root)).toHaveCount(2);
|
await expect(getRecipientStepCards(root)).toHaveCount(2);
|
||||||
await expect(getSigningOrderInputs(root).nth(0)).toHaveValue('1');
|
await expect(root.getByText('Group 1', { exact: true })).toBeVisible();
|
||||||
await expect(getSigningOrderInputs(root).nth(1)).toHaveValue('2');
|
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);
|
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', () => {
|
test.describe('document editor', () => {
|
||||||
@@ -61,8 +61,8 @@ test.describe('document editor', () => {
|
|||||||
await setRecipientName(root, 1, CC_RECIPIENT.name);
|
await setRecipientName(root, 1, CC_RECIPIENT.name);
|
||||||
await setRecipientRole(root, 1, 'Receives copy');
|
await setRecipientRole(root, 1, 'Receives copy');
|
||||||
|
|
||||||
// Once the row becomes CC, its signing order input disappears.
|
// Once the row becomes CC, it drops out of the ordered group cards.
|
||||||
await expect(getSigningOrderInputs(root)).toHaveCount(1);
|
await expect(getRecipientStepCards(root)).toHaveCount(1);
|
||||||
|
|
||||||
// Add signer B third. The new row is inserted before the CC recipient,
|
// Add signer B third. The new row is inserted before the CC recipient,
|
||||||
// which is kept last by the client-side sorting.
|
// which is kept last by the client-side sorting.
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { expect, test } from '@playwright/test';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
clickAddSignerButton,
|
clickAddSignerButton,
|
||||||
getSigningOrderInputs,
|
dragGroupCardOntoCard,
|
||||||
|
dragRecipientRowToGap,
|
||||||
openDocumentEnvelopeEditor,
|
openDocumentEnvelopeEditor,
|
||||||
openTemplateEnvelopeEditor,
|
openTemplateEnvelopeEditor,
|
||||||
setRecipientEmail,
|
setRecipientEmail,
|
||||||
setRecipientName,
|
setRecipientName,
|
||||||
setSigningOrderValue,
|
|
||||||
type TEnvelopeEditorSurface,
|
type TEnvelopeEditorSurface,
|
||||||
toggleSigningOrder,
|
toggleSigningOrder,
|
||||||
} from '../fixtures/envelope-editor';
|
} from '../fixtures/envelope-editor';
|
||||||
@@ -48,20 +48,16 @@ const runGroupingFlow = async (surface: TEnvelopeEditorSurface) => {
|
|||||||
|
|
||||||
await toggleSigningOrder(root, true);
|
await toggleSigningOrder(root, true);
|
||||||
|
|
||||||
// Three standalone steps.
|
// Three standalone groups.
|
||||||
await expect(root.getByText('Step 1', { exact: true })).toBeVisible();
|
await expect(root.getByText('Group 1', { exact: true })).toBeVisible();
|
||||||
await expect(root.getByText('Step 3', { exact: true })).toBeVisible();
|
await expect(root.getByText('Group 3', { exact: true })).toBeVisible();
|
||||||
|
|
||||||
// Type-to-join: carol (step 3) joins bob (step 2).
|
// Drag carol's card onto bob's card to merge them into one group.
|
||||||
await setSigningOrderValue(root, 2, 2);
|
await dragGroupCardOntoCard(root, 2, 1);
|
||||||
|
|
||||||
await expect(root.getByText('2 signers · any order')).toBeVisible();
|
await expect(root.getByText('2 signers · any order')).toBeVisible();
|
||||||
await expect(root.getByTestId('ungroup-step-button')).toBeVisible();
|
await expect(root.getByTestId('ungroup-step-button')).toBeVisible();
|
||||||
await expect(root.getByText('Step 3', { exact: true })).not.toBeVisible();
|
await expect(root.getByText('Group 3', { exact: true })).not.toBeVisible();
|
||||||
|
|
||||||
const orderInputs = getSigningOrderInputs(root);
|
|
||||||
await expect(orderInputs.nth(1)).toHaveValue('2');
|
|
||||||
await expect(orderInputs.nth(2)).toHaveValue('2');
|
|
||||||
|
|
||||||
await expectRecipientOrders(surface, [
|
await expectRecipientOrders(surface, [
|
||||||
['alice@example.com', 1],
|
['alice@example.com', 1],
|
||||||
@@ -73,11 +69,11 @@ const runGroupingFlow = async (surface: TEnvelopeEditorSurface) => {
|
|||||||
await root.reload();
|
await root.reload();
|
||||||
await expect(root.getByText('2 signers · any order')).toBeVisible();
|
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 root.getByTestId('ungroup-step-button').click();
|
||||||
|
|
||||||
await expect(root.getByText('2 signers · any order')).not.toBeVisible();
|
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, [
|
await expectRecipientOrders(surface, [
|
||||||
['alice@example.com', 1],
|
['alice@example.com', 1],
|
||||||
@@ -85,9 +81,8 @@ const runGroupingFlow = async (surface: TEnvelopeEditorSurface) => {
|
|||||||
['carol@example.com', 3],
|
['carol@example.com', 3],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Out-of-bounds extraction: bob (step 2) types 4 (> 3 steps) and becomes the
|
// Drag bob's row into the gap after the last group, moving him to the end.
|
||||||
// last standalone step.
|
await dragRecipientRowToGap(root, 1, 3);
|
||||||
await setSigningOrderValue(root, 1, 4);
|
|
||||||
|
|
||||||
await expectRecipientOrders(surface, [
|
await expectRecipientOrders(surface, [
|
||||||
['alice@example.com', 1],
|
['alice@example.com', 1],
|
||||||
@@ -97,7 +92,7 @@ const runGroupingFlow = async (surface: TEnvelopeEditorSurface) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
test.describe('document editor', () => {
|
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);
|
const surface = await openDocumentEnvelopeEditor(page);
|
||||||
|
|
||||||
await runGroupingFlow(surface);
|
await runGroupingFlow(surface);
|
||||||
@@ -105,7 +100,7 @@ test.describe('document editor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe('template 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);
|
const surface = await openTemplateEnvelopeEditor(page);
|
||||||
|
|
||||||
await runGroupingFlow(surface);
|
await runGroupingFlow(surface);
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import {
|
|||||||
clickAddMyselfButton,
|
clickAddMyselfButton,
|
||||||
clickAddSignerButton,
|
clickAddSignerButton,
|
||||||
clickEnvelopeEditorStep,
|
clickEnvelopeEditorStep,
|
||||||
|
dragRecipientRowToGap,
|
||||||
getEnvelopeEditorSettingsTrigger,
|
getEnvelopeEditorSettingsTrigger,
|
||||||
getRecipientEmailInputs,
|
getRecipientEmailInputs,
|
||||||
getRecipientNameInputs,
|
getRecipientNameInputs,
|
||||||
getRecipientRemoveButtons,
|
getRecipientRemoveButtons,
|
||||||
getSigningOrderInputs,
|
getRecipientStepCards,
|
||||||
openDocumentEnvelopeEditor,
|
openDocumentEnvelopeEditor,
|
||||||
openEmbeddedEnvelopeEditor,
|
openEmbeddedEnvelopeEditor,
|
||||||
openTemplateEnvelopeEditor,
|
openTemplateEnvelopeEditor,
|
||||||
@@ -21,7 +22,6 @@ import {
|
|||||||
setRecipientEmail,
|
setRecipientEmail,
|
||||||
setRecipientName,
|
setRecipientName,
|
||||||
setRecipientRole,
|
setRecipientRole,
|
||||||
setSigningOrderValue,
|
|
||||||
type TEnvelopeEditorSurface,
|
type TEnvelopeEditorSurface,
|
||||||
toggleAllowDictateSigners,
|
toggleAllowDictateSigners,
|
||||||
toggleSigningOrder,
|
toggleSigningOrder,
|
||||||
@@ -116,46 +116,61 @@ const runRecipientFlow = async (surface: TEnvelopeEditorSurface): Promise<Recipi
|
|||||||
await expect(getRecipientEmailInputs(surface.root)).toHaveCount(2);
|
await expect(getRecipientEmailInputs(surface.root)).toHaveCount(2);
|
||||||
|
|
||||||
await toggleSigningOrder(surface.root, true);
|
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
|
// Reordering is drag-only. Pointer-emulated drags are unreliable inside the
|
||||||
// out-of-bounds value (> step count) is used to move the first recipient to
|
// embedded authoring surface (its inner scroll container auto-scrolls and
|
||||||
// the end as their own step, swapping the two.
|
// cancels the emulated drag), so the drag-swap is exercised on the native
|
||||||
await setSigningOrderValue(surface.root, 0, 3);
|
// 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 toggleAllowDictateSigners(surface.root, true);
|
||||||
|
|
||||||
await navigateToAddFieldsAndBack(surface.root);
|
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)).toHaveCount(2);
|
||||||
await expect(getRecipientEmailInputs(surface.root).nth(0)).toHaveValue(TEST_RECIPIENT_VALUES.secondRecipient.email);
|
await expect(getRecipientEmailInputs(surface.root).nth(0)).toHaveValue(firstRecipient.email);
|
||||||
await expect(getRecipientEmailInputs(surface.root).nth(1)).toHaveValue(primaryRecipient.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(0)).toHaveValue(firstRecipient.name);
|
||||||
await expect(getRecipientNameInputs(surface.root).nth(1)).toHaveValue(primaryRecipient.name);
|
await expect(getRecipientNameInputs(surface.root).nth(1)).toHaveValue(secondRecipient.name);
|
||||||
|
|
||||||
await assertRecipientRole(surface.root, 0, 'Needs to approve');
|
await assertRecipientRole(surface.root, 0, shouldSwapViaDrag ? 'Needs to approve' : 'Needs to sign');
|
||||||
await assertRecipientRole(surface.root, 1, '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('#signingOrder')).toHaveAttribute('aria-checked', 'true');
|
||||||
await expect(surface.root.locator('#allowDictateNextSigner')).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(surface.root.getByText('Group 1', { exact: true })).toBeVisible();
|
||||||
await expect(getSigningOrderInputs(surface.root).nth(1)).toHaveValue('2');
|
await expect(surface.root.getByText('Group 2', { exact: true })).toBeVisible();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
externalId,
|
externalId,
|
||||||
removedRecipientEmail: TEST_RECIPIENT_VALUES.thirdRecipient.email,
|
removedRecipientEmail: TEST_RECIPIENT_VALUES.thirdRecipient.email,
|
||||||
expectedRecipientsBySigningOrder: [
|
expectedRecipientsBySigningOrder: [
|
||||||
{
|
{
|
||||||
email: TEST_RECIPIENT_VALUES.secondRecipient.email,
|
email: firstRecipient.email,
|
||||||
name: TEST_RECIPIENT_VALUES.secondRecipient.name,
|
name: firstRecipient.name,
|
||||||
role: RecipientRole.APPROVER,
|
role: shouldSwapViaDrag ? RecipientRole.APPROVER : RecipientRole.SIGNER,
|
||||||
signingOrder: 1,
|
signingOrder: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
email: primaryRecipient.email,
|
email: secondRecipient.email,
|
||||||
name: primaryRecipient.name,
|
name: secondRecipient.name,
|
||||||
role: RecipientRole.SIGNER,
|
role: shouldSwapViaDrag ? RecipientRole.SIGNER : RecipientRole.APPROVER,
|
||||||
signingOrder: 2,
|
signingOrder: 2,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { DEFAULT_EMBEDDED_EDITOR_CONFIG } from '@documenso/lib/types/envelope-ed
|
|||||||
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
||||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
import type { Page } from '@playwright/test';
|
import type { Locator, Page } from '@playwright/test';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
import { apiSignin } from './authentication';
|
import { apiSignin } from './authentication';
|
||||||
@@ -264,8 +264,6 @@ export const getRecipientRows = (root: Page) =>
|
|||||||
|
|
||||||
export const getRecipientRemoveButtons = (root: Page) => root.locator('[data-testid="remove-signer-button"]');
|
export const getRecipientRemoveButtons = (root: Page) => root.locator('[data-testid="remove-signer-button"]');
|
||||||
|
|
||||||
export const getSigningOrderInputs = (root: Page) => root.locator('[data-testid="signing-order-input"]');
|
|
||||||
|
|
||||||
export const clickEnvelopeEditorStep = async (root: Page, stepId: 'upload' | 'addFields' | 'preview') => {
|
export const clickEnvelopeEditorStep = async (root: Page, stepId: 'upload' | 'addFields' | 'preview') => {
|
||||||
await root.waitForTimeout(200);
|
await root.waitForTimeout(200);
|
||||||
await root.locator(`[data-testid="envelope-editor-step-${stepId}"]`).first().click();
|
await root.locator(`[data-testid="envelope-editor-step-${stepId}"]`).first().click();
|
||||||
@@ -335,10 +333,143 @@ export const toggleAllowDictateSigners = async (root: Page, enabled: boolean) =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setSigningOrderValue = async (root: Page, index: number, value: number) => {
|
/**
|
||||||
const input = getSigningOrderInputs(root).nth(index);
|
* Performs a mouse-based drag from a drag handle onto a target element.
|
||||||
await input.fill(value.toString());
|
*
|
||||||
await input.blur();
|
* `@hello-pangea/dnd` only starts a drag once the pointer travels a small
|
||||||
|
* distance while pressed, and it hit-tests drop targets using the CENTRE of
|
||||||
|
* the dragged element — not the cursor. Since drag handles sit at the edge of
|
||||||
|
* wide rows/cards, the cursor destination is compensated so the dragged
|
||||||
|
* element's centre lands on the target's centre.
|
||||||
|
*/
|
||||||
|
export const dragHandleToTarget = async (
|
||||||
|
root: Page,
|
||||||
|
handle: Locator,
|
||||||
|
target: Locator,
|
||||||
|
options: { activeClass: string },
|
||||||
|
) => {
|
||||||
|
const { activeClass } = options;
|
||||||
|
|
||||||
|
await handle.scrollIntoViewIfNeeded();
|
||||||
|
|
||||||
|
const handleBox = await handle.boundingBox();
|
||||||
|
|
||||||
|
if (!handleBox) {
|
||||||
|
throw new Error('Unable to resolve drag handle position');
|
||||||
|
}
|
||||||
|
|
||||||
|
const startX = handleBox.x + handleBox.width / 2;
|
||||||
|
const startY = handleBox.y + handleBox.height / 2;
|
||||||
|
|
||||||
|
await root.mouse.move(startX, startY);
|
||||||
|
await root.mouse.down();
|
||||||
|
|
||||||
|
// Exceed the drag activation threshold, then wait for drag-dependent layout
|
||||||
|
// (e.g. expanding gap drop-zones) to settle before resolving positions.
|
||||||
|
const cursorX = startX + 8;
|
||||||
|
const cursorY = startY;
|
||||||
|
|
||||||
|
await root.mouse.move(cursorX, cursorY, { steps: 2 });
|
||||||
|
await root.waitForTimeout(300);
|
||||||
|
|
||||||
|
// The dragged element is the handle's draggable ancestor; while dragging it
|
||||||
|
// is fixed-positioned and follows the cursor at a constant offset. Drop
|
||||||
|
// targeting uses the dragged element's CENTRE, not the cursor, so the
|
||||||
|
// cursor destination is compensated by that offset.
|
||||||
|
const draggedElement = handle.locator('xpath=ancestor-or-self::*[@data-rfd-draggable-id][1]');
|
||||||
|
const draggedBox = await draggedElement.boundingBox();
|
||||||
|
const targetBox = await target.boundingBox();
|
||||||
|
|
||||||
|
if (!draggedBox || !targetBox) {
|
||||||
|
await root.mouse.up();
|
||||||
|
|
||||||
|
throw new Error('Unable to resolve drag positions');
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemOffsetX = draggedBox.x + draggedBox.width / 2 - cursorX;
|
||||||
|
const itemOffsetY = draggedBox.y + draggedBox.height / 2 - cursorY;
|
||||||
|
|
||||||
|
const hasBecomeActive = async () => {
|
||||||
|
const className = await target.getAttribute('class');
|
||||||
|
|
||||||
|
return Boolean(className?.includes(activeClass));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Crawl-and-drop: drop-target geometry is captured at drag start and can
|
||||||
|
// drift from the live layout (and auto-scrolling invalidates any cached
|
||||||
|
// coordinates), so precise aiming is unreliable. Instead, approach from
|
||||||
|
// well above the target and crawl downward in small increments, dropping
|
||||||
|
// the moment the target reports the drag as over it — the highlight class
|
||||||
|
// is rendered from the library's own drag state, so it cannot disagree
|
||||||
|
// with where the drop will land.
|
||||||
|
//
|
||||||
|
// The cursor is clamped inside the viewport: moving outside the window
|
||||||
|
// cancels the drag (pointercancel), and holding near the bottom edge lets
|
||||||
|
// the library auto-scroll the target up to the cursor instead.
|
||||||
|
const viewportHeight = root.viewportSize()?.height ?? 720;
|
||||||
|
const maxCursorY = viewportHeight - 40;
|
||||||
|
|
||||||
|
const crawlX = targetBox.x + targetBox.width / 2 - itemOffsetX;
|
||||||
|
const crawlStartY = Math.min(targetBox.y + targetBox.height / 2 - itemOffsetY - 140, maxCursorY);
|
||||||
|
|
||||||
|
await root.mouse.move(crawlX, crawlStartY, { steps: 15 });
|
||||||
|
await root.waitForTimeout(150);
|
||||||
|
|
||||||
|
let hasDropped = false;
|
||||||
|
|
||||||
|
for (let step = 1; step <= 80; step += 1) {
|
||||||
|
if (await hasBecomeActive()) {
|
||||||
|
await root.mouse.up();
|
||||||
|
|
||||||
|
hasDropped = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await root.mouse.move(crawlX, Math.min(crawlStartY + step * 6, maxCursorY), { steps: 2 });
|
||||||
|
await root.waitForTimeout(70);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasDropped) {
|
||||||
|
await root.mouse.up();
|
||||||
|
}
|
||||||
|
|
||||||
|
await root.waitForTimeout(400);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRecipientStepCards = (root: Page) => root.locator('[data-testid="recipient-step-card"]');
|
||||||
|
|
||||||
|
export const getRecipientStepGaps = (root: Page) => root.locator('[data-testid="recipient-step-gap"]');
|
||||||
|
|
||||||
|
export const getStepDragHandles = (root: Page) => root.locator('[data-testid="step-drag-handle"]');
|
||||||
|
|
||||||
|
export const getRecipientRowDragHandles = (root: Page) => root.locator('[data-testid="recipient-row-drag-handle"]');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drags a whole group card onto another card's centre, merging the two groups.
|
||||||
|
*/
|
||||||
|
export const dragGroupCardOntoCard = async (root: Page, sourceCardIndex: number, targetCardIndex: number) => {
|
||||||
|
await dragHandleToTarget(
|
||||||
|
root,
|
||||||
|
getStepDragHandles(root).nth(sourceCardIndex),
|
||||||
|
getRecipientStepCards(root).nth(targetCardIndex),
|
||||||
|
// The combine/join highlight on the target card.
|
||||||
|
{ activeClass: 'ring-primary' },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drags a recipient row into a gap between group cards, extracting it into
|
||||||
|
* its own standalone group at that position.
|
||||||
|
*/
|
||||||
|
export const dragRecipientRowToGap = async (root: Page, rowIndex: number, gapIndex: number) => {
|
||||||
|
await dragHandleToTarget(
|
||||||
|
root,
|
||||||
|
getRecipientRowDragHandles(root).nth(rowIndex),
|
||||||
|
getRecipientStepGaps(root).nth(gapIndex),
|
||||||
|
// The drag-over highlight on the gap drop-zone.
|
||||||
|
{ activeClass: 'border-primary' },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const persistEmbeddedEnvelope = async (surface: TEnvelopeEditorSurface) => {
|
export const persistEmbeddedEnvelope = async (surface: TEnvelopeEditorSurface) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user