From d29cfc8fb384d6e64c2753a5157430960125b040 Mon Sep 17 00:00:00 2001 From: Alan Nadolny Date: Wed, 27 May 2026 11:59:41 +0200 Subject: [PATCH 01/11] OBPIH-7884 Add PDF and table utility functions for text extraction and row value capture --- src/utils/pdfUtils.ts | 33 +++++++++++++++++++++++++++++++++ src/utils/tableUtils.ts | 18 ++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/utils/pdfUtils.ts create mode 100644 src/utils/tableUtils.ts diff --git a/src/utils/pdfUtils.ts b/src/utils/pdfUtils.ts new file mode 100644 index 0000000..5a7a251 --- /dev/null +++ b/src/utils/pdfUtils.ts @@ -0,0 +1,33 @@ +import fs from 'node:fs'; + +import { getDocument } from 'pdfjs-dist/legacy/build/pdf.js'; +import type { TextItem } from 'pdfjs-dist/types/src/display/api'; + +export const pdfContainsValues = async ( + filePath: string, + values: string[] +): Promise => { + const pdfText = await extractPdfText(filePath); + return values.every((value) => pdfText.includes(value)); +}; + +export const extractPdfText = async (filePath: string): Promise => { + const data = new Uint8Array(fs.readFileSync(filePath)); + const doc = await getDocument({ data }).promise; + + // Get page numbers available in the PDF + const pageNumbers = Array.from({ length: doc.numPages }, (_, i) => i + 1); + // Extract text from each page + const pageTexts = await Promise.all( + pageNumbers.map(async (pageNum) => { + const page = await doc.getPage(pageNum); + const content = await page.getTextContent(); + return content.items + .map((item) => ('str' in item ? (item as TextItem).str : '')) + .join(' '); + }) + ); + + // Combine text from all pages into a single string + return pageTexts.join('\n'); +}; diff --git a/src/utils/tableUtils.ts b/src/utils/tableUtils.ts new file mode 100644 index 0000000..39889f9 --- /dev/null +++ b/src/utils/tableUtils.ts @@ -0,0 +1,18 @@ +import { Locator } from '@playwright/test'; + +export const captureRowValues = async ( + rowCount: number, + getRow: (index: number) => TRow, + ...getters: ((row: TRow) => Locator)[] +): Promise => { + const rows = await Promise.all( + Array.from({ length: rowCount }, async (_, i) => { + const row = getRow(i); + return Promise.all(getters.map((g) => g(row).textContent())); + }) + ); + return rows + .flat() + .map((v) => v?.trim()) + .filter((v): v is string => Boolean(v)); +}; \ No newline at end of file From 5ef129e77dae60848a3b6b2f66840794f470997f Mon Sep 17 00:00:00 2001 From: Alan Nadolny Date: Wed, 27 May 2026 12:00:05 +0200 Subject: [PATCH 02/11] OBPIH-7884 Add pdfjs-dist dependency for PDF processing --- package-lock.json | 660 ++++++++++++++++++++++++++++++++++++++++++++-- package.json | 1 + 2 files changed, 641 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1062907..0f0161c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "xlsx": "~0.18.5" }, "devDependencies": { - "@playwright/test": "^1.57.0", + "@playwright/test": "~1.57.0", "@types/lodash": "~4.17.0", "@types/node": "~20.11.30", "@typescript-eslint/eslint-plugin": "~5.62.0", @@ -29,6 +29,7 @@ "eslint-plugin-playwright": "~1.0.1", "eslint-plugin-promise": "~6.0.0", "eslint-plugin-simple-import-sort": "~10.0.0", + "pdfjs-dist": "~3.11.174", "prettier": "~3.2.5", "typescript": "~5.1.6" } @@ -133,6 +134,28 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -183,6 +206,24 @@ "node": ">=18" } }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -408,8 +449,17 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "deprecated": "Potential CWE-502 - Update to 1.3.1 or higher", "dev": true }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -439,6 +489,20 @@ "node": ">=0.8" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -479,6 +543,30 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -688,6 +776,23 @@ "node": ">=6" } }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/cfb": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", @@ -716,6 +821,17 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, "node_modules/codepage": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", @@ -742,12 +858,31 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -846,6 +981,20 @@ } } }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -886,6 +1035,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -921,6 +1089,14 @@ "url": "https://dotenvx.com" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/env-var": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/env-var/-/env-var-7.4.1.tgz", @@ -1699,6 +1875,34 @@ "node": ">=0.8" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1755,6 +1959,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -1795,7 +2022,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -1961,6 +2188,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1973,6 +2208,21 @@ "node": ">= 0.4" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -2145,6 +2395,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2404,6 +2665,34 @@ "node": ">=10" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2426,6 +2715,20 @@ "node": ">=8.6" } }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2447,12 +2750,74 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/nan": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", + "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2465,6 +2830,71 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -2660,6 +3090,31 @@ "node": ">=8" } }, + "node_modules/path2d-polyfill": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pdfjs-dist": { + "version": "3.11.174", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", + "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d-polyfill": "^2.0.1" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2672,24 +3127,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/playwright": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", - "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", - "dev": true, - "dependencies": { - "playwright-core": "1.57.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, "node_modules/playwright-core": { "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", @@ -2764,6 +3201,22 @@ } ] }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -2887,6 +3340,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -2919,6 +3394,14 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -2999,6 +3482,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3019,6 +3545,33 @@ "node": ">=0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -3125,6 +3678,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3143,6 +3716,14 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -3316,6 +3897,34 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3366,6 +3975,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wmf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", diff --git a/package.json b/package.json index 718543f..ea05f3e 100755 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "eslint-plugin-playwright": "~1.0.1", "eslint-plugin-promise": "~6.0.0", "eslint-plugin-simple-import-sort": "~10.0.0", + "pdfjs-dist": "~3.11.174", "prettier": "~3.2.5", "typescript": "~5.1.6" }, From d81030e7a6f912dfb549f3c230d7ac58e826f56d Mon Sep 17 00:00:00 2001 From: Alan Nadolny Date: Wed, 27 May 2026 12:00:22 +0200 Subject: [PATCH 03/11] OBPIH-7884 Refactor FileIOUtils to use synchronous file deletion and add writeBufferToFile function --- src/utils/FileIOUtils.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/FileIOUtils.ts b/src/utils/FileIOUtils.ts index 4cf04c7..1ce235f 100644 --- a/src/utils/FileIOUtils.ts +++ b/src/utils/FileIOUtils.ts @@ -52,12 +52,12 @@ const writeToFile = (path: string, data: unknown) => { const deleteFile = (path: string) => { if (fs.existsSync(path)) { - fs.unlink(path, (err) => { - if (err) { - console.log(err); - } - }); + fs.unlinkSync(path); } }; -export { deleteFile, readFile, writeToFile }; +const writeBufferToFile = (path: string, data: Buffer): void => { + fs.writeFileSync(path, data); +}; + +export { deleteFile, readFile, writeBufferToFile, writeToFile }; From 35ed78084f9aca1bd7d271cc7f434c2893df1de8 Mon Sep 17 00:00:00 2001 From: Alan Nadolny Date: Wed, 27 May 2026 12:00:45 +0200 Subject: [PATCH 04/11] OBPIH-7884 Add functions for row data retrieval and integrate FileHandler in PutawayDetailsPage --- .../putawayDetails/PutawayDetailsPage.ts | 3 +++ .../components/ItemStatusTable.ts | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/pages/putaway/putawayDetails/PutawayDetailsPage.ts b/src/pages/putaway/putawayDetails/PutawayDetailsPage.ts index cde87f4..ef281a5 100644 --- a/src/pages/putaway/putawayDetails/PutawayDetailsPage.ts +++ b/src/pages/putaway/putawayDetails/PutawayDetailsPage.ts @@ -1,5 +1,6 @@ import { expect, Page } from '@playwright/test'; +import FileHandler from '@/components/FileHandler'; import BasePageModel from '@/pages/BasePageModel'; import AuditingTable from './components/AuditingTable'; @@ -14,6 +15,7 @@ class PutawayDetailsPage extends BasePageModel { itemStatusTable: ItemStatusTable; itemDetailsTable: ItemDetailsTable; auditingTable: AuditingTable; + fileHandler: FileHandler; constructor(page: Page) { super(page); @@ -22,6 +24,7 @@ class PutawayDetailsPage extends BasePageModel { this.itemStatusTable = new ItemStatusTable(page); this.itemDetailsTable = new ItemDetailsTable(page); this.auditingTable = new AuditingTable(page); + this.fileHandler = new FileHandler(page); } async isLoaded() { diff --git a/src/pages/putaway/putawayDetails/components/ItemStatusTable.ts b/src/pages/putaway/putawayDetails/components/ItemStatusTable.ts index 28d0a18..6426087 100644 --- a/src/pages/putaway/putawayDetails/components/ItemStatusTable.ts +++ b/src/pages/putaway/putawayDetails/components/ItemStatusTable.ts @@ -26,6 +26,10 @@ class ItemStatusTable extends BasePageModel { get orderItemRows() { return this.table.locator('tr.order-item'); } + + orderItemRow(index: number) { + return new Row(this.page, this.orderItemRows.nth(index)); + } } class Row extends BasePageModel { @@ -39,6 +43,26 @@ class Row extends BasePageModel { return this.row.getByTestId('order-item-status-code'); } + get code() { + return this.row.getByTestId('product-code'); + } + + get productName() { + return this.row.getByTestId('product-name'); + } + + get quantity() { + return this.row.getByTestId('quantity'); + } + + get lotNumber() { + return this.row.getByTestId('lot-number'); + } + + get expirationDate() { + return this.row.getByTestId('expiration-date'); + } + get originBin() { return this.row.getByTestId('origin-bin-location'); } From 5bec69ecd3fd464110e4b61c0d37f4cf5406b1bb Mon Sep 17 00:00:00 2001 From: Alan Nadolny Date: Wed, 27 May 2026 12:01:02 +0200 Subject: [PATCH 05/11] OBPIH-7884 Enhance PDF download tests to assert table data presence --- .../putaway/assertPutawayDetailsPage.test.ts | 107 +++++++++++++++--- 1 file changed, 93 insertions(+), 14 deletions(-) diff --git a/src/tests/putaway/assertPutawayDetailsPage.test.ts b/src/tests/putaway/assertPutawayDetailsPage.test.ts index 0c1c015..e8ff071 100644 --- a/src/tests/putaway/assertPutawayDetailsPage.test.ts +++ b/src/tests/putaway/assertPutawayDetailsPage.test.ts @@ -1,17 +1,24 @@ +import path from 'node:path'; + import AppConfig from '@/config/AppConfig'; import { ShipmentType } from '@/constants/ShipmentType'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { StockMovementResponse } from '@/types'; +import { deleteFile, writeBufferToFile } from '@/utils/FileIOUtils'; +import { pdfContainsValues } from '@/utils/pdfUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteReceivedShipment, getShipmentId, getShipmentItemId, } from '@/utils/shipmentUtils'; +import { captureRowValues } from '@/utils/tableUtils'; test.describe('Assert putaway details page', () => { let STOCK_MOVEMENT: StockMovementResponse; + const downloadedFilePaths: string[] = []; + let expectedPdfValues: string[] = []; test.beforeEach( async ({ @@ -79,6 +86,10 @@ test.describe('Assert putaway details page', () => { stockMovementService, STOCK_MOVEMENT, }); + + while (downloadedFilePaths.length) { + deleteFile(downloadedFilePaths.pop() as string); + } } ); @@ -257,15 +268,40 @@ test.describe('Assert putaway details page', () => { const putawayOrderIdentifier = await putawayDetailsPage.orderHeaderTable.orderNumberValue.textContent(); - await test.step('Download putaway pdf', async () => { - const generatePutawayPdfFileName = - 'Putaway ' + `${putawayOrderIdentifier}`.toString().trim() + '.pdf'; + const detailsPagePdfFileName = + 'Putaway ' + `${putawayOrderIdentifier}`.toString().trim() + '.pdf'; + + await test.step('Download putaway pdf from details page', async () => { + await putawayDetailsPage.fileHandler.onDownload(); await putawayDetailsPage.generatePutawayListButton.click(); - const downloadPromise = page.waitForEvent('download'); - const download = await downloadPromise; - await expect(download.suggestedFilename()).toBe( - generatePutawayPdfFileName + const { fileName, fullFilePath } = + await putawayDetailsPage.fileHandler.saveFile(); + expect(fileName).toBe(detailsPagePdfFileName); + downloadedFilePaths.push(fullFilePath); + }); + + await test.step('Assert details page pdf contains table data', async () => { + const rowCount = + await putawayDetailsPage.itemStatusTable.orderItemRows.count(); + expect(rowCount).toBeGreaterThan(0); + + expectedPdfValues = await captureRowValues( + rowCount, + (i) => putawayDetailsPage.itemStatusTable.orderItemRow(i), + (r) => r.code, + (r) => r.productName, + (r) => r.quantity, + (r) => r.lotNumber, + (r) => r.expirationDate, + (r) => r.destinationBin ); + + expect( + await pdfContainsValues( + downloadedFilePaths[downloadedFilePaths.length - 1], + expectedPdfValues + ) + ).toBeTruthy(); }); await test.step('Edit pending putaway and reload without losing data', async () => { @@ -275,6 +311,40 @@ test.describe('Assert putaway details page', () => { await createPutawayPage.startStep.isLoaded(); }); + const createPagePdfFileNameRegex = new RegExp( + `^PutawayReport-${`${putawayOrderIdentifier}`.toString().trim()}\\.pdf$` + ); + + await test.step('Download putaway list pdf from create putaway page', async () => { + const pdfResponsePromise = page.waitForResponse( + (resp) => + resp.url().includes('/putAway/generatePdf/') && + resp.status() === 200 + ); + const downloadPromise = page.waitForEvent('download'); + await createPutawayPage.startStep.generatePutawayListButton.click(); + const [pdfResponse, download] = await Promise.all([ + pdfResponsePromise, + downloadPromise, + ]); + + const fileName = download.suggestedFilename(); + expect(fileName).toMatch(createPagePdfFileNameRegex); + + const fullFilePath = path.join(AppConfig.LOCAL_FILES_DIR_PATH, fileName); + writeBufferToFile(fullFilePath, await pdfResponse.body()); + downloadedFilePaths.push(fullFilePath); + }); + + await test.step('Assert create putaway page pdf contains table data', async () => { + expect( + await pdfContainsValues( + downloadedFilePaths[downloadedFilePaths.length - 1], + expectedPdfValues + ) + ).toBeTruthy(); + }); + await test.step('Complete putaway', async () => { await createPutawayPage.startStep.nextButton.click(); await createPutawayPage.completeStep.isLoaded(); @@ -305,17 +375,26 @@ test.describe('Assert putaway details page', () => { }); await test.step('Download putaway pdf from putaway list', async () => { + const generatePutawayPdfFileName = + 'Putaway ' + `${putawayOrderIdentifier}`.toString().trim() + '.pdf'; const row = putawayListPage.table.row(1); await row.actionsButton.click(); await row.generatePdf.click(); - const generatePutawayPdfFileName = - 'Putaway ' + `${putawayOrderIdentifier}`.toString().trim() + '.pdf'; + await putawayDetailsPage.fileHandler.onDownload(); await putawayDetailsPage.generatePutawayListButton.click(); - const downloadPromise = page.waitForEvent('download'); - const download = await downloadPromise; - await expect(download.suggestedFilename()).toBe( - generatePutawayPdfFileName - ); + const { fileName, fullFilePath } = + await putawayDetailsPage.fileHandler.saveFile(); + expect(fileName).toBe(generatePutawayPdfFileName); + downloadedFilePaths.push(fullFilePath); + }); + + await test.step('Assert putaway list pdf contains table data', async () => { + expect( + await pdfContainsValues( + downloadedFilePaths[downloadedFilePaths.length - 1], + expectedPdfValues + ) + ).toBeTruthy(); }); await test.step('Filter by completed status and ordered by on putaway list page', async () => { From e2a7befd7fe766fea15ed9c5643d7e7efeb5b72a Mon Sep 17 00:00:00 2001 From: Alan Nadolny Date: Wed, 27 May 2026 13:42:38 +0200 Subject: [PATCH 06/11] OBPIH-7884 Fix ts and linter errors --- .eslintignore | 6 ++++++ src/pages/inbound/create/components/AddItemsTable.ts | 7 +++---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .eslintignore diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..9142fa7 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +node_modules/ +playwright-report/ +test-results/ +blob-report/ +playwright/.cache/ +localFiles/ diff --git a/src/pages/inbound/create/components/AddItemsTable.ts b/src/pages/inbound/create/components/AddItemsTable.ts index 94429bd..e49c8f5 100644 --- a/src/pages/inbound/create/components/AddItemsTable.ts +++ b/src/pages/inbound/create/components/AddItemsTable.ts @@ -72,10 +72,9 @@ class Row extends BasePageModel { if (!_.isNil(rowValues.lotNumber)) { await this.lotField.textbox.fill(rowValues.lotNumber); } - if (!_.isNil(rowValues.recipient?.name)) { - await this.recipientSelect.findAndSelectOption( - rowValues.recipient.name - ); + const recipientName = rowValues.recipient?.name; + if (!_.isNil(recipientName)) { + await this.recipientSelect.findAndSelectOption(recipientName); } if (!_.isNil(rowValues.expirationDate)) { await this.expirationDate.fill(rowValues.expirationDate); From 36944fae9d09b724c7d12a96a6426038d72e7a98 Mon Sep 17 00:00:00 2001 From: Alan Nadolny Date: Fri, 29 May 2026 11:25:19 +0200 Subject: [PATCH 07/11] OBPIH-7884 Add utility function to check if page contains specified values --- src/utils/pageUtils.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/utils/pageUtils.ts diff --git a/src/utils/pageUtils.ts b/src/utils/pageUtils.ts new file mode 100644 index 0000000..19b8432 --- /dev/null +++ b/src/utils/pageUtils.ts @@ -0,0 +1,10 @@ +import { Page } from '@playwright/test'; + +export const pageContainsValues = async ( + page: Page, + values: string[] +): Promise => { + await page.waitForLoadState(); + const text = await page.locator('body').innerText(); + return values.every((value) => text.includes(value)); +}; \ No newline at end of file From 3bcca548422581c90344539a352363e5ae230b78 Mon Sep 17 00:00:00 2001 From: Alan Nadolny Date: Fri, 29 May 2026 11:26:06 +0200 Subject: [PATCH 08/11] OBPIH-7884 Add methods to filter and retrieve item rows in CheckTable --- src/pages/receiving/components/CheckTable.ts | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/pages/receiving/components/CheckTable.ts b/src/pages/receiving/components/CheckTable.ts index c0207a7..e18af77 100644 --- a/src/pages/receiving/components/CheckTable.ts +++ b/src/pages/receiving/components/CheckTable.ts @@ -19,6 +19,21 @@ class CheckTable extends BasePageModel { return new Row(this.page, this.rows.nth(index)); } + // rows mixes container rows with item rows (e.g. the first item is at + // row(1), and multi-container shipments leave gaps), so it can't be indexed + // by item position. + get itemRows() { + return this.rows.filter({ + has: this.page + .getByTestId('label-field') + .and(this.page.getByLabel('Code', { exact: true })), + }); + } + + itemRow(index: number) { + return new Row(this.page, this.itemRows.nth(index)); + } + getColumnHeader(columnName: string) { return this.table .locator('.table-header') @@ -45,6 +60,12 @@ class Row extends BasePageModel { return this.row.getByTestId('label-field').getByText(name); } + get code() { + return this.row + .getByTestId('label-field') + .and(this.row.getByLabel('Code', { exact: true })); + } + get cancelRemainingCheckbox() { return this.row.getByTestId('form-field').getByTestId('checkbox'); } From 8e0b4dee67e6bcbd303d61c0908575833f02d4f5 Mon Sep 17 00:00:00 2001 From: Alan Nadolny Date: Fri, 29 May 2026 11:27:16 +0200 Subject: [PATCH 09/11] OBPIH-7884 Add assertions to verify Goods Receipt Note values in tests --- .../assertCreationOfGoodsReceiptNote.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/tests/receiving/assertCreationOfGoodsReceiptNote.test.ts b/src/tests/receiving/assertCreationOfGoodsReceiptNote.test.ts index b6b685a..ccf4392 100644 --- a/src/tests/receiving/assertCreationOfGoodsReceiptNote.test.ts +++ b/src/tests/receiving/assertCreationOfGoodsReceiptNote.test.ts @@ -4,6 +4,8 @@ import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { StockMovementResponse } from '@/types'; import BinLocationUtils from '@/utils/BinLocationUtils'; +import { pageContainsValues } from '@/utils/pageUtils'; +import { captureRowValues } from '@/utils/tableUtils'; test.describe('Assert Goods Receipt Note is created and opened', () => { let STOCK_MOVEMENT: StockMovementResponse; @@ -66,6 +68,8 @@ test.describe('Assert Goods Receipt Note is created and opened', () => { receivingPage, page, }) => { + let expectedValues: string[] = []; + await test.step('Go to stock movement show page', async () => { await stockMovementShowPage.goToPage(STOCK_MOVEMENT.id); await stockMovementShowPage.isLoaded(); @@ -99,6 +103,13 @@ test.describe('Assert Goods Receipt Note is created and opened', () => { await receivingPage.nextButton.click(); await receivingPage.checkStep.isLoaded(); await receivingPage.checkStep.isLoaded(); + const rowCount = await receivingPage.checkStep.table.itemRows.count(); + expect(rowCount).toBeGreaterThan(0); + expectedValues = await captureRowValues( + rowCount, + (i) => receivingPage.checkStep.table.itemRow(i), + (r) => r.code + ); await receivingPage.checkStep.receiveShipmentButton.click(); await stockMovementShowPage.isLoaded(); }); @@ -116,6 +127,7 @@ test.describe('Assert Goods Receipt Note is created and opened', () => { .downloadButton.click(); const popup = await popupPromise; await expect(popup.locator('.title')).toHaveText('Goods Receipt Note'); + expect(await pageContainsValues(popup, expectedValues)).toBeTruthy(); await popup.close(); }); @@ -152,6 +164,7 @@ test.describe('Assert Goods Receipt Note is created and opened', () => { .downloadButton.click(); const popup = await popupPromise; await expect(popup.locator('.title')).toHaveText('Goods Receipt Note'); + expect(await pageContainsValues(popup, expectedValues)).toBeTruthy(); await popup.close(); }); From 3824676f5a73b28521c6a280af5dc87c58a0c586 Mon Sep 17 00:00:00 2001 From: Alan Nadolny Date: Fri, 29 May 2026 11:36:45 +0200 Subject: [PATCH 10/11] OBPIH-7884 Add empty line --- src/utils/pageUtils.ts | 2 +- src/utils/tableUtils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/pageUtils.ts b/src/utils/pageUtils.ts index 19b8432..6d08553 100644 --- a/src/utils/pageUtils.ts +++ b/src/utils/pageUtils.ts @@ -7,4 +7,4 @@ export const pageContainsValues = async ( await page.waitForLoadState(); const text = await page.locator('body').innerText(); return values.every((value) => text.includes(value)); -}; \ No newline at end of file +}; diff --git a/src/utils/tableUtils.ts b/src/utils/tableUtils.ts index 39889f9..c6f0c0b 100644 --- a/src/utils/tableUtils.ts +++ b/src/utils/tableUtils.ts @@ -15,4 +15,4 @@ export const captureRowValues = async ( .flat() .map((v) => v?.trim()) .filter((v): v is string => Boolean(v)); -}; \ No newline at end of file +}; From aeb3e5d82b0f29f37752624b190caca20560710f Mon Sep 17 00:00:00 2001 From: Alan Nadolny Date: Fri, 29 May 2026 11:45:47 +0200 Subject: [PATCH 11/11] OBPIH-7884 Replace hardcoded url --- src/consts/applicationUrls.ts | 1 + src/tests/putaway/assertPutawayDetailsPage.test.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/consts/applicationUrls.ts b/src/consts/applicationUrls.ts index ee9c01b..24ad458 100644 --- a/src/consts/applicationUrls.ts +++ b/src/consts/applicationUrls.ts @@ -71,6 +71,7 @@ const USER_URL = { const PUTAWAY_URL = { base: './putAway', create: () => `${PUTAWAY_URL.base}/create`, + generatePdfPattern: /\/putAway\/generatePdf\//, }; const ORDER_URL = { diff --git a/src/tests/putaway/assertPutawayDetailsPage.test.ts b/src/tests/putaway/assertPutawayDetailsPage.test.ts index e8ff071..ff4d61e 100644 --- a/src/tests/putaway/assertPutawayDetailsPage.test.ts +++ b/src/tests/putaway/assertPutawayDetailsPage.test.ts @@ -2,6 +2,7 @@ import path from 'node:path'; import AppConfig from '@/config/AppConfig'; import { ShipmentType } from '@/constants/ShipmentType'; +import { PUTAWAY_URL } from '@/consts/applicationUrls'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { StockMovementResponse } from '@/types'; @@ -318,7 +319,7 @@ test.describe('Assert putaway details page', () => { await test.step('Download putaway list pdf from create putaway page', async () => { const pdfResponsePromise = page.waitForResponse( (resp) => - resp.url().includes('/putAway/generatePdf/') && + PUTAWAY_URL.generatePdfPattern.test(resp.url()) && resp.status() === 200 ); const downloadPromise = page.waitForEvent('download');