Skip to content

Commit b56fd7f

Browse files
committed
chore: add pdf-parse for obligation invoice
1 parent 95cf9a4 commit b56fd7f

File tree

3 files changed

+217
-16
lines changed

3 files changed

+217
-16
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { expect } from "@playwright/test";
2+
import { PDFParse } from "pdf-parse";
3+
import {
4+
INVOICE_VOID_WATERMARK_REGEX,
5+
FEES_AND_ADJUSTMENTS_TEXT,
6+
INVOICE_NUMBER_LABEL_REGEX,
7+
DEFAULT_ADJUSTMENT_REGEX,
8+
} from "@/compliance-e2e/utils/constants";
9+
10+
export class ObligationInvoicePOM {
11+
private constructor(
12+
private readonly parsed: any,
13+
private readonly text: string,
14+
) {}
15+
16+
static async fromBuffer(buffer: Buffer): Promise<ObligationInvoicePOM> {
17+
const parser = new PDFParse({ data: buffer });
18+
const parsed = await parser.getText();
19+
20+
const normalizedText = String(parsed?.text ?? "")
21+
.replace(/\s+/g, " ")
22+
.trim();
23+
24+
return new ObligationInvoicePOM(parsed, normalizedText);
25+
}
26+
27+
// -----------------------
28+
// Assertions (Chainable)
29+
// -----------------------
30+
31+
assertNotVoid(): this {
32+
expect(this.text).not.toMatch(INVOICE_VOID_WATERMARK_REGEX);
33+
return this;
34+
}
35+
36+
assertHasFeesAndAdjustments(): this {
37+
expect(this.text).toContain(FEES_AND_ADJUSTMENTS_TEXT);
38+
return this;
39+
}
40+
41+
assertHasAdjustmentLine(matcher: RegExp = DEFAULT_ADJUSTMENT_REGEX): this {
42+
expect(this.text).toMatch(matcher);
43+
return this;
44+
}
45+
46+
assertHasInvoiceNumber(): this {
47+
expect(this.text).toMatch(INVOICE_NUMBER_LABEL_REGEX);
48+
return this;
49+
}
50+
51+
// -----------------------
52+
// Getters
53+
// -----------------------
54+
55+
getText(): string {
56+
return this.text;
57+
}
58+
}

bciers/apps/compliance-e2e/tests/workflows/manage-obligation/supplementary/decrease.spec.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { ComplianceSummariesPOM } from "@/compliance-e2e/poms/compliance-summari
1010
import { ComplianceSetupPOM } from "@/compliance-e2e/poms/compliance-setup";
1111
import { ReportSetUpPOM } from "@/reporting-e2e/poms/report-setup";
1212
import { REVIEW_OBLIGATION_URL_PATTERN } from "@/compliance-e2e/utils/constants";
13+
import { ReviewComplianceObligationPOM } from "@/compliance-e2e/poms/manage-obligation/review-compliance-obligation";
14+
import { ObligationInvoicePOM } from "@/compliance-e2e/poms/manage-obligation/obligation-invoice";
1315

1416
// 👤 run test using the storageState for role UserRole.INDUSTRY_USER_ADMIN
1517
const test = setupBeforeEachTest(UserRole.INDUSTRY_USER_ADMIN);
@@ -43,26 +45,26 @@ test.describe("Test supplementary compliance report version flow", () => {
4345
});
4446

4547
// Assert BEFORE generate invoice
48+
const reviewObligationPOM = new ReviewComplianceObligationPOM(page);
4649
// Open summary and generate invoice
4750
await gridComplianceSummaries.openActionForOperation({
4851
operationName: ComplianceOperations.OBLIGATION_NOT_MET,
4952
linkName: GridActionText.MANAGE_OBLIGATION,
5053
urlPattern: REVIEW_OBLIGATION_URL_PATTERN,
5154
});
52-
// const reviewObligationPOM = new ReviewComplianceObligationPOM(page);
5355
// Capture invoice pdf as buffer
54-
// const pdfBuffer = await reviewObligationPOM.generateInvoiceAndGetPdfBuffer(
55-
// 2,
56-
// "obligation",
57-
// );
58-
// // create invoice object from buffer
59-
// const invoice = await ObligationInvoicePOM.fromBuffer(pdfBuffer);
56+
const pdfBuffer = await reviewObligationPOM.generateInvoiceAndGetPdfBuffer(
57+
2,
58+
"obligation",
59+
);
60+
// create invoice object from buffer
61+
const invoice = await ObligationInvoicePOM.fromBuffer(pdfBuffer);
6062

