diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index c236bf9..6887076 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -30,7 +30,7 @@ concurrency: jobs: test: - timeout-minutes: 60 + timeout-minutes: 90 runs-on: ubuntu-latest permissions: id-token: write diff --git a/src/api/PutawayService.ts b/src/api/PutawayService.ts index 4e617a1..30161cb 100644 --- a/src/api/PutawayService.ts +++ b/src/api/PutawayService.ts @@ -1,8 +1,12 @@ import { APIRequestContext } from '@playwright/test'; import BaseServiceModel from '@/api/BaseServiceModel'; -import { PUTAWAY_API } from '@/constants/apiUrls'; -import { ApiResponse, PutawayCandidate } from '@/types'; +import { + PUTAWAY_API, + PUTAWAY_BY_ID, + STOCK_TRANSFER_BY_ID, +} from '@/constants/apiUrls'; +import { ApiResponse, PutawayCandidate, PutawayResponse } from '@/types'; import { parseRequestToJSON } from '@/utils/ServiceUtils'; class PutawayService extends BaseServiceModel { @@ -24,6 +28,35 @@ class PutawayService extends BaseServiceModel { ); } } + + /** + Fetches a putaway order by id. Returns null when the order does not exist. + */ + async getPutaway( + orderId: string + ): Promise | null> { + const apiResponse = await this.request.get(PUTAWAY_BY_ID(orderId)); + if (!apiResponse.ok()) { + return null; + } + return await parseRequestToJSON(apiResponse); + } + + /** + Putaway orders are transfer orders under the hood, so they are deleted + through the stock transfer API. The server only allows deleting orders + that have not been completed yet. + */ + async deletePutawayOrder(orderId: string) { + const apiResponse = await this.request.delete( + STOCK_TRANSFER_BY_ID(orderId) + ); + if (!apiResponse.ok()) { + throw new Error( + `Problem deleting putaway order ${orderId}: ${apiResponse.status()}` + ); + } + } } export default PutawayService; diff --git a/src/api/StockMovementService.ts b/src/api/StockMovementService.ts index b37453e..7451c62 100644 --- a/src/api/StockMovementService.ts +++ b/src/api/StockMovementService.ts @@ -68,8 +68,6 @@ class StockMovementService extends BaseServiceModel { } async deleteStockMovement(id: string) { - // request.delete does not throw on HTTP error statuses, and a swallowed - // failed delete leaves the stock movement behind for the next tests const apiResponse = await this.request.delete(STOCK_MOVEMENT_BY_ID(id)); if (!apiResponse.ok()) { throw new Error( diff --git a/src/constants/apiUrls.ts b/src/constants/apiUrls.ts index b661ac7..0b0d6da 100644 --- a/src/constants/apiUrls.ts +++ b/src/constants/apiUrls.ts @@ -40,6 +40,12 @@ export const STOCK_MOVEMENT_ITEMS = (id: string) => // PUTAWAY export const PUTAWAY_API = `${API}/putaways`; +export const PUTAWAY_BY_ID = (id: string) => `${PUTAWAY_API}/${id}`; + +// STOCK TRANSFER +export const STOCK_TRANSFER_API = `${API}/stockTransfers`; +export const STOCK_TRANSFER_BY_ID = (id: string) => + `${STOCK_TRANSFER_API}/${id}`; // PARTIAL RECEIVING export const PARTIAL_RECEIVING_BY_ID = (id: string) => diff --git a/src/pages/putaway/CreatePutawayPage.ts b/src/pages/putaway/CreatePutawayPage.ts index dc193b1..d8d3b38 100644 --- a/src/pages/putaway/CreatePutawayPage.ts +++ b/src/pages/putaway/CreatePutawayPage.ts @@ -39,6 +39,21 @@ class CreatePutawayPage extends BasePageModel { return this.page.getByTestId('start-putaway').nth(0); } + /** + Clicks "Start Putaway" and returns the id of the pending putaway order + created by the click, captured from the create API response. + */ + async startPutaway(): Promise { + const createResponsePromise = this.page.waitForResponse( + (response) => + response.url().includes('/api/putaways') && + response.request().method() === 'POST' + ); + await this.startPutawayButton.click(); + const createResponse = await createResponsePromise; + return (await createResponse.json()).data.id; + } + get showByStockMovementFilter() { return this.page.getByTestId('show-by-button'); } diff --git a/src/pages/putaway/components/StartPutawayTable.ts b/src/pages/putaway/components/StartPutawayTable.ts index 70acacf..42998f5 100644 --- a/src/pages/putaway/components/StartPutawayTable.ts +++ b/src/pages/putaway/components/StartPutawayTable.ts @@ -92,7 +92,7 @@ class Row extends BasePageModel { return this.row.getByTestId('table-cell').nth(8); } - get currentdBin() { + get currentBin() { return this.row.getByTestId('table-cell').nth(9); } diff --git a/src/pages/stockMovementShow/StockMovementShowPage.ts b/src/pages/stockMovementShow/StockMovementShowPage.ts index a624464..2b8a60b 100644 --- a/src/pages/stockMovementShow/StockMovementShowPage.ts +++ b/src/pages/stockMovementShow/StockMovementShowPage.ts @@ -1,4 +1,4 @@ -import { expect, Page } from '@playwright/test'; +import { expect, Locator, Page } from '@playwright/test'; import { STOCK_MOVEMENT_URL } from '@/constants/applicationUrls'; import BasePageModel from '@/pages/BasePageModel'; @@ -112,25 +112,29 @@ class StockMovementShowPage extends BasePageModel { await this.deleteButton.click(); } - async openReceiptsTab() { - // tab content is fetched once per page load, so when it fails to render, - // re-clicking the tab never refetches it — only a reload does + // tab content is fetched once per page load, so when it fails to render, + // re-clicking the tab never refetches it — only a reload does + private async openTab( + tab: Locator, + tabContent: { isLoaded: () => Promise } + ) { let reloadOnRetry = false; await expect(async () => { if (reloadOnRetry) { await this.page.reload(); } reloadOnRetry = true; - await this.receiptTab.click(); - await this.receiptListTable.isLoaded(); - }).toPass({ timeout: 20000, intervals: [500, 1000, 2000] }); + await tab.click(); + await tabContent.isLoaded(); + }).toPass({ timeout: 45000, intervals: [500, 1000, 2000] }); + } + + async openReceiptsTab() { + await this.openTab(this.receiptTab, this.receiptListTable); } async openDocumentsTab() { - await expect(async () => { - await this.documentTab.click(); - await this.documentsListTable.isLoaded(); - }).toPass({ timeout: 20000, intervals: [500, 1000, 2000] }); + await this.openTab(this.documentTab, this.documentsListTable); } } diff --git a/src/tests/putaway/addCommentToPutaway.test.ts b/src/tests/putaway/addCommentToPutaway.test.ts index 153c5a8..bca3f5d 100644 --- a/src/tests/putaway/addCommentToPutaway.test.ts +++ b/src/tests/putaway/addCommentToPutaway.test.ts @@ -3,6 +3,7 @@ import { ShipmentType } from '@/constants/ShipmentType'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { StockMovementResponse } from '@/types'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -12,6 +13,7 @@ import { test.describe('Add comment to Putaway', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; test.beforeEach( async ({ @@ -20,6 +22,7 @@ test.describe('Add comment to Putaway', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -57,15 +60,26 @@ test.describe('Add comment to Putaway', () => { ); test.afterEach( - async ({ stockMovementService, navbar, transactionListPage }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); + async ( + { stockMovementService, navbar, transactionListPage, putawayService }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); } @@ -104,7 +118,7 @@ test.describe('Add comment to Putaway', () => { await test.step('Start putaway', async () => { await createPutawayPage.table.row(0).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); diff --git a/src/tests/putaway/assertAttemptToEditCompletedPutaway.test.ts b/src/tests/putaway/assertAttemptToEditCompletedPutaway.test.ts index 1a83718..87447b7 100644 --- a/src/tests/putaway/assertAttemptToEditCompletedPutaway.test.ts +++ b/src/tests/putaway/assertAttemptToEditCompletedPutaway.test.ts @@ -3,6 +3,7 @@ import { ShipmentType } from '@/constants/ShipmentType'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { StockMovementResponse } from '@/types'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -12,6 +13,7 @@ import { test.describe('Assert attempt to edit completed putaway', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; test.beforeEach( async ({ @@ -20,6 +22,7 @@ test.describe('Assert attempt to edit completed putaway', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -57,15 +60,26 @@ test.describe('Assert attempt to edit completed putaway', () => { ); test.afterEach( - async ({ stockMovementService, navbar, transactionListPage }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); + async ( + { stockMovementService, navbar, transactionListPage, putawayService }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); } @@ -93,7 +107,7 @@ test.describe('Assert attempt to edit completed putaway', () => { await test.step('Start putaway', async () => { await createPutawayPage.table.row(0).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); diff --git a/src/tests/putaway/assertPutawayDetailsPage.test.ts b/src/tests/putaway/assertPutawayDetailsPage.test.ts index 3ca4a3f..934cc0d 100644 --- a/src/tests/putaway/assertPutawayDetailsPage.test.ts +++ b/src/tests/putaway/assertPutawayDetailsPage.test.ts @@ -8,6 +8,7 @@ import { Product } from '@/generated/ProductCodes.generated'; import { StockMovementResponse } from '@/types'; import { deleteFile, writeBufferToFile } from '@/utils/FileIOUtils'; import { pdfContainsValues } from '@/utils/pdfUtils'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -18,6 +19,7 @@ import { captureRowValues } from '@/utils/tableUtils'; test.describe('Assert putaway details page', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; const downloadedFilePaths: string[] = []; let expectedPdfValues: string[] = []; @@ -28,6 +30,7 @@ test.describe('Assert putaway details page', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -65,15 +68,26 @@ test.describe('Assert putaway details page', () => { ); test.afterEach( - async ({ stockMovementService, navbar, transactionListPage }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); + async ( + { stockMovementService, navbar, transactionListPage, putawayService }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); @@ -117,7 +131,7 @@ test.describe('Assert putaway details page', () => { await test.step('Start putaway', async () => { await createPutawayPage.table.row(0).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); diff --git a/src/tests/putaway/assertReceivingBinsOnCreatePutawayPage.test.ts b/src/tests/putaway/assertReceivingBinsOnCreatePutawayPage.test.ts index 5e17f64..3a79b1c 100644 --- a/src/tests/putaway/assertReceivingBinsOnCreatePutawayPage.test.ts +++ b/src/tests/putaway/assertReceivingBinsOnCreatePutawayPage.test.ts @@ -4,6 +4,7 @@ import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { StockMovementResponse } from '@/types'; import { formatDate, getDateByOffset } from '@/utils/DateUtils'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -17,6 +18,7 @@ test.describe('Assert receiving bin on create putaway page', () => { let SECONDARY_STOCK_MOVEMENT: StockMovementResponse; const uniqueIdentifier = new UniqueIdentifier(); const lot = uniqueIdentifier.generateUniqueString('lot'); + let PUTAWAY_ORDER_IDS: string[] = []; test.beforeEach( async ({ @@ -25,6 +27,7 @@ test.describe('Assert receiving bin on create putaway page', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); await test.step('Create 1st stock movement', async () => { @@ -123,11 +126,22 @@ test.describe('Assert receiving bin on create putaway page', () => { ); test.afterEach( - async ({ stockMovementService, navbar, transactionListPage }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - for (let n = 1; n < 4; n++) { - await transactionListPage.deleteTransaction(1); + async ( + { stockMovementService, navbar, transactionListPage, putawayService }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + for (let n = 1; n < 4; n++) { + await transactionListPage.deleteTransaction(1); + } } await deleteShipment({ @@ -289,7 +303,7 @@ test.describe('Assert receiving bin on create putaway page', () => { await test.step('Start putaway', async () => { await createPutawayPage.table.row(1).checkbox.click(); await createPutawayPage.table.row(2).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); diff --git a/src/tests/putaway/assertZonesInPutaways.test.ts b/src/tests/putaway/assertZonesInPutaways.test.ts index 00bd78f..b6fa8db 100644 --- a/src/tests/putaway/assertZonesInPutaways.test.ts +++ b/src/tests/putaway/assertZonesInPutaways.test.ts @@ -1,9 +1,14 @@ +import path from 'node:path'; + import AppConfig from '@/config/AppConfig'; -import { LOCATION_URL } from '@/constants/applicationUrls'; +import { LOCATION_URL, PUTAWAY_URL } from '@/constants/applicationUrls'; import { ShipmentType } from '@/constants/ShipmentType'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { ProductResponse, StockMovementResponse } from '@/types'; +import { deleteFile, writeBufferToFile } from '@/utils/FileIOUtils'; +import { extractPdfColumnValues } from '@/utils/pdfUtils'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -19,6 +24,8 @@ test.describe('Assert zones on putaway pages', () => { let productB: ProductResponse; const uniqueIdentifier = new UniqueIdentifier(); const zoneLocationName = uniqueIdentifier.generateUniqueString('zone'); + const downloadedFilePaths: string[] = []; + let PUTAWAY_ORDER_IDS: string[] = []; test.beforeEach( async ({ @@ -34,6 +41,7 @@ test.describe('Assert zones on putaway pages', () => { locationListPage, createLocationPage, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); const mainLocation = await mainLocationService.getLocation(); const internalLocation = await internalLocationService.getLocation(); @@ -140,23 +148,35 @@ test.describe('Assert zones on putaway pages', () => { ); test.afterEach( - async ({ - stockMovementService, - navbar, - transactionListPage, - productService, - productShowPage, - productEditPage, - page, - locationListPage, - mainLocationService, - createLocationPage, - internalLocationService, - }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - for (let n = 1; n < 4; n++) { - await transactionListPage.deleteTransaction(1); + async ( + { + stockMovementService, + navbar, + transactionListPage, + productService, + productShowPage, + productEditPage, + page, + locationListPage, + mainLocationService, + createLocationPage, + internalLocationService, + putawayService, + }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + for (let n = 1; n < 4; n++) { + await transactionListPage.deleteTransaction(1); + } } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); const product = await productService.getProduct(Product.FOUR); @@ -217,6 +237,10 @@ test.describe('Assert zones on putaway pages', () => { await createLocationPage.locationConfigurationTabSection.activeCheckbox.uncheck(); await createLocationPage.locationConfigurationTabSection.saveButton.click(); }); + + while (downloadedFilePaths.length) { + deleteFile(downloadedFilePaths.pop() as string); + } } ); @@ -226,6 +250,7 @@ test.describe('Assert zones on putaway pages', () => { createPutawayPage, internalLocationService, putawayDetailsPage, + page, }) => { const receivingBin = AppConfig.instance.receivingBinPrefix + STOCK_MOVEMENT.identifier; @@ -248,7 +273,7 @@ test.describe('Assert zones on putaway pages', () => { .getExpandBinLocation(receivingBin) .click(); await createPutawayPage.table.row(1).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); await createPutawayPage.startStep.table.row(0).editButton.click(); await createPutawayPage.startStep.table.row(0).quantityInput.fill('5'); @@ -269,10 +294,35 @@ test.describe('Assert zones on putaway pages', () => { .row(1) .getPutawayBin(internalLocation.name) .click(); - await createPutawayPage.startStep.nextButton.click(); + }); + + await test.step('Generate putaway pdf and assert putaway bin column', async () => { + const pdfResponsePromise = page.waitForResponse( + (resp) => + PUTAWAY_URL.generatePdfPattern.test(resp.url()) && + resp.status() === 200 + ); + const downloadPromise = page.waitForEvent('download'); + await createPutawayPage.startStep.generatePutawayListButton.click(); + const [pdfResponse, download] = await Promise.all([ + pdfResponsePromise, + downloadPromise, + ]); + + const pdfFilePath = path.join( + AppConfig.LOCAL_FILES_DIR_PATH, + download.suggestedFilename() + ); + writeBufferToFile(pdfFilePath, await pdfResponse.body()); + downloadedFilePaths.push(pdfFilePath); + + expect(await extractPdfColumnValues(pdfFilePath, 'Putaway Bin')).toEqual([ + `${zoneLocationName}: ${internalLocation.name}`, + ]); }); await test.step('Assert zones on confirm page', async () => { + await createPutawayPage.startStep.nextButton.click(); await createPutawayPage.completeStep.isLoaded(); await expect( createPutawayPage.completeStep.table.row(2).putawayBin @@ -307,13 +357,13 @@ test.describe('Assert zones on putaway pages', () => { .click(); await createPutawayPage.table.row(1).checkbox.click(); await createPutawayPage.table.row(2).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); await test.step('Assert zones on start putaway page', async () => { await expect( - createPutawayPage.startStep.table.row(1).currentdBin + createPutawayPage.startStep.table.row(1).currentBin ).toContainText(`${zoneLocationName}: ${internalLocation.name}`); await createPutawayPage.startStep.table .row(1) @@ -351,39 +401,135 @@ test.describe('Assert zones on putaway pages', () => { .click(); }); + await test.step('Generate putaway pdf and assert bin columns', async () => { + const pdfResponsePromise = page.waitForResponse( + (resp) => + PUTAWAY_URL.generatePdfPattern.test(resp.url()) && + resp.status() === 200 + ); + const downloadPromise = page.waitForEvent('download'); + await createPutawayPage.startStep.generatePutawayListButton.click(); + const [pdfResponse, download] = await Promise.all([ + pdfResponsePromise, + downloadPromise, + ]); + + const pdfFilePath = path.join( + AppConfig.LOCAL_FILES_DIR_PATH, + download.suggestedFilename() + ); + writeBufferToFile(pdfFilePath, await pdfResponse.body()); + downloadedFilePaths.push(pdfFilePath); + + expect( + await extractPdfColumnValues(pdfFilePath, 'Preferred Bins') + ).toEqual(['', `${zoneLocationName}: ${internalLocation.name}`]); + expect(await extractPdfColumnValues(pdfFilePath, 'Current Bins')).toEqual( + [`${zoneLocationName}: ${internalLocation.name}`, 'null'] + ); + expect(await extractPdfColumnValues(pdfFilePath, 'Putaway Bin')).toEqual([ + `${zoneLocationName}: ${internalLocation.name}`, + `${zoneLocationName}: ${internalLocation.name}`, + ]); + }); + await test.step('Apply ordering by current bin', async () => { await createPutawayPage.startStep.sortButton.click(); await expect(createPutawayPage.startStep.sortButton).toContainText( 'Sort by current bins' ); await expect( - createPutawayPage.startStep.table.row(1).currentdBin + createPutawayPage.startStep.table.row(1).currentBin ).toContainText(`${zoneLocationName}: ${internalLocation.name}`); await expect( createPutawayPage.startStep.table.row(2).preferredBin ).toContainText(`${zoneLocationName}: ${internalLocation.name}`); }); + await test.step('Generate putaway pdf and assert bin columns after sorting by current bin', async () => { + const pdfResponsePromise = page.waitForResponse( + (resp) => + PUTAWAY_URL.generatePdfPattern.test(resp.url()) && + resp.status() === 200 + ); + const downloadPromise = page.waitForEvent('download'); + await createPutawayPage.startStep.generatePutawayListButton.click(); + const [pdfResponse, download] = await Promise.all([ + pdfResponsePromise, + downloadPromise, + ]); + + const pdfFilePath = path.join( + AppConfig.LOCAL_FILES_DIR_PATH, + download.suggestedFilename() + ); + writeBufferToFile(pdfFilePath, await pdfResponse.body()); + downloadedFilePaths.push(pdfFilePath); + + expect( + await extractPdfColumnValues(pdfFilePath, 'Preferred Bins') + ).toEqual(['', `${zoneLocationName}: ${internalLocation.name}`]); + expect(await extractPdfColumnValues(pdfFilePath, 'Current Bins')).toEqual( + [`${zoneLocationName}: ${internalLocation.name}`, 'null'] + ); + expect(await extractPdfColumnValues(pdfFilePath, 'Putaway Bin')).toEqual([ + `${zoneLocationName}: ${internalLocation.name}`, + `${zoneLocationName}: ${internalLocation.name}`, + ]); + }); + await test.step('Apply ordering by preferred bin', async () => { await createPutawayPage.startStep.sortButton.click(); await expect(createPutawayPage.startStep.sortButton).toContainText( 'Sort by preferred bin' ); await expect( - createPutawayPage.startStep.table.row(2).currentdBin + createPutawayPage.startStep.table.row(2).currentBin ).toContainText(`${zoneLocationName}: ${internalLocation.name}`); await expect( createPutawayPage.startStep.table.row(1).preferredBin ).toContainText(`${zoneLocationName}: ${internalLocation.name}`); }); + await test.step('Generate putaway pdf and assert bin columns after sorting by preferred bin', async () => { + const pdfResponsePromise = page.waitForResponse( + (resp) => + PUTAWAY_URL.generatePdfPattern.test(resp.url()) && + resp.status() === 200 + ); + const downloadPromise = page.waitForEvent('download'); + await createPutawayPage.startStep.generatePutawayListButton.click(); + const [pdfResponse, download] = await Promise.all([ + pdfResponsePromise, + downloadPromise, + ]); + + const pdfFilePath = path.join( + AppConfig.LOCAL_FILES_DIR_PATH, + download.suggestedFilename() + ); + writeBufferToFile(pdfFilePath, await pdfResponse.body()); + downloadedFilePaths.push(pdfFilePath); + + expect( + await extractPdfColumnValues(pdfFilePath, 'Preferred Bins') + ).toEqual([`${zoneLocationName}: ${internalLocation.name}`, '']); + expect(await extractPdfColumnValues(pdfFilePath, 'Current Bins')).toEqual( + ['null', `${zoneLocationName}: ${internalLocation.name}`] + ); + expect(await extractPdfColumnValues(pdfFilePath, 'Putaway Bin')).toEqual([ + `${zoneLocationName}: ${internalLocation.name}`, + `${zoneLocationName}: ${internalLocation.name}`, + ]); + }); + await test.step('Return to original order', async () => { await createPutawayPage.startStep.sortButton.click(); await expect(createPutawayPage.startStep.sortButton).toContainText( 'Sort by current bins' ); await expect( - createPutawayPage.startStep.table.row(1).currentdBin + createPutawayPage.startStep.table.row(1).currentBin ).toContainText(`${zoneLocationName}: ${internalLocation.name}`); await expect( createPutawayPage.startStep.table.row(2).preferredBin diff --git a/src/tests/putaway/changeLocationOnCreatePutawayPage.test.ts b/src/tests/putaway/changeLocationOnCreatePutawayPage.test.ts index 0f8dfcb..599595b 100644 --- a/src/tests/putaway/changeLocationOnCreatePutawayPage.test.ts +++ b/src/tests/putaway/changeLocationOnCreatePutawayPage.test.ts @@ -3,6 +3,7 @@ import { ShipmentType } from '@/constants/ShipmentType'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { StockMovementResponse } from '@/types'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -12,7 +13,7 @@ import { test.describe('Change location on putaway create page and list pages', () => { let STOCK_MOVEMENT: StockMovementResponse; - let putawayOrderIdentifier: string | undefined; + let PUTAWAY_ORDER_IDS: string[] = []; test.beforeEach( async ({ @@ -21,7 +22,7 @@ test.describe('Change location on putaway create page and list pages', () => { productService, receivingService, }) => { - putawayOrderIdentifier = undefined; + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -58,17 +59,12 @@ test.describe('Change location on putaway create page and list pages', () => { } ); - test.afterEach(async ({ stockMovementService, putawayListPage }) => { - // the received shipment must be cleaned up even when the test fails - // before the putaway is created - if (putawayOrderIdentifier) { - await putawayListPage.goToPage(); - await putawayListPage.table - .rowByOrderNumber(`${putawayOrderIdentifier}`.toString().trim()) - .actionsButton.click(); - await putawayListPage.table.clickDeleteOrderButton(1); - await putawayListPage.emptyPutawayList.isVisible(); - } + test.afterEach(async ({ stockMovementService, putawayService }, testInfo) => { + await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); }); @@ -144,13 +140,12 @@ test.describe('Change location on putaway create page and list pages', () => { .getExpandBinLocation(receivingBin) .click(); await createPutawayPage.table.row(1).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); - putawayOrderIdentifier = - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (await createPutawayPage.startStep.orderNumberValue.textContent())!; + const putawayOrderIdentifier = + await createPutawayPage.startStep.orderNumberValue.textContent(); const putawayOrderIdentifierContent = `${putawayOrderIdentifier}` .toString() diff --git a/src/tests/putaway/createMoreThan1PutawayForTheSameItem.test.ts b/src/tests/putaway/createMoreThan1PutawayForTheSameItem.test.ts index 31d3dca..d56177c 100644 --- a/src/tests/putaway/createMoreThan1PutawayForTheSameItem.test.ts +++ b/src/tests/putaway/createMoreThan1PutawayForTheSameItem.test.ts @@ -1,8 +1,14 @@ +import path from 'node:path'; + import AppConfig from '@/config/AppConfig'; +import { PUTAWAY_URL } from '@/constants/applicationUrls'; 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 { extractPdfColumnValues } from '@/utils/pdfUtils'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -12,6 +18,8 @@ import { test.describe('Create more than 1 putaway from the same item', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; + const downloadedFilePaths: string[] = []; test.beforeEach( async ({ @@ -20,6 +28,7 @@ test.describe('Create more than 1 putaway from the same item', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -57,17 +66,32 @@ test.describe('Create more than 1 putaway from the same item', () => { ); test.afterEach( - async ({ stockMovementService, navbar, transactionListPage }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); + async ( + { stockMovementService, navbar, transactionListPage, putawayService }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); + + while (downloadedFilePaths.length) { + deleteFile(downloadedFilePaths.pop() as string); + } } ); @@ -79,6 +103,7 @@ test.describe('Create more than 1 putaway from the same item', () => { productShowPage, putawayDetailsPage, productService, + page, }) => { const product = await productService.getProduct(Product.FIVE); const internalLocation = await internalLocationService.getLocation(); @@ -100,7 +125,7 @@ test.describe('Create more than 1 putaway from the same item', () => { await test.step('Start putaway', async () => { await createPutawayPage.table.row(0).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -182,7 +207,7 @@ test.describe('Create more than 1 putaway from the same item', () => { await test.step('Start putaway', async () => { await createPutawayPage.table.row(1).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -192,10 +217,35 @@ test.describe('Create more than 1 putaway from the same item', () => { .row(0) .getPutawayBin(internalLocation.name) .click(); - await createPutawayPage.startStep.nextButton.click(); + }); + + await test.step('Generate putaway pdf and assert current bins column', async () => { + const pdfResponsePromise = page.waitForResponse( + (resp) => + PUTAWAY_URL.generatePdfPattern.test(resp.url()) && + resp.status() === 200 + ); + const downloadPromise = page.waitForEvent('download'); + await createPutawayPage.startStep.generatePutawayListButton.click(); + const [pdfResponse, download] = await Promise.all([ + pdfResponsePromise, + downloadPromise, + ]); + + const pdfFilePath = path.join( + AppConfig.LOCAL_FILES_DIR_PATH, + download.suggestedFilename() + ); + writeBufferToFile(pdfFilePath, await pdfResponse.body()); + downloadedFilePaths.push(pdfFilePath); + + expect(await extractPdfColumnValues(pdfFilePath, 'Current Bins')).toEqual( + [internalLocation.name] + ); }); await test.step('Complete putaway', async () => { + await createPutawayPage.startStep.nextButton.click(); await createPutawayPage.completeStep.isLoaded(); await createPutawayPage.completeStep.completePutawayButton.click(); }); diff --git a/src/tests/putaway/createPutaway.test.ts b/src/tests/putaway/createPutaway.test.ts index 2534696..51500cb 100644 --- a/src/tests/putaway/createPutaway.test.ts +++ b/src/tests/putaway/createPutaway.test.ts @@ -1,8 +1,14 @@ +import path from 'node:path'; + import AppConfig from '@/config/AppConfig'; +import { PUTAWAY_URL } from '@/constants/applicationUrls'; 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 { extractPdfColumnValues } from '@/utils/pdfUtils'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -12,6 +18,8 @@ import { test.describe('Putaway received inbound shipment', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; + const downloadedFilePaths: string[] = []; test.beforeEach( async ({ @@ -20,6 +28,7 @@ test.describe('Putaway received inbound shipment', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -57,17 +66,32 @@ test.describe('Putaway received inbound shipment', () => { ); test.afterEach( - async ({ stockMovementService, navbar, transactionListPage }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); + async ( + { stockMovementService, navbar, transactionListPage, putawayService }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); + + while (downloadedFilePaths.length) { + deleteFile(downloadedFilePaths.pop() as string); + } } ); @@ -79,7 +103,10 @@ test.describe('Putaway received inbound shipment', () => { productShowPage, putawayDetailsPage, productService, + page, }) => { + const internalLocation = await internalLocationService.getLocation(); + await test.step('Go to stock movement show page and assert received status', async () => { await stockMovementShowPage.goToPage(STOCK_MOVEMENT.id); await stockMovementShowPage.isLoaded(); @@ -97,21 +124,66 @@ test.describe('Putaway received inbound shipment', () => { await test.step('Start putaway', async () => { await createPutawayPage.table.row(0).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); await test.step('Select bin to putaway', async () => { - const internalLocation = await internalLocationService.getLocation(); await createPutawayPage.startStep.table.row(0).putawayBinSelect.click(); await createPutawayPage.startStep.table .row(0) .getPutawayBin(internalLocation.name) .click(); + }); + + const downloadPutawayListPdf = async () => { + const pdfResponsePromise = page.waitForResponse( + (resp) => + PUTAWAY_URL.generatePdfPattern.test(resp.url()) && + resp.status() === 200 + ); + const downloadPromise = page.waitForEvent('download'); + await createPutawayPage.startStep.generatePutawayListButton.click(); + const [pdfResponse, download] = await Promise.all([ + pdfResponsePromise, + downloadPromise, + ]); + + const fullFilePath = path.join( + AppConfig.LOCAL_FILES_DIR_PATH, + download.suggestedFilename() + ); + writeBufferToFile(fullFilePath, await pdfResponse.body()); + downloadedFilePaths.push(fullFilePath); + return fullFilePath; + }; + + await test.step('Generate putaway pdf', async () => { + const pdfFilePath = await downloadPutawayListPdf(); + expect(await extractPdfColumnValues(pdfFilePath, 'Putaway Bin')).toEqual([ + internalLocation.name, + ]); + }); + + await test.step('Go to next page', async () => { await createPutawayPage.startStep.nextButton.click(); + await createPutawayPage.completeStep.isLoaded(); + }); + + await test.step('Go back to start step', async () => { + await createPutawayPage.completeStep.editButton.click(); + await createPutawayPage.startStep.isLoaded(); + }); + + await test.step('Generate putaway pdf again', async () => { + const pdfFilePath = await downloadPutawayListPdf(); + expect(await extractPdfColumnValues(pdfFilePath, 'Putaway Bin')).toEqual([ + internalLocation.name, + ]); }); await test.step('Go to next page and complete putaway', async () => { + await createPutawayPage.startStep.nextButton.click(); await createPutawayPage.completeStep.isLoaded(); await createPutawayPage.completeStep.completePutawayButton.click(); }); @@ -127,7 +199,6 @@ test.describe('Putaway received inbound shipment', () => { await productShowPage.goToPage(product.id); await productShowPage.inStockTab.click(); await productShowPage.inStockTabSection.isLoaded(); - const internalLocation = await internalLocationService.getLocation(); await expect( productShowPage.inStockTabSection.row(1).binLocation ).toHaveText(internalLocation.name); diff --git a/src/tests/putaway/deleteItemsFromPutaway.test.ts b/src/tests/putaway/deleteItemsFromPutaway.test.ts index 2976134..e511a56 100644 --- a/src/tests/putaway/deleteItemsFromPutaway.test.ts +++ b/src/tests/putaway/deleteItemsFromPutaway.test.ts @@ -3,6 +3,7 @@ import { ShipmentType } from '@/constants/ShipmentType'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { ProductResponse, StockMovementResponse } from '@/types'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -13,6 +14,7 @@ import { byNameAsc } from '@/utils/sortUtils'; test.describe('Delete items from putaway', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; let product: ProductResponse; let product2: ProductResponse; @@ -23,6 +25,7 @@ test.describe('Delete items from putaway', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -71,11 +74,22 @@ test.describe('Delete items from putaway', () => { ); test.afterEach( - async ({ stockMovementService, navbar, transactionListPage }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - for (let n = 1; n < 3; n++) { - await transactionListPage.deleteTransaction(1); + async ( + { stockMovementService, navbar, transactionListPage, putawayService }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + for (let n = 1; n < 3; n++) { + await transactionListPage.deleteTransaction(1); + } } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); } @@ -117,7 +131,7 @@ test.describe('Delete items from putaway', () => { ).toBeVisible(); await createPutawayPage.table.row(1).checkbox.click(); await createPutawayPage.table.row(2).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); diff --git a/src/tests/putaway/deletePendingPutaway.test.ts b/src/tests/putaway/deletePendingPutaway.test.ts index 973b17c..917a4b8 100644 --- a/src/tests/putaway/deletePendingPutaway.test.ts +++ b/src/tests/putaway/deletePendingPutaway.test.ts @@ -8,6 +8,7 @@ import PutawayListPage from '@/pages/putaway/list/PutawayListPage'; import PutawayDetailsPage from '@/pages/putaway/putawayDetails/PutawayDetailsPage'; import StockMovementShowPage from '@/pages/stockMovementShow/StockMovementShowPage'; import { StockMovementResponse } from '@/types'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -17,6 +18,7 @@ import { test.describe('Delete pending putaways', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; test.beforeEach( async ({ @@ -25,6 +27,7 @@ test.describe('Delete pending putaways', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -61,7 +64,13 @@ test.describe('Delete pending putaways', () => { } ); - test.afterEach(async ({ stockMovementService }) => { + test.afterEach(async ({ stockMovementService, putawayService }, testInfo) => { + await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); }); @@ -97,7 +106,7 @@ test.describe('Delete pending putaways', () => { createPutawayPage.table.row(1).getProductName(product.name) ).toBeVisible(); await createPutawayPage.table.row(1).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -160,7 +169,7 @@ test.describe('Delete pending putaways', () => { createPutawayPage.table.row(1).getProductName(product.name) ).toBeVisible(); await createPutawayPage.table.row(1).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -231,7 +240,7 @@ test.describe('Delete pending putaways', () => { createPutawayPage.table.row(1).getProductName(product.name) ).toBeVisible(); await createPutawayPage.table.row(1).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -299,7 +308,7 @@ test.describe('Delete pending putaways', () => { createPutawayPage.table.row(1).getProductName(product.name) ).toBeVisible(); await createPutawayPage.table.row(1).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -373,7 +382,7 @@ test.describe('Delete pending putaways', () => { createPutawayPage.table.row(1).getProductName(product.name) ).toBeVisible(); await createPutawayPage.table.row(1).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -441,7 +450,7 @@ test.describe('Delete pending putaways', () => { createPutawayPage.table.row(1).getProductName(product.name) ).toBeVisible(); await createPutawayPage.table.row(1).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -515,7 +524,7 @@ test.describe('Delete pending putaways', () => { createPutawayPage.table.row(1).getProductName(product.name) ).toBeVisible(); await createPutawayPage.table.row(1).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -583,7 +592,7 @@ test.describe('Delete pending putaways', () => { createPutawayPage.table.row(1).getProductName(product.name) ).toBeVisible(); await createPutawayPage.table.row(1).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); diff --git a/src/tests/putaway/performPutawayAsManagerUser.test.ts b/src/tests/putaway/performPutawayAsManagerUser.test.ts index e01e151..857ee76 100644 --- a/src/tests/putaway/performPutawayAsManagerUser.test.ts +++ b/src/tests/putaway/performPutawayAsManagerUser.test.ts @@ -7,6 +7,7 @@ import CreatePutawayPage from '@/pages/putaway/CreatePutawayPage'; import PutawayDetailsPage from '@/pages/putaway/putawayDetails/PutawayDetailsPage'; import StockMovementShowPage from '@/pages/stockMovementShow/StockMovementShowPage'; import { ProductResponse, StockMovementResponse } from '@/types'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -17,6 +18,7 @@ import { byNameAsc } from '@/utils/sortUtils'; test.describe('Perform putaway as manager user', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; let product: ProductResponse; let product2: ProductResponse; @@ -27,6 +29,7 @@ test.describe('Perform putaway as manager user', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -75,17 +78,29 @@ test.describe('Perform putaway as manager user', () => { ); test.afterEach( - async ({ - stockMovementShowPage, - stockMovementService, - navbar, - transactionListPage, - }) => { + async ( + { + stockMovementShowPage, + stockMovementService, + navbar, + transactionListPage, + putawayService, + }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + await stockMovementShowPage.goToPage(STOCK_MOVEMENT.id); - await navbar.configurationButton.click(); - await navbar.transactions.click(); - await transactionListPage.deleteTransaction(1); - await transactionListPage.deleteTransaction(1); + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + await transactionListPage.deleteTransaction(1); + await transactionListPage.deleteTransaction(1); + } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); } ); @@ -128,7 +143,7 @@ test.describe('Perform putaway as manager user', () => { ).toBeVisible(); await createPutawayPage.table.row(1).checkbox.click(); await createPutawayPage.table.row(2).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -249,7 +264,7 @@ test.describe('Perform putaway as manager user', () => { await test.step('Start putaway', async () => { await createPutawayPageManagerUser.table.row(0).checkbox.click(); - await createPutawayPageManagerUser.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPageManagerUser.startPutaway()); await createPutawayPageManagerUser.startStep.isLoaded(); }); diff --git a/src/tests/putaway/putawayImtemWithEmptyLot.test.ts b/src/tests/putaway/putawayImtemWithEmptyLot.test.ts index 6c713fe..051b491 100644 --- a/src/tests/putaway/putawayImtemWithEmptyLot.test.ts +++ b/src/tests/putaway/putawayImtemWithEmptyLot.test.ts @@ -3,6 +3,7 @@ import { ShipmentType } from '@/constants/ShipmentType'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { StockMovementResponse } from '@/types'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -12,6 +13,7 @@ import { test.describe('Putaway item with empty lot', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; test.beforeEach( async ({ @@ -20,6 +22,7 @@ test.describe('Putaway item with empty lot', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -57,11 +60,22 @@ test.describe('Putaway item with empty lot', () => { ); test.afterEach( - async ({ stockMovementService, navbar, transactionListPage }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - for (let n = 1; n < 5; n++) { - await transactionListPage.deleteTransaction(1); + async ( + { stockMovementService, navbar, transactionListPage, putawayService }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + for (let n = 1; n < 5; n++) { + await transactionListPage.deleteTransaction(1); + } } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); await RefreshCachesUtils.refreshCaches({ @@ -150,7 +164,7 @@ test.describe('Putaway item with empty lot', () => { await test.step('Start putaway', async () => { await createPutawayPage.table.row(1).checkbox.click(); await createPutawayPage.table.row(2).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); diff --git a/src/tests/putaway/putawayMoreThan1Item.test.ts b/src/tests/putaway/putawayMoreThan1Item.test.ts index ea5309d..f8c756c 100644 --- a/src/tests/putaway/putawayMoreThan1Item.test.ts +++ b/src/tests/putaway/putawayMoreThan1Item.test.ts @@ -3,6 +3,7 @@ import { ShipmentType } from '@/constants/ShipmentType'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { ProductResponse, StockMovementResponse } from '@/types'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -13,6 +14,7 @@ import { byNameAsc } from '@/utils/sortUtils'; test.describe('Create putaway for more than 1 item, separate putaways', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; let product: ProductResponse; let product2: ProductResponse; @@ -23,6 +25,7 @@ test.describe('Create putaway for more than 1 item, separate putaways', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -71,11 +74,22 @@ test.describe('Create putaway for more than 1 item, separate putaways', () => { ); test.afterEach( - async ({ stockMovementService, navbar, transactionListPage }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - for (let n = 1; n < 4; n++) { - await transactionListPage.deleteTransaction(1); + async ( + { stockMovementService, navbar, transactionListPage, putawayService }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + for (let n = 1; n < 4; n++) { + await transactionListPage.deleteTransaction(1); + } } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); } @@ -121,7 +135,7 @@ test.describe('Create putaway for more than 1 item, separate putaways', () => { createPutawayPage.table.row(2).getProductName(product2.name) ).toBeVisible(); await createPutawayPage.table.row(1).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -204,7 +218,7 @@ test.describe('Create putaway for more than 1 item, separate putaways', () => { createPutawayPage.table.row(1).getProductName(product2.name) ).toBeVisible(); await createPutawayPage.table.row(1).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -248,6 +262,7 @@ test.describe('Create putaway for more than 1 item, separate putaways', () => { test.describe('Putaway 2 items in the same putaway', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; let product: ProductResponse; let product2: ProductResponse; @@ -258,6 +273,7 @@ test.describe('Putaway 2 items in the same putaway', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -306,17 +322,29 @@ test.describe('Putaway 2 items in the same putaway', () => { ); test.afterEach( - async ({ - stockMovementShowPage, - stockMovementService, - navbar, - transactionListPage, - oldViewShipmentPage, - }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - await transactionListPage.deleteTransaction(1); - await transactionListPage.deleteTransaction(1); + async ( + { + stockMovementShowPage, + stockMovementService, + navbar, + transactionListPage, + oldViewShipmentPage, + putawayService, + }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + await transactionListPage.deleteTransaction(1); + await transactionListPage.deleteTransaction(1); + } await stockMovementShowPage.goToPage(STOCK_MOVEMENT.id); await stockMovementShowPage.detailsListTable.oldViewShipmentPage.click(); await oldViewShipmentPage.undoStatusChangeButton.click(); @@ -370,7 +398,7 @@ test.describe('Putaway 2 items in the same putaway', () => { ).toBeVisible(); await createPutawayPage.table.row(1).checkbox.click(); await createPutawayPage.table.row(2).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); diff --git a/src/tests/putaway/putawayToHoldBin.test.ts b/src/tests/putaway/putawayToHoldBin.test.ts index 478f74d..a7ab3a8 100644 --- a/src/tests/putaway/putawayToHoldBin.test.ts +++ b/src/tests/putaway/putawayToHoldBin.test.ts @@ -4,6 +4,7 @@ import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { StockMovementResponse } from '@/types'; import BinLocationUtils from '@/utils/BinLocationUtils'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -16,6 +17,7 @@ test.describe('Putaway item into hold bin', () => { test.describe.configure({ timeout: 60000 }); //timeout has been added for this test to make sure that the content on bin location tab will load as it can include a lot of data let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; const uniqueIdentifier = new UniqueIdentifier(); const holdBinLocationName = uniqueIdentifier.generateUniqueString('holdbin'); @@ -30,6 +32,7 @@ test.describe('Putaway item into hold bin', () => { locationListPage, createLocationPage, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); const product = await productService.getProduct(Product.FIVE); @@ -77,25 +80,37 @@ test.describe('Putaway item into hold bin', () => { ); test.afterEach( - async ({ - navbar, - transactionListPage, - stockMovementService, - page, - locationListPage, - mainLocationService, - createLocationPage, - }) => { + async ( + { + navbar, + transactionListPage, + stockMovementService, + page, + locationListPage, + mainLocationService, + createLocationPage, + putawayService, + }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + const receivingBin = AppConfig.instance.receivingBinPrefix + STOCK_MOVEMENT.identifier; - await navbar.configurationButton.click(); - await navbar.transactions.click(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); @@ -144,7 +159,7 @@ test.describe('Putaway item into hold bin', () => { await test.step('Start putaway', async () => { await createPutawayPage.table.row(0).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); diff --git a/src/tests/putaway/putawayToPreferredBin.test.ts b/src/tests/putaway/putawayToPreferredBin.test.ts index cedc477..f42ee49 100644 --- a/src/tests/putaway/putawayToPreferredBin.test.ts +++ b/src/tests/putaway/putawayToPreferredBin.test.ts @@ -1,8 +1,14 @@ +import path from 'node:path'; + import AppConfig from '@/config/AppConfig'; +import { PUTAWAY_URL } from '@/constants/applicationUrls'; import { ShipmentType } from '@/constants/ShipmentType'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { ProductResponse, StockMovementResponse } from '@/types'; +import { deleteFile, writeBufferToFile } from '@/utils/FileIOUtils'; +import { extractPdfColumnValues } from '@/utils/pdfUtils'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -13,8 +19,10 @@ import { byNameAsc } from '@/utils/sortUtils'; test.describe('Putaway to preferred bin and default bin', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; let product: ProductResponse; let product2: ProductResponse; + const downloadedFilePaths: string[] = []; test.beforeEach( async ({ @@ -26,6 +34,7 @@ test.describe('Putaway to preferred bin and default bin', () => { productEditPage, internalLocationService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -86,18 +95,30 @@ test.describe('Putaway to preferred bin and default bin', () => { ); test.afterEach( - async ({ - stockMovementService, - navbar, - transactionListPage, - productService, - productShowPage, - productEditPage, - }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - await transactionListPage.deleteTransaction(1); - await transactionListPage.deleteTransaction(1); + async ( + { + stockMovementService, + navbar, + transactionListPage, + productService, + productShowPage, + productEditPage, + putawayService, + }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + await transactionListPage.deleteTransaction(1); + await transactionListPage.deleteTransaction(1); + } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); const product2 = await productService.getProduct(Product.FOUR); await productShowPage.goToPage(product2.id); @@ -110,6 +131,10 @@ test.describe('Putaway to preferred bin and default bin', () => { productEditPage.inventoryLevelsTabSection.table ).toBeVisible(); await productEditPage.inventoryLevelsTabSection.createStockLevelModal.clickDeleteInventoryLevel(); + + while (downloadedFilePaths.length) { + deleteFile(downloadedFilePaths.pop() as string); + } } ); @@ -120,6 +145,7 @@ test.describe('Putaway to preferred bin and default bin', () => { internalLocationService, productShowPage, putawayDetailsPage, + page, }) => { const receivingBin = AppConfig.instance.receivingBinPrefix + STOCK_MOVEMENT.identifier; @@ -143,7 +169,7 @@ test.describe('Putaway to preferred bin and default bin', () => { .click(); await createPutawayPage.table.row(1).checkbox.click(); await createPutawayPage.table.row(2).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -162,6 +188,35 @@ test.describe('Putaway to preferred bin and default bin', () => { ).toContainText(internalLocation.name); }); + await test.step('Generate putaway pdf and assert bin columns', async () => { + const pdfResponsePromise = page.waitForResponse( + (resp) => + PUTAWAY_URL.generatePdfPattern.test(resp.url()) && + resp.status() === 200 + ); + const downloadPromise = page.waitForEvent('download'); + await createPutawayPage.startStep.generatePutawayListButton.click(); + const [pdfResponse, download] = await Promise.all([ + pdfResponsePromise, + downloadPromise, + ]); + + const pdfFilePath = path.join( + AppConfig.LOCAL_FILES_DIR_PATH, + download.suggestedFilename() + ); + writeBufferToFile(pdfFilePath, await pdfResponse.body()); + downloadedFilePaths.push(pdfFilePath); + + expect( + await extractPdfColumnValues(pdfFilePath, 'Preferred Bins') + ).toEqual(['', internalLocation.name]); + expect(await extractPdfColumnValues(pdfFilePath, 'Putaway Bin')).toEqual([ + '', + internalLocation.name, + ]); + }); + await test.step('Assert confirm complete putaway dialog when empty putaway bin', async () => { await createPutawayPage.startStep.nextButton.click(); await createPutawayPage.completeStep.isLoaded(); @@ -215,6 +270,7 @@ test.describe('Putaway to preferred bin and default bin', () => { internalLocation2Service, productShowPage, putawayDetailsPage, + page, }) => { const receivingBin = AppConfig.instance.receivingBinPrefix + STOCK_MOVEMENT.identifier; @@ -239,7 +295,7 @@ test.describe('Putaway to preferred bin and default bin', () => { .getExpandBinLocation(receivingBin) .click(); await createPutawayPage.table.row(2).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -252,6 +308,34 @@ test.describe('Putaway to preferred bin and default bin', () => { ).toContainText(internalLocation.name); }); + await test.step('Generate putaway pdf and assert bin columns', async () => { + const pdfResponsePromise = page.waitForResponse( + (resp) => + PUTAWAY_URL.generatePdfPattern.test(resp.url()) && + resp.status() === 200 + ); + const downloadPromise = page.waitForEvent('download'); + await createPutawayPage.startStep.generatePutawayListButton.click(); + const [pdfResponse, download] = await Promise.all([ + pdfResponsePromise, + downloadPromise, + ]); + + const pdfFilePath = path.join( + AppConfig.LOCAL_FILES_DIR_PATH, + download.suggestedFilename() + ); + writeBufferToFile(pdfFilePath, await pdfResponse.body()); + downloadedFilePaths.push(pdfFilePath); + + expect( + await extractPdfColumnValues(pdfFilePath, 'Preferred Bins') + ).toEqual([internalLocation.name]); + expect(await extractPdfColumnValues(pdfFilePath, 'Putaway Bin')).toEqual([ + internalLocation.name, + ]); + }); + await test.step('Edit putaway bin', async () => { await createPutawayPage.startStep.table.row(1).putawayBinSelect.click(); await createPutawayPage.startStep.table @@ -260,6 +344,34 @@ test.describe('Putaway to preferred bin and default bin', () => { .click(); }); + await test.step('Generate putaway pdf again and assert edited putaway bin', async () => { + const pdfResponsePromise = page.waitForResponse( + (resp) => + PUTAWAY_URL.generatePdfPattern.test(resp.url()) && + resp.status() === 200 + ); + const downloadPromise = page.waitForEvent('download'); + await createPutawayPage.startStep.generatePutawayListButton.click(); + const [pdfResponse, download] = await Promise.all([ + pdfResponsePromise, + downloadPromise, + ]); + + const pdfFilePath = path.join( + AppConfig.LOCAL_FILES_DIR_PATH, + download.suggestedFilename() + ); + writeBufferToFile(pdfFilePath, await pdfResponse.body()); + downloadedFilePaths.push(pdfFilePath); + + expect( + await extractPdfColumnValues(pdfFilePath, 'Preferred Bins') + ).toEqual([internalLocation.name]); + expect(await extractPdfColumnValues(pdfFilePath, 'Putaway Bin')).toEqual([ + internalLocation2.name, + ]); + }); + await test.step('Go to next page and assert edited putaway bin', async () => { await createPutawayPage.startStep.nextButton.click(); await createPutawayPage.completeStep.isLoaded(); diff --git a/src/tests/putaway/qtyValidationsInPutaways.test.ts b/src/tests/putaway/qtyValidationsInPutaways.test.ts index 0c53f6f..3885479 100644 --- a/src/tests/putaway/qtyValidationsInPutaways.test.ts +++ b/src/tests/putaway/qtyValidationsInPutaways.test.ts @@ -3,6 +3,7 @@ import { ShipmentType } from '@/constants/ShipmentType'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { StockMovementResponse } from '@/types'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -12,7 +13,7 @@ import { test.describe('Assert qty validations in putaways', () => { let STOCK_MOVEMENT: StockMovementResponse; - let putawayOrderIdentifier: string | undefined; + let PUTAWAY_ORDER_IDS: string[] = []; test.beforeEach( async ({ @@ -21,7 +22,7 @@ test.describe('Assert qty validations in putaways', () => { productService, receivingService, }) => { - putawayOrderIdentifier = undefined; + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -58,17 +59,12 @@ test.describe('Assert qty validations in putaways', () => { } ); - test.afterEach(async ({ putawayListPage, stockMovementService }) => { - // the received shipment must be cleaned up even when the test fails - // before the putaway is started - if (putawayOrderIdentifier) { - await putawayListPage.goToPage(); - await putawayListPage.table - .rowByOrderNumber(`${putawayOrderIdentifier}`.toString().trim()) - .actionsButton.click(); - await putawayListPage.table.clickDeleteOrderButton(1); - await putawayListPage.emptyPutawayList.isVisible(); - } + test.afterEach(async ({ stockMovementService, putawayService }, testInfo) => { + await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); }); @@ -97,14 +93,10 @@ test.describe('Assert qty validations in putaways', () => { await test.step('Start putaway', async () => { await createPutawayPage.table.row(0).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); - putawayOrderIdentifier = - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (await createPutawayPage.startStep.orderNumberValue.textContent())!; - await test.step('Try to edit qty to higher and assert validations', async () => { await createPutawayPage.startStep.table.row(0).editButton.click(); await createPutawayPage.startStep.table.row(0).quantityInput.fill('20'); diff --git a/src/tests/putaway/rollbackLastReceiptWhenPutawayCreated.test.ts b/src/tests/putaway/rollbackLastReceiptWhenPutawayCreated.test.ts index da9fa20..feb9ba0 100644 --- a/src/tests/putaway/rollbackLastReceiptWhenPutawayCreated.test.ts +++ b/src/tests/putaway/rollbackLastReceiptWhenPutawayCreated.test.ts @@ -3,6 +3,7 @@ import { ShipmentType } from '@/constants/ShipmentType'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { StockMovementResponse } from '@/types'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -12,6 +13,7 @@ import { test.describe('Rollback last receipt behavior when putaway created', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; test.beforeEach( async ({ @@ -20,6 +22,7 @@ test.describe('Rollback last receipt behavior when putaway created', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -57,15 +60,26 @@ test.describe('Rollback last receipt behavior when putaway created', () => { ); test.afterEach( - async ({ stockMovementService, navbar, transactionListPage }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); + async ( + { stockMovementService, navbar, transactionListPage, putawayService }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); } ); @@ -96,7 +110,7 @@ test.describe('Rollback last receipt behavior when putaway created', () => { await test.step('Start putaway', async () => { await createPutawayPage.table.row(0).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); diff --git a/src/tests/putaway/sortPutawayByCurrentPreferredAndOriginalOrder.test.ts b/src/tests/putaway/sortPutawayByCurrentPreferredAndOriginalOrder.test.ts index 6ab5bbb..b29a7e3 100644 --- a/src/tests/putaway/sortPutawayByCurrentPreferredAndOriginalOrder.test.ts +++ b/src/tests/putaway/sortPutawayByCurrentPreferredAndOriginalOrder.test.ts @@ -6,6 +6,7 @@ import { StockMovementResponse, } from '@/types'; import { assignPreferredBin } from '@/utils/productUtils'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, receiveInbound } from '@/utils/shipmentUtils'; import { byNameAsc } from '@/utils/sortUtils'; @@ -31,7 +32,7 @@ test.describe('Sort putaway by current bin, preferred bin and original order', ( let binOne: LocationResponse; let binTwo: LocationResponse; - let putawayOrderIdentifier: string | undefined; + let PUTAWAY_ORDER_IDS: string[] = []; test.beforeEach( async ({ @@ -44,7 +45,7 @@ test.describe('Sort putaway by current bin, preferred bin and original order', ( productShowPage, productEditPage, }) => { - putawayOrderIdentifier = undefined; + PUTAWAY_ORDER_IDS = []; inboundTwo = undefined; [productA, productB, productC] = [ await productService.getProduct(Product.ONE), @@ -87,26 +88,24 @@ test.describe('Sort putaway by current bin, preferred bin and original order', ( ); test.afterEach( - async ({ - stockMovementService, - navbar, - transactionListPage, - putawayListPage, - productShowPage, - productEditPage, - }) => { - // Remove the pending 2nd putaway and the 3 transactions created along - // the way; they exist only once the 2nd putaway has been started, but - // the shipments and preferred bins must be cleaned up regardless of - // how far the test got - if (putawayOrderIdentifier) { - await putawayListPage.goToPage(); - await putawayListPage.isLoaded(); - await putawayListPage.table - .rowByOrderNumber(`${putawayOrderIdentifier}`.toString().trim()) - .actionsButton.click(); - await putawayListPage.table.clickDeleteOrderButton(1); + async ( + { + stockMovementService, + navbar, + transactionListPage, + putawayService, + productShowPage, + productEditPage, + }, + testInfo + ) => { + const { anyPutawayCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + if (anyPutawayCompleted) { await navbar.configurationButton.click(); await navbar.transactions.click(); for (let i = 0; i < 3; i++) { @@ -166,7 +165,7 @@ test.describe('Sort putaway by current bin, preferred bin and original order', ( await createPutawayPage.table .rowByProductName(productA.name) .checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); // Putaway A into binTwo (its preferred bin is auto-suggested). @@ -219,14 +218,10 @@ test.describe('Sort putaway by current bin, preferred bin and original order', ( await createPutawayPage.table .rowByProductName(productC.name) .checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); - putawayOrderIdentifier = - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (await createPutawayPage.startStep.orderNumberValue.textContent())!; - await test.step('assert original order of items', async () => { await expect(createPutawayPage.startStep.sortButton).toContainText( 'Sort by current bins' diff --git a/src/tests/putaway/splitLineInPutaway.test.ts b/src/tests/putaway/splitLineInPutaway.test.ts index 7604ec5..70dae1e 100644 --- a/src/tests/putaway/splitLineInPutaway.test.ts +++ b/src/tests/putaway/splitLineInPutaway.test.ts @@ -3,6 +3,7 @@ import { ShipmentType } from '@/constants/ShipmentType'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import { StockMovementResponse } from '@/types'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -12,6 +13,7 @@ import { test.describe('Split line in Putaway', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; test.beforeEach( async ({ @@ -20,6 +22,7 @@ test.describe('Split line in Putaway', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -57,18 +60,29 @@ test.describe('Split line in Putaway', () => { ); test.afterEach( - async ({ stockMovementService, navbar, transactionListPage }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); - await transactionListPage.table.row(1).actionsButton.click(); - await transactionListPage.table.deleteButton.click(); - await expect(transactionListPage.successMessage).toBeVisible(); + async ( + { stockMovementService, navbar, transactionListPage, putawayService }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.deleteButton.click(); + await expect(transactionListPage.successMessage).toBeVisible(); + } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); } @@ -101,7 +115,7 @@ test.describe('Split line in Putaway', () => { await test.step('Start putaway', async () => { await createPutawayPage.table.row(0).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); @@ -208,7 +222,7 @@ test.describe('Split line in Putaway', () => { await test.step('Start putaway', async () => { await createPutawayPage.table.row(0).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); diff --git a/src/tests/putaway/validationOnQtyRemovedFromReceivingBin.test.ts b/src/tests/putaway/validationOnQtyRemovedFromReceivingBin.test.ts index 1f42307..0586e5a 100644 --- a/src/tests/putaway/validationOnQtyRemovedFromReceivingBin.test.ts +++ b/src/tests/putaway/validationOnQtyRemovedFromReceivingBin.test.ts @@ -6,6 +6,7 @@ import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; import ProductShowPage from '@/pages/product/productShow/ProductShowPage'; import { ProductResponse, StockMovementResponse } from '@/types'; +import { cleanupPendingPutaways } from '@/utils/putawayUtils'; import RefreshCachesUtils from '@/utils/RefreshCaches'; import { deleteShipment, @@ -16,6 +17,7 @@ import { byNameAsc } from '@/utils/sortUtils'; test.describe('Assert validation on qty removed from receiving bin', () => { let STOCK_MOVEMENT: StockMovementResponse; + let PUTAWAY_ORDER_IDS: string[] = []; let product: ProductResponse; let product2: ProductResponse; @@ -26,6 +28,7 @@ test.describe('Assert validation on qty removed from receiving bin', () => { productService, receivingService, }) => { + PUTAWAY_ORDER_IDS = []; const supplierLocation = await supplierLocationService.getLocation(); STOCK_MOVEMENT = await stockMovementService.createInbound({ originId: supplierLocation.id, @@ -74,11 +77,22 @@ test.describe('Assert validation on qty removed from receiving bin', () => { ); test.afterEach( - async ({ stockMovementService, navbar, transactionListPage }) => { - await navbar.configurationButton.click(); - await navbar.transactions.click(); - for (let n = 1; n < 6; n++) { - await transactionListPage.deleteTransaction(1); + async ( + { stockMovementService, navbar, transactionListPage, putawayService }, + testInfo + ) => { + const { allPutawaysCompleted } = await cleanupPendingPutaways({ + putawayService, + putawayOrderIds: PUTAWAY_ORDER_IDS, + testInfo, + }); + + if (allPutawaysCompleted) { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + for (let n = 1; n < 6; n++) { + await transactionListPage.deleteTransaction(1); + } } await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); } @@ -142,7 +156,7 @@ test.describe('Assert validation on qty removed from receiving bin', () => { ).toBeVisible(); await createPutawayPage.table.row(1).checkbox.click(); await createPutawayPage.table.row(2).checkbox.click(); - await createPutawayPage.startPutawayButton.click(); + PUTAWAY_ORDER_IDS.push(await createPutawayPage.startPutaway()); await createPutawayPage.startStep.isLoaded(); }); diff --git a/src/types.d.ts b/src/types.d.ts index 14254e2..a90c97f 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -34,6 +34,13 @@ type StockMovementListResponse = { totalCount: number; }; +// GET /api/putaways/ +type PutawayResponse = { + id: string; + putawayNumber?: string | null; + putawayStatus?: string | null; +}; + // GET /api/putaways returns rows flattened into dotted keys // (e.g. "product.name"), not nested objects type PutawayCandidate = { diff --git a/src/utils/pdfUtils.ts b/src/utils/pdfUtils.ts index 5a7a251..b62eafa 100644 --- a/src/utils/pdfUtils.ts +++ b/src/utils/pdfUtils.ts @@ -1,7 +1,305 @@ import fs from 'node:fs'; import { getDocument } from 'pdfjs-dist/legacy/build/pdf.js'; -import type { TextItem } from 'pdfjs-dist/types/src/display/api'; +import type { + PDFDocumentProxy, + PDFPageProxy, + TextItem, +} from 'pdfjs-dist/types/src/display/api'; + +type PositionedTextItem = { + str: string; + x: number; + y: number; +}; + +type PdfTextLine = { + y: number; + items: PositionedTextItem[]; +}; + +type ColumnBounds = { + startX: number; + endX: number; +}; + +type ColumnHeader = { + y: number; + bounds: ColumnBounds; +}; + +const Y_TOLERANCE = 2; +const X_TOLERANCE = 2; +/** + Max vertical distance (in pdf units) between text lines belonging to the + same table header row. Header cells are vertically centered, so a header + row with wrapped labels renders up to three text lines a few units apart, + while the first data row starts further below (cell padding + borders). +*/ +const HEADER_BLOCK_SPAN = 12; +/** + Max vertical distance between consecutive table row lines. A bigger gap + means the table has ended and following text (e.g. a signature section or + page footer) is not part of it. +*/ +const MAX_ROW_GAP = 25; +/** + Max vertical distance between text lines belonging to the same table row. + A cell with wrapped content renders continuation lines one text line + (~10 units) below, while the next table row starts further down + (cell padding + borders). +*/ +const MAX_CELL_LINE_GAP = 12; + +const loadPdfDocument = (filePath: string): Promise => { + const data = new Uint8Array(fs.readFileSync(filePath)); + return getDocument({ data }).promise; +}; + +const getPageNumbers = (doc: PDFDocumentProxy): number[] => + Array.from({ length: doc.numPages }, (_, i) => i + 1); + +const getPositionedTextItems = async ( + page: PDFPageProxy +): Promise => { + const content = await page.getTextContent(); + return ( + content.items + // getTextContent() returns TextItem | TextMarkedContent; only TextItem + // carries actual text (`str`), so this type guard drops marked-content + // markers and narrows the type for the mapping below + .filter((item): item is TextItem => 'str' in item) + .filter((item) => item.str.trim()) + .map((item) => ({ + str: item.str.trim(), + x: item.transform[4], + y: item.transform[5], + })) + ); +}; + +const groupItemsIntoLines = (items: PositionedTextItem[]): PdfTextLine[] => + [...items] + .sort((a, b) => b.y - a.y) + .reduce((lines, item) => { + const line = lines.find((l) => Math.abs(l.y - item.y) <= Y_TOLERANCE); + if (!line) { + return [...lines, { y: item.y, items: [item] }]; + } + line.items.push(item); + return lines; + }, []) + .map((line) => ({ + ...line, + items: [...line.items].sort((a, b) => a.x - b.x), + })); + +type HeaderMatch = { + startX: number; + topY: number; + bottomY: number; + matchedItems: Set; +}; + +/** + Tries to match the full header text starting from the given item. + A wrapped header label continues on following text lines of the header + block, with each continuation fragment left-aligned with the first one. +*/ +const matchHeaderFromItem = ( + lines: PdfTextLine[], + lineIndex: number, + itemIndex: number, + columnHeader: string +): HeaderMatch | null => { + const startX = lines[lineIndex].items[itemIndex].x; + const matchedItems = new Set(); + let text = ''; + let bottomY = lines[lineIndex].y; + + const consumeLineItems = (line: PdfTextLine, fromIndex: number) => { + for (let i = fromIndex; i < line.items.length; i++) { + const candidate = text + ? `${text} ${line.items[i].str}` + : line.items[i].str; + if (!columnHeader.startsWith(candidate)) { + return; + } + text = candidate; + matchedItems.add(line.items[i]); + bottomY = line.y; + } + }; + + consumeLineItems(lines[lineIndex], itemIndex); + + for (let i = lineIndex + 1; i < lines.length; i++) { + if (text === columnHeader || lines[i].y < bottomY - HEADER_BLOCK_SPAN) { + break; + } + const alignedItemIndex = lines[i].items.findIndex( + (item) => Math.abs(item.x - startX) <= X_TOLERANCE + ); + if (alignedItemIndex === -1) { + continue; + } + consumeLineItems(lines[i], alignedItemIndex); + } + + if (text !== columnHeader) { + return null; + } + return { startX, topY: lines[lineIndex].y, bottomY, matchedItems }; +}; + +const getHeaderBlockLines = ( + lines: PdfTextLine[], + match: HeaderMatch +): PdfTextLine[] => + lines + .filter((line) => line.y <= match.topY + HEADER_BLOCK_SPAN) + .filter((line) => line.y >= match.bottomY - HEADER_BLOCK_SPAN); + +/** + The column ends where the closest other text of the header block starts, + i.e. at the smallest x (greater than the column start) among header block + items not belonging to the matched header itself. +*/ +const findColumnEndX = ( + headerBlockLines: PdfTextLine[], + match: HeaderMatch +): number => + headerBlockLines + .reduce((items, line) => items.concat(line.items), []) + .filter((item) => !match.matchedItems.has(item)) + .filter((item) => item.x > match.startX + X_TOLERANCE) + .reduce((endX, item) => Math.min(endX, item.x), Infinity); + +const findColumnHeader = ( + lines: PdfTextLine[], + columnHeader: string +): ColumnHeader | null => { + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + for ( + let itemIndex = 0; + itemIndex < lines[lineIndex].items.length; + itemIndex++ + ) { + const match = matchHeaderFromItem( + lines, + lineIndex, + itemIndex, + columnHeader + ); + if (!match) { + continue; + } + const headerBlockLines = getHeaderBlockLines(lines, match); + return { + y: Math.min(...headerBlockLines.map((line) => line.y)), + bounds: { + startX: match.startX, + endX: findColumnEndX(headerBlockLines, match), + }, + }; + } + } + return null; +}; + +/** + Collects the lines making up the table body: lines below the header row, + until the first vertical gap bigger than a table row. +*/ +const getTableRowLines = ( + lines: PdfTextLine[], + headerBottomY: number +): PdfTextLine[] => { + const rowLines: PdfTextLine[] = []; + let previousY = headerBottomY; + + for (const line of lines) { + if (line.y >= headerBottomY - Y_TOLERANCE) { + continue; + } + if (previousY - line.y > MAX_ROW_GAP) { + break; + } + rowLines.push(line); + previousY = line.y; + } + + return rowLines; +}; + +const getCellValue = ( + line: PdfTextLine, + { startX, endX }: ColumnBounds +): string => + line.items + .filter((item) => item.x >= startX - X_TOLERANCE) + .filter((item) => item.x < endX - X_TOLERANCE) + .map((item) => item.str) + .join(' '); + +/** + Groups consecutive table body lines into rows: a line close enough to the + previous one is a continuation of the same row (wrapped cell content). +*/ +const groupLinesIntoRows = (rowLines: PdfTextLine[]): PdfTextLine[][] => + rowLines.reduce((rows, line) => { + const currentRow = rows[rows.length - 1]; + const previousLine = currentRow?.[currentRow.length - 1]; + if (previousLine && previousLine.y - line.y <= MAX_CELL_LINE_GAP) { + currentRow.push(line); + return rows; + } + return [...rows, [line]]; + }, []); + +const extractColumnValuesFromLines = ( + lines: PdfTextLine[], + columnHeader: string +): string[] => { + const header = findColumnHeader(lines, columnHeader); + if (!header) { + return []; + } + return groupLinesIntoRows(getTableRowLines(lines, header.y)).map((rowLines) => + rowLines + .map((line) => getCellValue(line, header.bounds)) + .filter(Boolean) + .join(' ') + ); +}; + +/** + Extracts cell values of a single table column from a PDF, identified by + its column header (wrapped, multi-line header labels are supported). + Column boundaries are derived from x coordinates of the header text, so + only values positioned within that column are returned — one entry per + table row (per page), with an empty string for an empty cell. A cell + wrapped into multiple text lines is joined back with spaces. +*/ +export const extractPdfColumnValues = async ( + filePath: string, + columnHeader: string +): Promise => { + const doc = await loadPdfDocument(filePath); + + const pageValues = await Promise.all( + getPageNumbers(doc).map(async (pageNum) => { + const page = await doc.getPage(pageNum); + const items = await getPositionedTextItems(page); + return extractColumnValuesFromLines( + groupItemsIntoLines(items), + columnHeader + ); + }) + ); + + return pageValues.reduce((all, values) => all.concat(values), []); +}; export const pdfContainsValues = async ( filePath: string, @@ -12,14 +310,11 @@ export const pdfContainsValues = async ( }; export const extractPdfText = async (filePath: string): Promise => { - const data = new Uint8Array(fs.readFileSync(filePath)); - const doc = await getDocument({ data }).promise; + const doc = await loadPdfDocument(filePath); - // 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) => { + getPageNumbers(doc).map(async (pageNum) => { const page = await doc.getPage(pageNum); const content = await page.getTextContent(); return content.items diff --git a/src/utils/putawayUtils.ts b/src/utils/putawayUtils.ts new file mode 100644 index 0000000..a6e77cc --- /dev/null +++ b/src/utils/putawayUtils.ts @@ -0,0 +1,57 @@ +import { TestInfo } from '@playwright/test'; + +import PutawayService from '@/api/PutawayService'; + +/** + Deletes the putaway order if it is still in PENDING status. Returns true + when a pending putaway was deleted, false when there was nothing to delete + (the order was completed or no longer exists). +*/ +async function deletePutawayOrderIfPending( + putawayService: PutawayService, + putawayOrderId: string +): Promise { + const putaway = await putawayService.getPutaway(putawayOrderId); + if (putaway?.data?.putawayStatus !== 'PENDING') { + return false; + } + await putawayService.deletePutawayOrder(putawayOrderId); + return true; +} + +/** + Shared afterEach cleanup for putaway tests: extends the hook timeout to + leave room for the cleanup itself, then deletes every recorded putaway + order that is still pending. + + Returns flags describing whether the putaways had already been completed, + so callers know whether the side effects of completing a putaway (e.g. + transactions) exist and need their own cleanup. +*/ +export async function cleanupPendingPutaways({ + putawayService, + putawayOrderIds, + testInfo, +}: { + putawayService: PutawayService; + putawayOrderIds: string[]; + testInfo: TestInfo; +}) { + testInfo.setTimeout(testInfo.timeout + 60_000); + + const deletedPendingPutaways = []; + for (const putawayOrderId of putawayOrderIds) { + deletedPendingPutaways.push( + await deletePutawayOrderIfPending(putawayService, putawayOrderId) + ); + } + + return { + allPutawaysCompleted: + putawayOrderIds.length > 0 && + deletedPendingPutaways.every((wasPending) => !wasPending), + anyPutawayCompleted: deletedPendingPutaways.some( + (wasPending) => !wasPending + ), + }; +}