feat: add new field overflow methods (#2715)
@@ -86,6 +86,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
fontSize: 10,
|
||||
textAlign: 'left',
|
||||
type: 'email',
|
||||
overflow: 'auto',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(0, 0),
|
||||
@@ -96,6 +97,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
fieldMeta: {
|
||||
textAlign: 'center',
|
||||
type: 'email',
|
||||
overflow: 'auto',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(0, 1),
|
||||
@@ -107,6 +109,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
fontSize: 20,
|
||||
textAlign: 'right',
|
||||
type: 'email',
|
||||
overflow: 'auto',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(0, 2),
|
||||
@@ -156,6 +159,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
fontSize: 10,
|
||||
textAlign: 'left',
|
||||
type: 'date',
|
||||
overflow: 'auto',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(2, 0),
|
||||
@@ -166,6 +170,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
fieldMeta: {
|
||||
textAlign: 'center',
|
||||
type: 'date',
|
||||
overflow: 'auto',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(2, 1),
|
||||
@@ -177,6 +182,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
fontSize: 20,
|
||||
textAlign: 'right',
|
||||
type: 'date',
|
||||
overflow: 'auto',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(2, 2),
|
||||
@@ -424,6 +430,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
fieldMeta: {
|
||||
fontSize: 10,
|
||||
type: 'signature',
|
||||
overflow: 'auto',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(9, 0),
|
||||
@@ -434,6 +441,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: FieldType.SIGNATURE,
|
||||
fieldMeta: {
|
||||
type: 'signature',
|
||||
overflow: 'auto',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(9, 1),
|
||||
@@ -445,6 +453,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
fieldMeta: {
|
||||
fontSize: 20,
|
||||
type: 'signature',
|
||||
overflow: 'auto',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(9, 2),
|
||||
|
||||
@@ -0,0 +1,790 @@
|
||||
import { FieldType } from '@prisma/client';
|
||||
|
||||
import type { FieldTestData } from './field-alignment-pdf';
|
||||
|
||||
/**
|
||||
* Overflow test data extends FieldTestData with a `seedFieldMeta` property.
|
||||
*
|
||||
* - `fieldMeta`: Minimal field meta sent via the API. Omit properties that the API
|
||||
* auto-applies via ZEnvelopeFieldAndMetaSchema defaults (e.g. `overflow: 'auto'` for
|
||||
* date/email/signature fields). This tests that the API correctly sets defaults.
|
||||
*
|
||||
* - `seedFieldMeta`: Full field meta written directly to the DB by the seed function.
|
||||
* Must include ALL properties explicitly since the seed bypasses API validation/defaults.
|
||||
* Used by `seedOverflowTestDocument` in initial-seed.ts.
|
||||
*/
|
||||
export type OverflowFieldTestData = Omit<FieldTestData, 'fieldMeta'> & {
|
||||
fieldMeta?: FieldTestData['fieldMeta'];
|
||||
seedFieldMeta: FieldTestData['fieldMeta'];
|
||||
};
|
||||
|
||||
const SINGLE_LINE_HEIGHT = 1.75;
|
||||
const MULTI_LINE_HEIGHT = 12;
|
||||
const DEFAULT_BOX_WIDTH = 25;
|
||||
const SINGLE_TYPE_BOX_WIDTH = 35;
|
||||
const DEFAULT_START_X = 10;
|
||||
|
||||
/**
|
||||
* Pages 1-3: Date, Email, Signature
|
||||
* Single-line section (rows 0-2): single column, full width
|
||||
* Pages 1-2 multi-line: 3×3 grid (rows = TA_LEFT/CENTER/RIGHT, columns = short/medium/long text)
|
||||
* Page 3 multi-line: stacked single column (signature has no text align control)
|
||||
*/
|
||||
const SINGLE_TYPE_ML_COLUMN_X = [2.5, 35, 67.5];
|
||||
const SINGLE_TYPE_ML_BOX_WIDTH = 30;
|
||||
const SINGLE_TYPE_ML_ROW_Y = [45, 63, 83];
|
||||
|
||||
const calculateSingleLinePosition = (row: number) => {
|
||||
const singleLineYPositions = [15, 23, 31];
|
||||
|
||||
return {
|
||||
positionX: DEFAULT_START_X,
|
||||
positionY: singleLineYPositions[row],
|
||||
width: SINGLE_TYPE_BOX_WIDTH,
|
||||
height: SINGLE_LINE_HEIGHT,
|
||||
};
|
||||
};
|
||||
|
||||
/** Pages 1-2: multi-line 3×3 grid */
|
||||
const calculateMultiLinePosition = (row: number, column: number) => {
|
||||
return {
|
||||
positionX: SINGLE_TYPE_ML_COLUMN_X[column],
|
||||
positionY: SINGLE_TYPE_ML_ROW_Y[row],
|
||||
width: SINGLE_TYPE_ML_BOX_WIDTH,
|
||||
height: MULTI_LINE_HEIGHT,
|
||||
};
|
||||
};
|
||||
|
||||
/** Page 3: multi-line stacked single column */
|
||||
const calculateStackedMultiLinePosition = (row: number) => {
|
||||
const yPositions = [45, 63, 81];
|
||||
|
||||
return {
|
||||
positionX: DEFAULT_START_X,
|
||||
positionY: yPositions[row],
|
||||
width: SINGLE_TYPE_BOX_WIDTH,
|
||||
height: MULTI_LINE_HEIGHT,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Pages 4-5: Text Auto Mode (3x3 grid)
|
||||
*/
|
||||
const TEXT_AUTO_COLUMN_X = [5, 35.5, 66];
|
||||
const TEXT_AUTO_BOX_WIDTH = 28;
|
||||
|
||||
const calculateTextAutoPosition = (row: number, column: number, isSingleLine: boolean) => {
|
||||
if (isSingleLine) {
|
||||
// Single-line: all 9 items evenly spaced down the page.
|
||||
// Order: row0-col0, row0-col1, row0-col2, row1-col0, ...
|
||||
const startY = 10;
|
||||
const endY = 92;
|
||||
const spacing = (endY - startY) / 8; // 9 items, 8 gaps = 10.25%
|
||||
const itemIndex = row * 3 + column;
|
||||
|
||||
return {
|
||||
positionX: TEXT_AUTO_COLUMN_X[column],
|
||||
positionY: startY + itemIndex * spacing,
|
||||
width: TEXT_AUTO_BOX_WIDTH,
|
||||
height: SINGLE_LINE_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
// Multi-line: 3 rows evenly spaced, bottom row near page bottom.
|
||||
// Box is 12% tall. Top of last box at 80% so bottom edge is at 92%.
|
||||
const multiLineYPositions = [10, 45, 80];
|
||||
|
||||
return {
|
||||
positionX: TEXT_AUTO_COLUMN_X[column],
|
||||
positionY: multiLineYPositions[row],
|
||||
width: TEXT_AUTO_BOX_WIDTH,
|
||||
height: MULTI_LINE_HEIGHT,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Page 6: Explicit Modes
|
||||
*/
|
||||
const HORIZONTAL_CENTERED_X = (100 - DEFAULT_BOX_WIDTH) / 2; // 37.5%
|
||||
|
||||
const calculateExplicitHorizontalPosition = (row: number) => {
|
||||
const yPositions = [15, 21, 27];
|
||||
|
||||
return {
|
||||
positionX: HORIZONTAL_CENTERED_X,
|
||||
positionY: yPositions[row],
|
||||
width: DEFAULT_BOX_WIDTH,
|
||||
height: SINGLE_LINE_HEIGHT,
|
||||
};
|
||||
};
|
||||
|
||||
const calculateExplicitVerticalPosition = (column: number) => {
|
||||
const xPositions = [5, 37.5, 70];
|
||||
|
||||
return {
|
||||
positionX: xPositions[column],
|
||||
positionY: 43,
|
||||
width: DEFAULT_BOX_WIDTH,
|
||||
height: MULTI_LINE_HEIGHT,
|
||||
};
|
||||
};
|
||||
|
||||
export const OVERFLOW_TEST_FIELDS: OverflowFieldTestData[] = [
|
||||
/**
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*
|
||||
* PAGE 1: DATE OVERFLOW
|
||||
*
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*/
|
||||
// Single-line: Row 0-2 (default date meta — API auto-adds overflow: 'auto')
|
||||
{
|
||||
type: FieldType.DATE,
|
||||
fieldMeta: undefined,
|
||||
seedFieldMeta: { type: 'date', overflow: 'auto' },
|
||||
page: 1,
|
||||
...calculateSingleLinePosition(0),
|
||||
customText: 'Apr 16 2026',
|
||||
},
|
||||
{
|
||||
type: FieldType.DATE,
|
||||
fieldMeta: undefined,
|
||||
seedFieldMeta: { type: 'date', overflow: 'auto' },
|
||||
page: 1,
|
||||
...calculateSingleLinePosition(1),
|
||||
customText: 'Wednesday, April 16, 2026 at 14:30:45 UTC',
|
||||
},
|
||||
{
|
||||
type: FieldType.DATE,
|
||||
fieldMeta: undefined,
|
||||
seedFieldMeta: { type: 'date', overflow: 'auto' },
|
||||
page: 1,
|
||||
...calculateSingleLinePosition(2),
|
||||
customText:
|
||||
'Wednesday, April 16, 2026 at 14:30:45.123 Coordinated Universal Time signed in Melbourne, Australia',
|
||||
},
|
||||
// Multi-line 3×3: Row 0 = TA_LEFT (short / medium / long)
|
||||
{
|
||||
type: FieldType.DATE,
|
||||
fieldMeta: { type: 'date', textAlign: 'left' },
|
||||
seedFieldMeta: { type: 'date', overflow: 'auto', textAlign: 'left' },
|
||||
page: 1,
|
||||
...calculateMultiLinePosition(0, 0),
|
||||
customText: 'Apr 16 2026',
|
||||
},
|
||||
{
|
||||
type: FieldType.DATE,
|
||||
fieldMeta: { type: 'date', textAlign: 'left' },
|
||||
seedFieldMeta: { type: 'date', overflow: 'auto', textAlign: 'left' },
|
||||
page: 1,
|
||||
...calculateMultiLinePosition(0, 1),
|
||||
customText: 'Wednesday, April 16, 2026 at 14:30:45 Coordinated Universal Time',
|
||||
},
|
||||
{
|
||||
type: FieldType.DATE,
|
||||
fieldMeta: { type: 'date', textAlign: 'left' },
|
||||
seedFieldMeta: { type: 'date', overflow: 'auto', textAlign: 'left' },
|
||||
page: 1,
|
||||
...calculateMultiLinePosition(0, 2),
|
||||
customText:
|
||||
'Wednesday, April 16, 2026 at 14:30:45.123 Coordinated Universal Time signed in Melbourne, Australia. Count to 20, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty',
|
||||
},
|
||||
// Multi-line 3×3: Row 1 = TA_CENTER (short / medium / long)
|
||||
{
|
||||
type: FieldType.DATE,
|
||||
fieldMeta: { type: 'date', textAlign: 'center' },
|
||||
seedFieldMeta: { type: 'date', overflow: 'auto', textAlign: 'center' },
|
||||
page: 1,
|
||||
...calculateMultiLinePosition(1, 0),
|
||||
customText: 'Apr 16 2026',
|
||||
},
|
||||
{
|
||||
type: FieldType.DATE,
|
||||
fieldMeta: { type: 'date', textAlign: 'center' },
|
||||
seedFieldMeta: { type: 'date', overflow: 'auto', textAlign: 'center' },
|
||||
page: 1,
|
||||
...calculateMultiLinePosition(1, 1),
|
||||
customText: 'Wednesday, April 16, 2026 at 14:30:45 Coordinated Universal Time',
|
||||
},
|
||||
{
|
||||
type: FieldType.DATE,
|
||||
fieldMeta: { type: 'date', textAlign: 'center' },
|
||||
seedFieldMeta: { type: 'date', overflow: 'auto', textAlign: 'center' },
|
||||
page: 1,
|
||||
...calculateMultiLinePosition(1, 2),
|
||||
customText:
|
||||
'Wednesday, April 16, 2026 at 14:30:45.123 Coordinated Universal Time signed in Melbourne, Australia. Count to 20, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty',
|
||||
},
|
||||
// Multi-line 3×3: Row 2 = TA_RIGHT (short / medium / long)
|
||||
{
|
||||
type: FieldType.DATE,
|
||||
fieldMeta: { type: 'date', textAlign: 'right' },
|
||||
seedFieldMeta: { type: 'date', overflow: 'auto', textAlign: 'right' },
|
||||
page: 1,
|
||||
...calculateMultiLinePosition(2, 0),
|
||||
customText: 'Apr 16 2026',
|
||||
},
|
||||
{
|
||||
type: FieldType.DATE,
|
||||
fieldMeta: { type: 'date', textAlign: 'right' },
|
||||
seedFieldMeta: { type: 'date', overflow: 'auto', textAlign: 'right' },
|
||||
page: 1,
|
||||
...calculateMultiLinePosition(2, 1),
|
||||
customText: 'Wednesday, April 16, 2026 at 14:30:45 Coordinated Universal Time',
|
||||
},
|
||||
{
|
||||
type: FieldType.DATE,
|
||||
fieldMeta: { type: 'date', textAlign: 'right' },
|
||||
seedFieldMeta: { type: 'date', overflow: 'auto', textAlign: 'right' },
|
||||
page: 1,
|
||||
...calculateMultiLinePosition(2, 2),
|
||||
customText:
|
||||
'Wednesday, April 16, 2026 at 14:30:45.123 Coordinated Universal Time signed in Melbourne, Australia. Count to 20, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty',
|
||||
},
|
||||
|
||||
/**
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*
|
||||
* PAGE 2: EMAIL OVERFLOW
|
||||
*
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*/
|
||||
// Single-line: Row 0-2 (default email meta — API auto-adds overflow: 'auto')
|
||||
{
|
||||
type: FieldType.EMAIL,
|
||||
fieldMeta: undefined,
|
||||
seedFieldMeta: { type: 'email', overflow: 'auto' },
|
||||
page: 2,
|
||||
...calculateSingleLinePosition(0),
|
||||
customText: 'example@documenso.com',
|
||||
},
|
||||
{
|
||||
type: FieldType.EMAIL,
|
||||
fieldMeta: undefined,
|
||||
seedFieldMeta: { type: 'email', overflow: 'auto' },
|
||||
page: 2,
|
||||
...calculateSingleLinePosition(1),
|
||||
customText: 'example+medium-overflow-test@documenso.com',
|
||||
},
|
||||
{
|
||||
type: FieldType.EMAIL,
|
||||
fieldMeta: undefined,
|
||||
seedFieldMeta: { type: 'email', overflow: 'auto' },
|
||||
page: 2,
|
||||
...calculateSingleLinePosition(2),
|
||||
customText:
|
||||
'example+maximum-overflow-testing-across-the-page-width-to-verify-text-extends-beyond-field@documenso.com',
|
||||
},
|
||||
// Multi-line 3×3: Row 0 = TA_LEFT (short / medium / long)
|
||||
{
|
||||
type: FieldType.EMAIL,
|
||||
fieldMeta: { type: 'email', textAlign: 'left' },
|
||||
seedFieldMeta: { type: 'email', overflow: 'auto', textAlign: 'left' },
|
||||
page: 2,
|
||||
...calculateMultiLinePosition(0, 0),
|
||||
customText: 'example@documenso.com',
|
||||
},
|
||||
{
|
||||
type: FieldType.EMAIL,
|
||||
fieldMeta: { type: 'email', textAlign: 'left' },
|
||||
seedFieldMeta: { type: 'email', overflow: 'auto', textAlign: 'left' },
|
||||
page: 2,
|
||||
...calculateMultiLinePosition(0, 1),
|
||||
customText: 'example+medium-wrapped-text@documenso.com',
|
||||
},
|
||||
{
|
||||
type: FieldType.EMAIL,
|
||||
fieldMeta: { type: 'email', textAlign: 'left' },
|
||||
seedFieldMeta: { type: 'email', overflow: 'auto', textAlign: 'left' },
|
||||
page: 2,
|
||||
...calculateMultiLinePosition(0, 2),
|
||||
customText:
|
||||
'example+this-is-an-extremely-long-email-address-that-is-designed-to-overflow-vertically-out-of-the-field-box-and-extend-well-beyond-the-bottom-of-the-page-to-verify-that-the-vertical-overflow-logic-correctly-handles-text-that-wraps@documenso.com',
|
||||
},
|
||||
// Multi-line 3×3: Row 1 = TA_CENTER (short / medium / long)
|
||||
{
|
||||
type: FieldType.EMAIL,
|
||||
fieldMeta: { type: 'email', textAlign: 'center' },
|
||||
seedFieldMeta: { type: 'email', overflow: 'auto', textAlign: 'center' },
|
||||
page: 2,
|
||||
...calculateMultiLinePosition(1, 0),
|
||||
customText: 'example@documenso.com',
|
||||
},
|
||||
{
|
||||
type: FieldType.EMAIL,
|
||||
fieldMeta: { type: 'email', textAlign: 'center' },
|
||||
seedFieldMeta: { type: 'email', overflow: 'auto', textAlign: 'center' },
|
||||
page: 2,
|
||||
...calculateMultiLinePosition(1, 1),
|
||||
customText: 'example+medium-wrapped-text@documenso.com',
|
||||
},
|
||||
{
|
||||
type: FieldType.EMAIL,
|
||||
fieldMeta: { type: 'email', textAlign: 'center' },
|
||||
seedFieldMeta: { type: 'email', overflow: 'auto', textAlign: 'center' },
|
||||
page: 2,
|
||||
...calculateMultiLinePosition(1, 2),
|
||||
customText:
|
||||
'example+this-is-an-extremely-long-email-address-that-is-designed-to-overflow-vertically-out-of-the-field-box-and-extend-well-beyond-the-bottom-of-the-page-to-verify-that-the-vertical-overflow-logic-correctly-handles-text-that-wraps@documenso.com',
|
||||
},
|
||||
// Multi-line 3×3: Row 2 = TA_RIGHT (short / medium / long)
|
||||
{
|
||||
type: FieldType.EMAIL,
|
||||
fieldMeta: { type: 'email', textAlign: 'right' },
|
||||
seedFieldMeta: { type: 'email', overflow: 'auto', textAlign: 'right' },
|
||||
page: 2,
|
||||
...calculateMultiLinePosition(2, 0),
|
||||
customText: 'example@documenso.com',
|
||||
},
|
||||
{
|
||||
type: FieldType.EMAIL,
|
||||
fieldMeta: { type: 'email', textAlign: 'right' },
|
||||
seedFieldMeta: { type: 'email', overflow: 'auto', textAlign: 'right' },
|
||||
page: 2,
|
||||
...calculateMultiLinePosition(2, 1),
|
||||
customText: 'example+medium-wrapped-text@documenso.com',
|
||||
},
|
||||
{
|
||||
type: FieldType.EMAIL,
|
||||
fieldMeta: { type: 'email', textAlign: 'right' },
|
||||
seedFieldMeta: { type: 'email', overflow: 'auto', textAlign: 'right' },
|
||||
page: 2,
|
||||
...calculateMultiLinePosition(2, 2),
|
||||
customText:
|
||||
'example+this-is-an-extremely-long-email-address-that-is-designed-to-overflow-vertically-out-of-the-field-box-and-extend-well-beyond-the-bottom-of-the-page-to-verify-that-the-vertical-overflow-logic-correctly-handles-text-that-wraps@documenso.com',
|
||||
},
|
||||
|
||||
/**
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*
|
||||
* PAGE 3: SIGNATURE OVERFLOW
|
||||
*
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*/
|
||||
// Single-line: Row 0-2 (default signature meta — API auto-adds overflow: 'auto')
|
||||
{
|
||||
type: FieldType.SIGNATURE,
|
||||
fieldMeta: undefined,
|
||||
seedFieldMeta: { type: 'signature', overflow: 'auto' },
|
||||
page: 3,
|
||||
...calculateSingleLinePosition(0),
|
||||
customText: '',
|
||||
signature: 'John Doe',
|
||||
},
|
||||
{
|
||||
type: FieldType.SIGNATURE,
|
||||
fieldMeta: undefined,
|
||||
seedFieldMeta: { type: 'signature', overflow: 'auto' },
|
||||
page: 3,
|
||||
...calculateSingleLinePosition(1),
|
||||
customText: '',
|
||||
signature: 'My Signature should overflow the field width',
|
||||
},
|
||||
{
|
||||
type: FieldType.SIGNATURE,
|
||||
fieldMeta: undefined,
|
||||
seedFieldMeta: { type: 'signature', overflow: 'auto' },
|
||||
page: 3,
|
||||
...calculateSingleLinePosition(2),
|
||||
customText: '',
|
||||
signature:
|
||||
'My Signature should overflow the full signature field width and continue across the page to verify text is no longer clipped by the box boundary',
|
||||
},
|
||||
// Multi-line stacked: short / medium / long
|
||||
{
|
||||
type: FieldType.SIGNATURE,
|
||||
fieldMeta: undefined,
|
||||
seedFieldMeta: { type: 'signature', overflow: 'auto' },
|
||||
page: 3,
|
||||
...calculateStackedMultiLinePosition(0),
|
||||
customText: '',
|
||||
signature: 'John Doe',
|
||||
},
|
||||
{
|
||||
type: FieldType.SIGNATURE,
|
||||
fieldMeta: undefined,
|
||||
seedFieldMeta: { type: 'signature', overflow: 'auto' },
|
||||
page: 3,
|
||||
...calculateStackedMultiLinePosition(1),
|
||||
customText: '',
|
||||
signature: 'My Signature wraps within the tall field',
|
||||
},
|
||||
{
|
||||
type: FieldType.SIGNATURE,
|
||||
fieldMeta: undefined,
|
||||
seedFieldMeta: { type: 'signature', overflow: 'auto' },
|
||||
page: 3,
|
||||
...calculateStackedMultiLinePosition(2),
|
||||
customText: '',
|
||||
signature:
|
||||
'Count to 40, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty one twenty two twenty three twenty four twenty five twenty six twenty seven twenty eight twenty nine thirty thirty one thirty two thirty three thirty four thirty five thirty six thirty seven thirty eight thirty nine forty',
|
||||
},
|
||||
|
||||
/**
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*
|
||||
* PAGE 4: TEXT AUTO - SINGLE-LINE (3x3 grid)
|
||||
*
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*/
|
||||
// Row 0 (top)
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'top' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'top' },
|
||||
page: 4,
|
||||
...calculateTextAutoPosition(0, 0, true),
|
||||
customText: 'This text should overflow horizontally',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'top' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'top' },
|
||||
page: 4,
|
||||
...calculateTextAutoPosition(0, 1, true),
|
||||
customText: 'This text should overflow horizontally',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'top' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'top' },
|
||||
page: 4,
|
||||
...calculateTextAutoPosition(0, 2, true),
|
||||
customText: 'This text should overflow horizontally',
|
||||
},
|
||||
// Row 1 (middle)
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'middle' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'middle' },
|
||||
page: 4,
|
||||
...calculateTextAutoPosition(1, 0, true),
|
||||
customText: 'This text should overflow horizontally',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'middle' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'middle' },
|
||||
page: 4,
|
||||
...calculateTextAutoPosition(1, 1, true),
|
||||
customText: 'This text should overflow horizontally',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'middle' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'middle' },
|
||||
page: 4,
|
||||
...calculateTextAutoPosition(1, 2, true),
|
||||
customText: 'This text should overflow horizontally',
|
||||
},
|
||||
// Row 2 (bottom)
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'bottom' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'bottom' },
|
||||
page: 4,
|
||||
...calculateTextAutoPosition(2, 0, true),
|
||||
customText: 'This text should overflow horizontally',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'bottom' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'bottom' },
|
||||
page: 4,
|
||||
...calculateTextAutoPosition(2, 1, true),
|
||||
customText: 'This text should overflow horizontally',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'bottom' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'bottom' },
|
||||
page: 4,
|
||||
...calculateTextAutoPosition(2, 2, true),
|
||||
customText: 'This text should overflow horizontally',
|
||||
},
|
||||
|
||||
/**
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*
|
||||
* PAGE 5: TEXT AUTO - MULTI-LINE (3x3 grid)
|
||||
*
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*/
|
||||
// Row 0 (top)
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'top' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'top' },
|
||||
page: 5,
|
||||
...calculateTextAutoPosition(0, 0, false),
|
||||
customText:
|
||||
'Count to 20, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'top' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'top' },
|
||||
page: 5,
|
||||
...calculateTextAutoPosition(0, 1, false),
|
||||
customText:
|
||||
'Count to 20, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'top' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'top' },
|
||||
page: 5,
|
||||
...calculateTextAutoPosition(0, 2, false),
|
||||
customText:
|
||||
'Count to 20, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty',
|
||||
},
|
||||
// Row 1 (middle)
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'middle' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'middle' },
|
||||
page: 5,
|
||||
...calculateTextAutoPosition(1, 0, false),
|
||||
customText:
|
||||
'Count to 20, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'middle' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'middle' },
|
||||
page: 5,
|
||||
...calculateTextAutoPosition(1, 1, false),
|
||||
customText:
|
||||
'Count to 20, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'middle' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'middle' },
|
||||
page: 5,
|
||||
...calculateTextAutoPosition(1, 2, false),
|
||||
customText:
|
||||
'Count to 20, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty',
|
||||
},
|
||||
// Row 2 (bottom)
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'bottom' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'bottom' },
|
||||
page: 5,
|
||||
...calculateTextAutoPosition(2, 0, false),
|
||||
customText:
|
||||
'Count to 20, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'bottom' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'bottom' },
|
||||
page: 5,
|
||||
...calculateTextAutoPosition(2, 1, false),
|
||||
customText:
|
||||
'Count to 20, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'bottom' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'bottom' },
|
||||
page: 5,
|
||||
...calculateTextAutoPosition(2, 2, false),
|
||||
customText:
|
||||
'Count to 20, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty',
|
||||
},
|
||||
|
||||
/**
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*
|
||||
* PAGE 6: TEXT AUTO - MULTI-LINE HEIGHT OVERFLOW
|
||||
*
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*/
|
||||
// Same 3×3 grid as page 5 but with longer text that overflows vertically.
|
||||
// left / top
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'top' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'top' },
|
||||
page: 6,
|
||||
...calculateTextAutoPosition(0, 0, false),
|
||||
customText:
|
||||
'Count to 40, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty one twenty two twenty three twenty four twenty five twenty six twenty seven twenty eight twenty nine thirty thirty one thirty two thirty three thirty four thirty five thirty six thirty seven thirty eight thirty nine forty',
|
||||
},
|
||||
// center / top
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'top' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'top' },
|
||||
page: 6,
|
||||
...calculateTextAutoPosition(0, 1, false),
|
||||
customText:
|
||||
'Count to 40, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty one twenty two twenty three twenty four twenty five twenty six twenty seven twenty eight twenty nine thirty thirty one thirty two thirty three thirty four thirty five thirty six thirty seven thirty eight thirty nine forty',
|
||||
},
|
||||
// right / top
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'top' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'top' },
|
||||
page: 6,
|
||||
...calculateTextAutoPosition(0, 2, false),
|
||||
customText:
|
||||
'Count to 40, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty one twenty two twenty three twenty four twenty five twenty six twenty seven twenty eight twenty nine thirty thirty one thirty two thirty three thirty four thirty five thirty six thirty seven thirty eight thirty nine forty',
|
||||
},
|
||||
// left / middle
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'middle' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'middle' },
|
||||
page: 6,
|
||||
...calculateTextAutoPosition(1, 0, false),
|
||||
customText:
|
||||
'Count to 40, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty one twenty two twenty three twenty four twenty five twenty six twenty seven twenty eight twenty nine thirty thirty one thirty two thirty three thirty four thirty five thirty six thirty seven thirty eight thirty nine forty',
|
||||
},
|
||||
// center / middle
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'middle' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'middle' },
|
||||
page: 6,
|
||||
...calculateTextAutoPosition(1, 1, false),
|
||||
customText:
|
||||
'Count to 40, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty one twenty two twenty three twenty four twenty five twenty six twenty seven twenty eight twenty nine thirty thirty one thirty two thirty three thirty four thirty five thirty six thirty seven thirty eight thirty nine forty',
|
||||
},
|
||||
// right / middle
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'middle' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'middle' },
|
||||
page: 6,
|
||||
...calculateTextAutoPosition(1, 2, false),
|
||||
customText:
|
||||
'Count to 40, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty one twenty two twenty three twenty four twenty five twenty six twenty seven twenty eight twenty nine thirty thirty one thirty two thirty three thirty four thirty five thirty six thirty seven thirty eight thirty nine forty',
|
||||
},
|
||||
// left / bottom
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'bottom' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'left', verticalAlign: 'bottom' },
|
||||
page: 6,
|
||||
...calculateTextAutoPosition(2, 0, false),
|
||||
customText:
|
||||
'Count to 40, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty one twenty two twenty three twenty four twenty five twenty six twenty seven twenty eight twenty nine thirty thirty one thirty two thirty three thirty four thirty five thirty six thirty seven thirty eight thirty nine forty',
|
||||
},
|
||||
// center / bottom
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'bottom' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'center', verticalAlign: 'bottom' },
|
||||
page: 6,
|
||||
...calculateTextAutoPosition(2, 1, false),
|
||||
customText:
|
||||
'Count to 40, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty one twenty two twenty three twenty four twenty five twenty six twenty seven twenty eight twenty nine thirty thirty one thirty two thirty three thirty four thirty five thirty six thirty seven thirty eight thirty nine forty',
|
||||
},
|
||||
// right / bottom
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'bottom' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'auto', textAlign: 'right', verticalAlign: 'bottom' },
|
||||
page: 6,
|
||||
...calculateTextAutoPosition(2, 2, false),
|
||||
customText:
|
||||
'Count to 40, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty one twenty two twenty three twenty four twenty five twenty six twenty seven twenty eight twenty nine thirty thirty one thirty two thirty three thirty four thirty five thirty six thirty seven thirty eight thirty nine forty',
|
||||
},
|
||||
|
||||
/**
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*
|
||||
* PAGE 7: EXPLICIT MODES
|
||||
*
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*/
|
||||
// Section A: Horizontal mode (3 boxes in a row)
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'horizontal', textAlign: 'left' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'horizontal', textAlign: 'left' },
|
||||
page: 7,
|
||||
...calculateExplicitHorizontalPosition(0),
|
||||
customText: 'Explicit horizontal overflow text that should extend beyond the field',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'horizontal', textAlign: 'center' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'horizontal', textAlign: 'center' },
|
||||
page: 7,
|
||||
...calculateExplicitHorizontalPosition(1),
|
||||
customText: 'Explicit horizontal overflow text that should extend beyond the field',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'horizontal', textAlign: 'right' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'horizontal', textAlign: 'right' },
|
||||
page: 7,
|
||||
...calculateExplicitHorizontalPosition(2),
|
||||
customText: 'Explicit horizontal overflow text that should extend beyond the field',
|
||||
},
|
||||
// Section B: Vertical mode (3 boxes in a column)
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'vertical', verticalAlign: 'top' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'vertical', verticalAlign: 'top' },
|
||||
page: 7,
|
||||
...calculateExplicitVerticalPosition(0),
|
||||
customText:
|
||||
'Count to 30, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty one twenty two twenty three twenty four twenty five twenty six twenty seven twenty eight twenty nine thirty',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'vertical', verticalAlign: 'middle' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'vertical', verticalAlign: 'middle' },
|
||||
page: 7,
|
||||
...calculateExplicitVerticalPosition(1),
|
||||
customText:
|
||||
'Count to 30, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty one twenty two twenty three twenty four twenty five twenty six twenty seven twenty eight twenty nine thirty',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: { type: 'text', overflow: 'vertical', verticalAlign: 'bottom' },
|
||||
seedFieldMeta: { type: 'text', overflow: 'vertical', verticalAlign: 'bottom' },
|
||||
page: 7,
|
||||
...calculateExplicitVerticalPosition(2),
|
||||
customText:
|
||||
'Count to 30, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty one twenty two twenty three twenty four twenty five twenty six twenty seven twenty eight twenty nine thirty',
|
||||
},
|
||||
|
||||
/**
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*
|
||||
* PAGE 8: CROP MODE
|
||||
*
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*/
|
||||
// Box 1: Single-line crop
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: undefined,
|
||||
seedFieldMeta: { type: 'text' },
|
||||
page: 8,
|
||||
positionX: 10,
|
||||
positionY: 15,
|
||||
width: 25,
|
||||
height: SINGLE_LINE_HEIGHT,
|
||||
customText: 'This text should be cropped and not overflow',
|
||||
},
|
||||
// Box 2: Multi-line crop
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: undefined,
|
||||
seedFieldMeta: { type: 'text' },
|
||||
page: 8,
|
||||
positionX: 10,
|
||||
positionY: 30,
|
||||
width: 25,
|
||||
height: MULTI_LINE_HEIGHT,
|
||||
customText:
|
||||
'Count to 30, one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty one twenty two twenty three twenty four twenty five twenty six twenty seven twenty eight twenty nine thirty',
|
||||
},
|
||||
] as const;
|
||||
@@ -31,6 +31,9 @@ const baseUrl = `${WEBAPP_BASE_URL}/api/v2`;
|
||||
|
||||
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||
|
||||
/**
|
||||
* DON'T COMMIT THIS WITHOUT THE "SKIP" COMMAND.
|
||||
*/
|
||||
test.skip('seed alignment test document', async ({ page }) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
@@ -232,6 +235,47 @@ test('field placement visual regression', async ({ page, request }, testInfo) =>
|
||||
}),
|
||||
);
|
||||
|
||||
// Override email fields with test values after distribution.
|
||||
// Email fields are auto-inserted with the signer's email during distribution,
|
||||
// so we override customText directly to test with specific values.
|
||||
const emailFields = await prisma.field.findMany({
|
||||
where: {
|
||||
envelopeId: envelope.id,
|
||||
type: FieldType.EMAIL,
|
||||
},
|
||||
include: {
|
||||
envelopeItem: {
|
||||
select: {
|
||||
title: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
emailFields.map(async (field) => {
|
||||
const testFields =
|
||||
field.envelopeItem.title === 'alignment-pdf'
|
||||
? ALIGNMENT_TEST_FIELDS
|
||||
: FIELD_META_TEST_FIELDS;
|
||||
|
||||
const foundField = testFields.find(
|
||||
(f) =>
|
||||
f.type === FieldType.EMAIL &&
|
||||
field.page === f.page &&
|
||||
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
|
||||
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2),
|
||||
);
|
||||
|
||||
if (foundField) {
|
||||
await prisma.field.update({
|
||||
where: { id: field.id },
|
||||
data: { customText: foundField.customText },
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const recipientToken = envelope.recipients[0].token;
|
||||
const signUrl = `/sign/${recipientToken}`;
|
||||
|
||||
@@ -335,6 +379,45 @@ test.skip('download envelope images', async ({ page, request }) => {
|
||||
|
||||
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
|
||||
|
||||
// Override email fields with test values after distribution.
|
||||
const emailFields = await prisma.field.findMany({
|
||||
where: {
|
||||
envelopeId: envelope.id,
|
||||
type: FieldType.EMAIL,
|
||||
},
|
||||
include: {
|
||||
envelopeItem: {
|
||||
select: {
|
||||
title: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
emailFields.map(async (field) => {
|
||||
const testFields =
|
||||
field.envelopeItem.title === 'alignment-pdf'
|
||||
? ALIGNMENT_TEST_FIELDS
|
||||
: FIELD_META_TEST_FIELDS;
|
||||
|
||||
const foundField = testFields.find(
|
||||
(f) =>
|
||||
f.type === FieldType.EMAIL &&
|
||||
field.page === f.page &&
|
||||
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
|
||||
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2),
|
||||
);
|
||||
|
||||
if (foundField) {
|
||||
await prisma.field.update({
|
||||
where: { id: field.id },
|
||||
data: { customText: foundField.customText },
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const token = envelope.recipients[0].token;
|
||||
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
@@ -0,0 +1,543 @@
|
||||
import { createCanvas } from '@napi-rs/canvas';
|
||||
import type { TestInfo } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { DocumentStatus, EnvelopeType, FieldType } from '@prisma/client';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
|
||||
import pixelMatch from 'pixelmatch';
|
||||
import { PNG } from 'pngjs';
|
||||
|
||||
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedOverflowTestDocument } from '@documenso/prisma/seed/initial-seed';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../lib/constants/app';
|
||||
import { isBase64Image } from '../../../lib/constants/signatures';
|
||||
import { createApiToken } from '../../../lib/server-only/public-api/create-api-token';
|
||||
import { RecipientRole } from '../../../prisma/generated/types';
|
||||
import type {
|
||||
TCreateEnvelopePayload,
|
||||
TCreateEnvelopeResponse,
|
||||
} from '../../../trpc/server/envelope-router/create-envelope.types';
|
||||
import type { TDistributeEnvelopeRequest } from '../../../trpc/server/envelope-router/distribute-envelope.types';
|
||||
import { OVERFLOW_TEST_FIELDS } from '../../constants/field-overflow-pdf';
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||
const baseUrl = `${WEBAPP_BASE_URL}/api/v2`;
|
||||
|
||||
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||
|
||||
/**
|
||||
* DON'T COMMIT THIS WITHOUT THE "SKIP" COMMAND.
|
||||
*/
|
||||
test.skip('seed overflow test document', async ({ page }) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
email: 'example@documenso.com',
|
||||
},
|
||||
include: {
|
||||
ownedOrganisations: {
|
||||
include: {
|
||||
teams: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userId = user.id;
|
||||
const teamId = user.ownedOrganisations[0].teams[0].id;
|
||||
|
||||
await seedOverflowTestDocument({
|
||||
userId,
|
||||
teamId,
|
||||
recipientName: user.name || '',
|
||||
recipientEmail: user.email,
|
||||
insertFields: false,
|
||||
status: DocumentStatus.DRAFT,
|
||||
});
|
||||
});
|
||||
|
||||
test('overflow visual regression', async ({ page, request }, testInfo) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const { token } = await createApiToken({
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
// Step 1: Create initial envelope with overflow PDF
|
||||
const overflowPdf = fs.readFileSync(
|
||||
path.join(__dirname, '../../../../assets/field-overflow.pdf'),
|
||||
);
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
const overflowFields = OVERFLOW_TEST_FIELDS.map((field) => ({
|
||||
identifier: 'field-overflow',
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
fieldMeta: field.fieldMeta,
|
||||
}));
|
||||
|
||||
const createEnvelopePayload: TCreateEnvelopePayload = {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Overflow Test',
|
||||
recipients: [
|
||||
{
|
||||
email: user.email,
|
||||
name: user.name || '',
|
||||
role: RecipientRole.SIGNER,
|
||||
fields: overflowFields,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
formData.append('payload', JSON.stringify(createEnvelopePayload));
|
||||
formData.append('files', new File([overflowPdf], 'field-overflow', { type: 'application/pdf' }));
|
||||
|
||||
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: formData,
|
||||
});
|
||||
|
||||
expect(createEnvelopeRequest.ok()).toBeTruthy();
|
||||
expect(createEnvelopeRequest.status()).toBe(200);
|
||||
|
||||
const { id: createdEnvelopeId }: TCreateEnvelopeResponse = await createEnvelopeRequest.json();
|
||||
|
||||
const envelope = await prisma.envelope.findUniqueOrThrow({
|
||||
where: {
|
||||
id: createdEnvelopeId,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
envelopeItems: true,
|
||||
},
|
||||
});
|
||||
|
||||
const recipientId = envelope.recipients[0].id;
|
||||
const overflowItem = envelope.envelopeItems.find((item: { order: number }) => item.order === 1);
|
||||
|
||||
expect(recipientId).toBeDefined();
|
||||
expect(overflowItem).toBeDefined();
|
||||
|
||||
if (!overflowItem) {
|
||||
throw new Error('Envelope item not found');
|
||||
}
|
||||
|
||||
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: {
|
||||
envelopeId: envelope.id,
|
||||
} satisfies TDistributeEnvelopeRequest,
|
||||
});
|
||||
|
||||
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
|
||||
|
||||
const uninsertedFields = await prisma.field.findMany({
|
||||
where: {
|
||||
envelopeId: envelope.id,
|
||||
OR: [
|
||||
{
|
||||
inserted: false,
|
||||
},
|
||||
{
|
||||
// Include email fields because they are automatically inserted during envelope distribution.
|
||||
// We need to extract it to override their values for accurate comparison in tests.
|
||||
type: FieldType.EMAIL,
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
envelopeItem: {
|
||||
select: {
|
||||
title: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
uninsertedFields.map(async (field) => {
|
||||
const foundField = OVERFLOW_TEST_FIELDS.find(
|
||||
(f) =>
|
||||
field.page === f.page &&
|
||||
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
|
||||
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2) &&
|
||||
Number(field.width).toFixed(2) === f.width.toFixed(2) &&
|
||||
Number(field.height).toFixed(2) === f.height.toFixed(2),
|
||||
);
|
||||
|
||||
if (!foundField) {
|
||||
throw new Error('Field not found');
|
||||
}
|
||||
|
||||
await prisma.field.update({
|
||||
where: {
|
||||
id: field.id,
|
||||
},
|
||||
data: {
|
||||
inserted: true,
|
||||
customText: foundField.customText,
|
||||
signature: foundField.signature
|
||||
? {
|
||||
create: {
|
||||
recipientId: envelope.recipients[0].id,
|
||||
signatureImageAsBase64: isBase64Image(foundField.signature)
|
||||
? foundField.signature
|
||||
: null,
|
||||
typedSignature: isBase64Image(foundField.signature) ? null : foundField.signature,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Override email fields with test values after distribution.
|
||||
// Email fields are auto-inserted with the signer's email during distribution,
|
||||
// so we override customText directly to test overflow with specific text lengths.
|
||||
const emailFields = await prisma.field.findMany({
|
||||
where: {
|
||||
envelopeId: envelope.id,
|
||||
type: FieldType.EMAIL,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
emailFields.map(async (field) => {
|
||||
const foundField = OVERFLOW_TEST_FIELDS.find(
|
||||
(f) =>
|
||||
f.type === FieldType.EMAIL &&
|
||||
field.page === f.page &&
|
||||
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
|
||||
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2),
|
||||
);
|
||||
|
||||
if (foundField) {
|
||||
await prisma.field.update({
|
||||
where: { id: field.id },
|
||||
data: { customText: foundField.customText },
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const recipientToken = envelope.recipients[0].token;
|
||||
const signUrl = `/sign/${recipientToken}`;
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: signUrl,
|
||||
});
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
|
||||
await expect(async () => {
|
||||
const { status } = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: envelope.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toBe(DocumentStatus.COMPLETED);
|
||||
}).toPass({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
const completedDocument = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: envelope.id,
|
||||
},
|
||||
include: {
|
||||
envelopeItems: {
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const storedImages = fs.readdirSync(path.join(__dirname, '../../visual-regression'));
|
||||
|
||||
await Promise.all(
|
||||
completedDocument.envelopeItems.map(async (item) => {
|
||||
const documentUrl = getEnvelopeItemPdfUrl({
|
||||
type: 'download',
|
||||
envelopeItem: item,
|
||||
token: recipientToken,
|
||||
version: 'signed',
|
||||
});
|
||||
|
||||
const pdfData = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
||||
|
||||
const loadedImages = storedImages
|
||||
.filter((image) => image.startsWith(`field-overflow-`))
|
||||
.sort((leftImage, rightImage) => {
|
||||
return (
|
||||
getVisualRegressionImageIndex(leftImage) - getVisualRegressionImageIndex(rightImage)
|
||||
);
|
||||
})
|
||||
.map((image) => fs.readFileSync(path.join(__dirname, '../../visual-regression', image)));
|
||||
|
||||
await compareSignedPdfWithImages({
|
||||
id: 'field-overflow',
|
||||
pdfData: new Uint8Array(pdfData),
|
||||
images: loadedImages,
|
||||
testInfo,
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Used to download the envelope images when updating the visual regression test.
|
||||
*
|
||||
* DON'T COMMIT THIS WITHOUT THE "SKIP" COMMAND.
|
||||
*/
|
||||
test.skip('download overflow images', async ({ page, request }) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const { token: apiToken } = await createApiToken({
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
const envelope = await seedOverflowTestDocument({
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
recipientName: user.name || '',
|
||||
recipientEmail: user.email,
|
||||
insertFields: true,
|
||||
status: DocumentStatus.DRAFT,
|
||||
});
|
||||
|
||||
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${apiToken}` },
|
||||
data: {
|
||||
envelopeId: envelope.id,
|
||||
} satisfies TDistributeEnvelopeRequest,
|
||||
});
|
||||
|
||||
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
|
||||
|
||||
// Override email fields with test values after distribution.
|
||||
const emailFields = await prisma.field.findMany({
|
||||
where: {
|
||||
envelopeId: envelope.id,
|
||||
type: FieldType.EMAIL,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
emailFields.map(async (field) => {
|
||||
const foundField = OVERFLOW_TEST_FIELDS.find(
|
||||
(f) =>
|
||||
f.type === FieldType.EMAIL &&
|
||||
field.page === f.page &&
|
||||
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
|
||||
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2),
|
||||
);
|
||||
|
||||
if (foundField) {
|
||||
await prisma.field.update({
|
||||
where: { id: field.id },
|
||||
data: { customText: foundField.customText },
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const token = envelope.recipients[0].token;
|
||||
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: signUrl,
|
||||
});
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
|
||||
await expect(async () => {
|
||||
const { status } = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: envelope.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toBe(DocumentStatus.COMPLETED);
|
||||
}).toPass({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
const completedDocument = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: envelope.id,
|
||||
},
|
||||
include: {
|
||||
envelopeItems: {
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
completedDocument.envelopeItems.map(async (item) => {
|
||||
const documentUrl = getEnvelopeItemPdfUrl({
|
||||
type: 'download',
|
||||
envelopeItem: item,
|
||||
token,
|
||||
version: 'signed',
|
||||
});
|
||||
|
||||
const pdfData = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
||||
|
||||
const pdfImages = await renderPdfToImage(new Uint8Array(pdfData));
|
||||
|
||||
for (const [index, { image }] of pdfImages.entries()) {
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '../../visual-regression', `field-overflow-${index}.png`),
|
||||
new Uint8Array(image),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Helper functions
|
||||
// ============================================================================
|
||||
|
||||
async function renderPdfToImage(pdfBytes: Uint8Array) {
|
||||
const loadingTask = pdfjsLib.getDocument({ data: pdfBytes });
|
||||
const pdf = await loadingTask.promise;
|
||||
|
||||
// Increase for higher resolution
|
||||
const scale = 4;
|
||||
|
||||
return await Promise.all(
|
||||
Array.from({ length: pdf.numPages }, async (_, index) => {
|
||||
const page = await pdf.getPage(index + 1);
|
||||
|
||||
const viewport = page.getViewport({ scale });
|
||||
|
||||
const canvas = createCanvas(viewport.width, viewport.height);
|
||||
const canvasContext = canvas.getContext('2d');
|
||||
canvasContext.imageSmoothingEnabled = false;
|
||||
|
||||
await page.render({
|
||||
// @ts-expect-error @napi-rs/canvas satisfies runtime requirements for pdfjs
|
||||
canvas,
|
||||
// @ts-expect-error @napi-rs/canvas satisfies runtime requirements for pdfjs
|
||||
canvasContext,
|
||||
viewport,
|
||||
}).promise;
|
||||
|
||||
return {
|
||||
image: await canvas.encode('png'),
|
||||
|
||||
// Rounded down because the certificate page somehow gives dimensions with decimals
|
||||
width: Math.floor(viewport.width),
|
||||
height: Math.floor(viewport.height),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
type CompareSignedPdfWithImagesOptions = {
|
||||
id: string;
|
||||
pdfData: Uint8Array;
|
||||
images: Buffer[];
|
||||
testInfo: TestInfo;
|
||||
};
|
||||
|
||||
const compareSignedPdfWithImages = async ({
|
||||
id,
|
||||
pdfData,
|
||||
images,
|
||||
testInfo,
|
||||
}: CompareSignedPdfWithImagesOptions) => {
|
||||
const renderedImages = await renderPdfToImage(pdfData);
|
||||
|
||||
expect(images).toHaveLength(renderedImages.length);
|
||||
|
||||
for (const [index, { image, width, height }] of renderedImages.entries()) {
|
||||
const isCertificate = index === renderedImages.length - 1;
|
||||
|
||||
// Skip certificate page comparison.
|
||||
if (isCertificate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const diff = new PNG({ width, height });
|
||||
|
||||
const storedImage = PNG.sync.read(images[index]).data;
|
||||
|
||||
const newImage = PNG.sync.read(image).data;
|
||||
|
||||
const comparison = pixelMatch(
|
||||
new Uint8Array(storedImage),
|
||||
new Uint8Array(newImage),
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
diff.data as unknown as Uint8Array,
|
||||
width,
|
||||
height,
|
||||
{
|
||||
threshold: 0.25,
|
||||
// includeAA: true, // This allows stricter testing.
|
||||
},
|
||||
);
|
||||
console.log(`${id}-${index}: ${comparison}`);
|
||||
|
||||
const diffFilePath = path.join(testInfo.outputPath(), `${id}-${index}-diff.png`);
|
||||
const oldFilePath = path.join(testInfo.outputPath(), `${id}-${index}-old.png`);
|
||||
const newFilePath = path.join(testInfo.outputPath(), `${id}-${index}-new.png`);
|
||||
|
||||
fs.writeFileSync(diffFilePath, new Uint8Array(PNG.sync.write(diff)));
|
||||
fs.writeFileSync(oldFilePath, new Uint8Array(images[index]));
|
||||
fs.writeFileSync(newFilePath, new Uint8Array(image));
|
||||
|
||||
expect.soft(comparison).toBeLessThan(2);
|
||||
}
|
||||
};
|
||||
|
||||
const getVisualRegressionImageIndex = (image: string) => {
|
||||
const match = image.match(/-(\d+)\.png$/);
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`Unexpected visual regression image name: ${image}`);
|
||||
}
|
||||
|
||||
return Number(match[1]);
|
||||
};
|
||||
|
Before Width: | Height: | Size: 352 KiB After Width: | Height: | Size: 354 KiB |
|
After Width: | Height: | Size: 622 KiB |
|
After Width: | Height: | Size: 636 KiB |
|
After Width: | Height: | Size: 357 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 575 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 506 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 267 KiB |