61-
// invoice
62-
// .assertHasInvoiceNumber()
63-
// .assertHasFeesAndAdjustments()
64-
// .assertHasAdjustmentLine()
65-
// .assertNotVoid();
63+
invoice
64+
.assertHasInvoiceNumber()
65+
.assertHasFeesAndAdjustments()
66+
.assertHasAdjustmentLine()
67+
.assertNotVoid();
6668

6769
// Submit supplementary decrease
6870
await gridReportingReports.route();

bciers/libs/e2e/src/utils/helpers.ts

Lines changed: 145 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
webkit,
99
Browser,
1010
} from "@playwright/test";
11-
import { baseUrlSetup } from "@bciers/e2e/utils/constants";
11+
import { baseUrlSetup, GRID_ROOT } from "@bciers/e2e/utils/constants";
1212
import { DataTestID, MessageTextResponse } from "@bciers/e2e/utils/enums";
1313
import AxeBuilder from "@axe-core/playwright";
1414
import path from "node:path";
@@ -33,14 +33,21 @@ export async function analyzeAccessibility(
3333
expect(accessibilityScanResults.violations).toEqual([]);
3434
}
3535

36+
/**
37+
* Clicks a button by accessible name and optionally waits for navigation.
38+
* @param page - Playwright Page instance
39+
* @param buttonText - Button accessible name or RegExp
40+
* @param opts.inForm - Scope the button search to a <form> (default: false)
41+
* @param opts.waitForUrl - RegExp to wait for after clicking (optional)
42+
*/
3643
export async function clickButton(
3744
page: Page,
3845
buttonText: string | RegExp,
3946
opts?: {
4047
inForm?: boolean; // default false
4148
waitForUrl?: RegExp;
4249
},
43-
) {
50+
): Promise<void> {
4451
const { inForm = false, waitForUrl } = opts ?? {};
4552

4653
const name =
@@ -49,8 +56,18 @@ export async function clickButton(
4956
const root = inForm ? page.locator("form") : page;
5057
const button = root.getByRole("button", { name });
5158

59+
await expect(button).toBeVisible({ timeout: 30_000 });
60+
await expect(button).toBeEnabled({ timeout: 30_000 });
61+
5262
if (waitForUrl) {
53-
await Promise.all([page.waitForURL(waitForUrl), button.click()]);
63+
// Use a lighter wait for SPA navigation
64+
await Promise.all([
65+
page.waitForURL((u) => waitForUrl.test(u.toString()), {
66+
timeout: 30_000,
67+
waitUntil: "domcontentloaded",
68+
}),
69+
button.click(),
70+
]);
5471
} else {
5572
await button.click();
5673
}
@@ -83,6 +100,55 @@ export async function fillDropdownByLabel(
83100
await input.fill(value);
84101
}
85102

103+
export async function fillInputValueByLabel(
104+
page: Page,
105+
label: string | RegExp,
106+
value: string | number,
107+
opts?: {
108+
blur?: "tab" | "enter" | "none"; // default "tab"
109+
},
110+
): Promise<void> {
111+
const { blur = "tab" } = opts ?? {};
112+
113+
const name = label instanceof RegExp ? label : new RegExp(label, "i");
114+
const field = page.getByLabel(name);
115+
116+
await expect(field).toBeVisible({ timeout: 30_000 });
117+
await expect(field).toBeEnabled({ timeout: 30_000 });
118+
119+
await field.click();
120+
await field.press("Control+A");
121+
await field.press("Backspace");
122+
123+
await field.fill(String(value));
124+
125+
if (blur === "tab") await field.press("Tab");
126+
if (blur === "enter") await field.press("Enter");
127+
}
128+
129+
export async function fillInputValueByLocator(
130+
field: Locator,
131+
value: string | number,
132+
opts?: {
133+
blur?: "tab" | "enter" | "none"; // default "tab"
134+
},
135+
): Promise<void> {
136+
const { blur = "tab" } = opts ?? {};
137+
138+
await expect(field).toBeVisible({ timeout: 30_000 });
139+
await expect(field).toBeEnabled({ timeout: 30_000 });
140+
141+
// focus and clear field
142+
await field.click();
143+
await field.press("Control+A");
144+
await field.press("Backspace");
145+
146+
await field.fill(String(value));
147+
148+
if (blur === "tab") await field.press("Tab");
149+
if (blur === "enter") await field.press("Enter");
150+
}
151+
86152
export async function checkAllRadioButtons(page: Page) {
87153
const radioButtons = page.getByRole("radio", { name: "Yes" });
88154
// Wait for at least one radio button to be visible before counting.
@@ -100,6 +166,24 @@ export async function checkAllRadioButtons(page: Page) {
100166
}
101167
}
102168

169+
export async function checkCheckboxByLabel(
170+
page: Page,
171+
label: string | RegExp,
172+
): Promise<void> {
173+
const name = label instanceof RegExp ? label : new RegExp(label, "i");
174+
175+
const checkbox = page.getByRole("checkbox", { name });
176+
177+
await expect(checkbox).toBeVisible({ timeout: 30_000 });
178+
await expect(checkbox).toBeEnabled({ timeout: 30_000 });
179+
180+
if (!(await checkbox.isChecked())) {
181+
await checkbox.check();
182+
}
183+
184+
await expect(checkbox).toBeChecked();
185+
}
186+
103187
// 🛠️ Function: checks expected alert message
104188
export async function checkAlertMessage(
105189
page: Page,
@@ -231,7 +315,7 @@ export async function tableColumnNamesAreCorrect(
231315
expectedColumnNames: string[],
232316
) {
233317
const columnHeaders = page.locator(".MuiDataGrid-columnHeaderTitle");
234-
const actualColumnNames = await columnHeaders.allTextContents();
318+
const actualColumnNames = await columnHeaders.getByText();
235319
expect(actualColumnNames).toEqual(expectedColumnNames);
236320
}
237321

@@ -525,3 +609,60 @@ export function getCrvIdFromUrl({ url }: { url: string }): number {
525609
if (!match) throw new Error(`Could not extract crvId from URL: ${url}`);
526610
return Number(match[1]);
527611
}
612+
613+
/**
614+
* Wait until the grid is actually "ready":
615+
* - GRID_ROOT exists
616+
* - root + role=grid visible
617+
* - (optional) progressbar/spinner is gone
618+
* - at least one gridcell exists
619+
*
620+
* Tolerates re-mounts (e.g. HMR) with re-check of counts on every attempt
621+
*/
622+
export async function waitForGridReady(
623+
page: Page,
624+
options?: { timeout?: number },
625+
): Promise<void> {
626+
const timeout = options?.timeout ?? 30_000;
627+
628+
await expect(async () => {
629+
const rootCount = await page.locator(GRID_ROOT).count();
630+
expect(rootCount).toBeGreaterThan(0);
631+
632+
const grid = page.locator(GRID_ROOT);
633+
await expect(grid).toBeVisible();
634+
635+
const progressbar = grid.locator('[role="progressbar"]');
636+
const anyCell = grid.locator('[role="gridcell"]').first();
637+
638+
if ((await progressbar.count()) > 0) {
639+
await expect(progressbar).toBeHidden();
640+
}
641+
642+
await expect(anyCell).toBeVisible();
643+
}).toPass({ timeout });
644+
}
645+
646+
export async function getGridRowByText(
647+
page: Page,
648+
rowText: string | RegExp,
649+
): Promise<Locator> {
650+
await waitForGridReady(page, { timeout: 30_000 });
651+
652+
const grid = page.locator(GRID_ROOT).first().locator('[role="grid"]').first();
653+
const row = grid.getByRole("row").filter({ hasText: rowText }).first();
654+
655+
await expect(async () => {
656+
const matchingRowCount = await row.count();
657+
const totalRowCount = await grid.getByRole("row").count();
658+
659+
expect(totalRowCount).toBeGreaterThan(0);
660+
expect(matchingRowCount).toBeGreaterThan(0);
661+
await expect(row).toBeVisible();
662+
663+
const cellCount = await row.locator('[role="gridcell"]').count();
664+
expect(cellCount).toBeGreaterThan(0);
665+
}).toPass({ timeout: 30_000 });
666+
667+
return row;
668+
}

0 commit comments

Comments
 (0)