From 53646bd33c1ae86e811e1189b910915b95bfaab1 Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Thu, 20 Aug 2026 09:05:53 +0200 Subject: [PATCH 1/8] add page elements --- src/components/Navbar.ts | 4 +++ .../manageCycleCount/ManageCycleCountPage.ts | 32 ++++++++++++++++++- .../components/CountStepTable.ts | 5 +++ .../components/RecountStepTable.ts | 4 +++ .../productShow/tabs/InStockTabSection.ts | 4 +++ 5 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/components/Navbar.ts b/src/components/Navbar.ts index 42e07abd..35f62b3d 100644 --- a/src/components/Navbar.ts +++ b/src/components/Navbar.ts @@ -116,6 +116,10 @@ class Navbar extends BasePageModel { get inventory() { return this.getNavItem('Inventory'); } + + get performCycleCount() { + return this.getNavItem('Perform Cycle Count'); + } } export default Navbar; diff --git a/src/pages/manageCycleCount/ManageCycleCountPage.ts b/src/pages/manageCycleCount/ManageCycleCountPage.ts index afe33334..54f14662 100644 --- a/src/pages/manageCycleCount/ManageCycleCountPage.ts +++ b/src/pages/manageCycleCount/ManageCycleCountPage.ts @@ -8,17 +8,23 @@ import ToCountTable from '@/pages/manageCycleCount/components/ToCountTable'; class ManageCycleCountPage extends BasePageModel { allProductsTable: AllProductsTable; toCountTable: ToCountTable; + toResolveTable: ToCountTable; constructor(page: Page) { super(page); this.allProductsTable = new AllProductsTable(page); this.toCountTable = new ToCountTable(page); + this.toResolveTable = new ToCountTable(page); } async goToPage() { await this.page.goto(CYCLE_COUNT_URL.base); } + async goToPerformCycleCount() { + await this.page.goto(`${CYCLE_COUNT_URL.base}?tab=TO_COUNT`); + } + async isLoaded() { await expect(this.allProductsTab).toBeVisible(); } @@ -64,10 +70,30 @@ class ManageCycleCountPage extends BasePageModel { } async searchProduct(productName: string) { - await this.searchInput.fill(productName); + await this.searchInput.click(); + await this.page.keyboard.press('ControlOrMeta+A'); + await this.searchInput.pressSequentially(productName, { delay: 20 }); + await expect(this.searchInput).toHaveValue(productName); await this.filterButton.click(); } + get negativeQuantityFilterGroup() { + return this.page.locator('.filter-group', { + has: this.page.locator('label[for="negativeQuantity"]'), + }); + } + + get negativeQuantityCheckbox() { + return this.negativeQuantityFilterGroup.getByRole('checkbox'); + } + + async openNegativeQuantityTooltip() { + const trigger = this.negativeQuantityFilterGroup.locator('[data-tooltipped]'); + await trigger.hover(); + const describedBy = await trigger.getAttribute('aria-describedby'); + return this.page.locator(`#${describedBy}`); + } + // ACTIONS get markAsToCountButton() { return this.page.getByRole('button', { name: 'Mark as To Count' }); @@ -76,6 +102,10 @@ class ManageCycleCountPage extends BasePageModel { get startCountButton() { return this.page.getByRole('button', { name: 'Start Count' }); } + + get startResolutionButton() { + return this.page.getByRole('button', { name: 'Start resolution' }); + } } export default ManageCycleCountPage; diff --git a/src/pages/manageCycleCount/components/CountStepTable.ts b/src/pages/manageCycleCount/components/CountStepTable.ts index 0571e1cd..51e30071 100644 --- a/src/pages/manageCycleCount/components/CountStepTable.ts +++ b/src/pages/manageCycleCount/components/CountStepTable.ts @@ -14,6 +14,10 @@ class CountStepTable extends BasePageModel { row(index: number) { return new Row(this.page, this.rows.nth(index)); } + + rowByBinLocation(binLocationName: string) { + return new Row(this.page, this.rows.filter({ hasText: binLocationName }).first()); + } } class Row extends BasePageModel { @@ -59,6 +63,7 @@ class Row extends BasePageModel { await this.quantityCountedInput.pressSequentially(value, { delay: 100, }); + await this.page.waitForTimeout(300); await this.page.keyboard.press('Tab'); await expect(this.quantityCountedInput).toHaveValue(value, { timeout: 2000, diff --git a/src/pages/manageCycleCount/components/RecountStepTable.ts b/src/pages/manageCycleCount/components/RecountStepTable.ts index 194bdf06..f2977493 100644 --- a/src/pages/manageCycleCount/components/RecountStepTable.ts +++ b/src/pages/manageCycleCount/components/RecountStepTable.ts @@ -14,6 +14,10 @@ class RecountStepTable extends BasePageModel { row(index: number) { return new Row(this.page, this.rows.nth(index)); } + + rowByBinLocation(binLocationName: string) { + return new Row(this.page, this.rows.filter({ hasText: binLocationName }).first()); + } } class Row extends BasePageModel { diff --git a/src/pages/product/productShow/tabs/InStockTabSection.ts b/src/pages/product/productShow/tabs/InStockTabSection.ts index 9b7ee9a2..ee8489c1 100644 --- a/src/pages/product/productShow/tabs/InStockTabSection.ts +++ b/src/pages/product/productShow/tabs/InStockTabSection.ts @@ -32,6 +32,10 @@ class InStockTabSection extends BasePageModel { return new Row(this.page, this.rows.nth(index)); } + rowByBinLocation(binLocationName: string) { + return new Row(this.page, this.rows.filter({ hasText: binLocationName }).first()); + } + get stockTransferButton() { return this.page.getByRole('link', { name: 'Transfer Stock' }); } From c7befe5bddfa0d93f23dbbfba67935ed2bf29828 Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Thu, 20 Aug 2026 09:06:38 +0200 Subject: [PATCH 2/8] add new products and inventory --- src/setup/dataImport/cycleCountInventory.csv | 1 + src/setup/dataImport/products.csv | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/setup/dataImport/cycleCountInventory.csv b/src/setup/dataImport/cycleCountInventory.csv index 34b3e0a7..6ff63738 100644 --- a/src/setup/dataImport/cycleCountInventory.csv +++ b/src/setup/dataImport/cycleCountInventory.csv @@ -1,2 +1,3 @@ Product code,Product,Lot number,Expiration date,Bin location,Quantity,Comment 7,CC-product-one,,,CC-BIN-1,100, +9,CC-product-three,,,CC-BIN-2,10, diff --git a/src/setup/dataImport/products.csv b/src/setup/dataImport/products.csv index 74b82c91..ec90ba23 100644 --- a/src/setup/dataImport/products.csv +++ b/src/setup/dataImport/products.csv @@ -7,5 +7,5 @@ Id,Active,ProductCode,ProductType,Name,ProductFamily,Category,GLAccount,Descript ,true,6,Default,E2E-product-six,,ARVS,,,,,,,,,,,,,,,,,,,,, ,true,7,Default,CC-product-one,,Anesteshia,,,,,,,,,,,,,,,,,,,,, ,true,8,Default,CC-product-two,,Anesteshia,,,,,,,,,,,,,,,,,,,,, - +,true,9,Default,CC-product-three,,ARVS,,,,,,,,,,,,,,,,,,,,, From 96bdce86fc70cb608f8843e37245e962aeb49a9a Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Thu, 20 Aug 2026 09:07:14 +0200 Subject: [PATCH 3/8] add improvement for existing test --- src/tests/cycleCount/performCC.test.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/tests/cycleCount/performCC.test.ts b/src/tests/cycleCount/performCC.test.ts index 6fae34dc..a1d98aae 100644 --- a/src/tests/cycleCount/performCC.test.ts +++ b/src/tests/cycleCount/performCC.test.ts @@ -46,6 +46,8 @@ test.describe('Perform cycle count for item', () => { confirmToRecountStepPage, productShowPage, }) => { + test.setTimeout(150_000); + const USER = await mainUserService.getUser(); await test.step('Assert content of inventory menu', async () => { @@ -68,9 +70,15 @@ test.describe('Perform cycle count for item', () => { }); await test.step('Search for product on All Products tab', async () => { - await manageCycleCountPage.openAllProductsTab(); - await manageCycleCountPage.searchProduct(productName); - await expect(manageCycleCountPage.allProductsTable.rows).toHaveCount(1); + await expect(async () => { + await manageCycleCountPage.goToPage(); + await manageCycleCountPage.isLoaded(); + await manageCycleCountPage.openAllProductsTab(); + await manageCycleCountPage.searchProduct(productName); + await expect(manageCycleCountPage.allProductsTable.rows).toHaveCount( + 1 + ); + }).toPass({ timeout: 90_000, intervals: [2000, 3000, 5000] }); }); await test.step('Assert Last Counted date on All Products tab matches stock card', async () => { From 3f58d04150f307f7cb6501c46ad885fbf6eb0762 Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Thu, 20 Aug 2026 09:07:28 +0200 Subject: [PATCH 4/8] add new cc test --- ...performCCitemWithNegativeInventory.test.ts | 421 ++++++++++++++++++ 1 file changed, 421 insertions(+) create mode 100644 src/tests/cycleCount/performCCitemWithNegativeInventory.test.ts diff --git a/src/tests/cycleCount/performCCitemWithNegativeInventory.test.ts b/src/tests/cycleCount/performCCitemWithNegativeInventory.test.ts new file mode 100644 index 00000000..b84b6c6f --- /dev/null +++ b/src/tests/cycleCount/performCCitemWithNegativeInventory.test.ts @@ -0,0 +1,421 @@ +import AppConfig from '@/config/AppConfig'; +import { expect, test } from '@/fixtures/fixtures'; +import { StockMovementResponse } from '@/types'; +import { deleteShipment, receiveInbound } from '@/utils/shipmentUtils'; + +test.describe('Perform cycle count when item has negative inventory', () => { + const productCode = '9'; + const productName = AppConfig.instance.products[productCode].name; + const facilityId = AppConfig.instance.locations.ccDepot.readId(); + + let STOCK_MOVEMENT: StockMovementResponse; + let cycleCountId: string | undefined; + + test.beforeEach( + async ({ + supplierLocationService, + stockMovementService, + receivingService, + }) => { + const supplierLocation = await supplierLocationService.getLocation(); + const productId = AppConfig.instance.products[productCode].readId(); + + STOCK_MOVEMENT = await stockMovementService.createInbound({ + originId: supplierLocation.id, + }); + + await stockMovementService.addItemsToInboundStockMovement( + STOCK_MOVEMENT.id, + [{ productId, quantity: 20 }] + ); + + await receiveInbound( + { stockMovementService, receivingService }, + STOCK_MOVEMENT, + [20] + ); + } + ); + + test.afterEach( + async ({ + cycleCountService, + stockMovementService, + productShowPage, + transactionService, + }) => { + if (cycleCountId) { + await cycleCountService.deleteCycleCount(facilityId, cycleCountId); + } + await deleteShipment({ stockMovementService, STOCK_MOVEMENT }); + + const productId = AppConfig.instance.products[productCode].readId(); + await productShowPage.goToPage(productId); + await productShowPage.stockHistoryTab.click(); + await productShowPage.stockHistoryTabSection.isLoaded(); + + const rows = productShowPage.stockHistoryTabSection.rows; + const count = await rows.count(); + + const transactionIds: string[] = []; + for (let i = 2; i < count; i++) { + const row = productShowPage.stockHistoryTabSection.row(i); + const links = await row.transactionLink.all(); + for (const link of links) { + const href = await link.getAttribute('href'); + const id = href?.match(/showTransaction\/(\w+)/)?.[1]; + if (id && !transactionIds.includes(id)) { + transactionIds.push(id); + } + } + } + + for (const id of transactionIds.reverse()) { + await transactionService.deleteTransaction(id); + } + } + ); + + test('Perform cycle count when item has negative inventory', async ({ + page, + navbar, + manageCycleCountPage, + countStepPage, + confirmToCountStepPage, + recountStepPage, + confirmToRecountStepPage, + productShowPage, + ccDepotService, + transactionService, + }) => { + test.setTimeout(120_000); + + const productId = AppConfig.instance.products[productCode].readId(); + const receivingBin = + AppConfig.instance.receivingBinPrefix + STOCK_MOVEMENT.identifier; + let inboundTransferInRowIndex: number; + + await test.step('Open All Products tab and assert product is not displayed first', async () => { + await expect(async () => { + await manageCycleCountPage.goToPage(); + await manageCycleCountPage.isLoaded(); + await manageCycleCountPage.openAllProductsTab(); + + const row = manageCycleCountPage.allProductsTable.row(0); + await expect(row.product).not.toContainText(productName); + }).toPass({ timeout: 30_000, intervals: [2000, 3000, 5000] }); + }); + + await test.step('Go to stock card and note the inbound Transfer In transaction', async () => { + await productShowPage.goToPage(productId); + await productShowPage.stockHistoryTab.click(); + await productShowPage.stockHistoryTabSection.isLoaded(); + + inboundTransferInRowIndex = + (await productShowPage.stockHistoryTabSection.rows.count()) - 1; + }); + + await test.step('Perform Stock Transfer of inventory from receiving bin to CC-BIN-1', async () => { + const ccDepot = await ccDepotService.getLocation(); + + await productShowPage.inStockTab.click(); + await productShowPage.inStockTabSection.isLoaded(); + + const row = + productShowPage.inStockTabSection.rowByBinLocation(receivingBin); + await row.actionsButton.click(); + await productShowPage.inStockTabSection.stockTransferButton.click(); + await productShowPage.inStockTabSection.stockTransferDialog.isLoaded(); + + await productShowPage.inStockTabSection.stockTransferDialog.locationSelect.click(); + await productShowPage.inStockTabSection.stockTransferDialog.selectLocation( + ccDepot.name + ); + await productShowPage.inStockTabSection.stockTransferDialog.binLocationSelect.click(); + await productShowPage.inStockTabSection.stockTransferDialog.selectLocation( + 'CC-BIN-1' + ); + await productShowPage.inStockTabSection.stockTransferDialog.transferStockButton.click(); + + await productShowPage.inStockTab.click(); + await productShowPage.inStockTabSection.isLoaded(); + await expect( + productShowPage.inStockTabSection.rowByBinLocation('CC-BIN-1') + .binLocation + ).toContainText('CC-BIN-1'); + }); + + await test.step('Delete the Transfer In transaction created by the inbound receipt', async () => { + await productShowPage.stockHistoryTab.click(); + await productShowPage.stockHistoryTabSection.isLoaded(); + + const row = productShowPage.stockHistoryTabSection.row( + inboundTransferInRowIndex + ); + const href = await row.transactionLink.getAttribute('href'); + const transactionId = href?.match(/showTransaction\/(\w+)/)?.[1]; + // eslint-disable-next-line playwright/no-conditional-in-test + if (!transactionId) { + throw new Error( + 'Could not find id of the inbound Transfer In transaction' + ); + } + await transactionService.deleteTransaction(transactionId); + }); + + await test.step('Assert negative inventory in the receiving bin', async () => { + await productShowPage.goToPage(productId); + await productShowPage.inStockTab.click(); + await productShowPage.inStockTabSection.isLoaded(); + + const row = + productShowPage.inStockTabSection.rowByBinLocation(receivingBin); + await expect(row.quantityOnHand).toHaveText('-20'); + }); + + await test.step('Assert product shows first on All Products tab due to negative inv', async () => { + await expect(async () => { + await manageCycleCountPage.goToPage(); + await manageCycleCountPage.isLoaded(); + await manageCycleCountPage.openAllProductsTab(); + + const row = manageCycleCountPage.allProductsTable.row(0); + await expect(row.product).toContainText(productName); + await expect(row.quantity).not.toBeEmpty(); + await expect(row.binLocation).toContainText('CC-BIN-1'); + await expect(row.binLocation).toContainText('CC-BIN-2'); + await expect(row.binLocation).toContainText(receivingBin); + }).toPass({ timeout: 30_000, intervals: [2000, 3000, 5000] }); + }); + + await test.step('Assert Negative quantity filter tooltip and apply the filter', async () => { + const tooltip = await manageCycleCountPage.openNegativeQuantityTooltip(); + await expect(tooltip).toContainText( + 'Returns products with negative inventory items in stock. Unselected for all products.' + ); + + await manageCycleCountPage.negativeQuantityCheckbox.check(); + await manageCycleCountPage.filterButton.click(); + await manageCycleCountPage.allProductsTable.isLoaded(); + + await expect(manageCycleCountPage.allProductsTable.rows).toHaveCount(1); + const row = manageCycleCountPage.allProductsTable.row(0); + await expect(row.product).toContainText(productName); + await expect(row.quantity).not.toBeEmpty(); + }); + + await test.step('Select item and mark as To Count', async () => { + const row = manageCycleCountPage.allProductsTable.row(0); + await row.checkbox.check(); + await manageCycleCountPage.markAsToCountButton.click(); + }); + + await test.step('Start count from the To Count tab', async () => { + await expect(manageCycleCountPage.toCountTab).toHaveClass('active-tab'); + await manageCycleCountPage.toCountTable.isLoaded(); + + const [response] = await Promise.all([ + page.waitForResponse((res) => res.url().includes('/start/batch')), + manageCycleCountPage.startCountButton.click(), + ]); + const body = await response.json(); + cycleCountId = body.data[0].id; + await countStepPage.isLoaded(); + }); + + await test.step('Assert bin locations on count step and fill quantities', async () => { + await expect(countStepPage.countStepTable.rows).toHaveCount(3); + + const ccBin1Row = + countStepPage.countStepTable.rowByBinLocation('CC-BIN-1'); + await expect(ccBin1Row.binLocation).toContainText('CC-BIN-1'); + await ccBin1Row.fillQuantityCounted('0'); + + const ccBin2Row = + countStepPage.countStepTable.rowByBinLocation('CC-BIN-2'); + await expect(ccBin2Row.binLocation).toContainText('CC-BIN-2'); + await ccBin2Row.fillQuantityCounted('10'); + + const receivingBinRow = + countStepPage.countStepTable.rowByBinLocation(receivingBin); + await expect(receivingBinRow.binLocation).toContainText(receivingBin); + await receivingBinRow.fillQuantityCounted('0'); + }); + + await test.step('Save progress and go to Perform Cycle Count', async () => { + await countStepPage.saveProgressButton.click(); + await navbar.inventory.click(); + await expect(navbar.performCycleCount).toBeVisible(); + await page.keyboard.press('Escape'); + + await manageCycleCountPage.goToPerformCycleCount(); + await expect(manageCycleCountPage.toCountTab).toHaveClass('active-tab'); + }); + + await test.step('Assert product present on To Count tab with In progress status', async () => { + await expect(async () => { + await manageCycleCountPage.goToPerformCycleCount(); + await manageCycleCountPage.toCountTable.isLoaded(); + await expect(manageCycleCountPage.toCountTable.rows).toHaveCount(1); + + const row = manageCycleCountPage.toCountTable.row(0); + await expect(row.product).toContainText(productName); + await expect(row.status).toHaveText('In progress'); + }).toPass({ timeout: 30_000, intervals: [2000, 3000, 5000] }); + }); + + await test.step('Apply Negative quantity filter on To Count tab', async () => { + await manageCycleCountPage.negativeQuantityCheckbox.check(); + await manageCycleCountPage.filterButton.click(); + + await expect(manageCycleCountPage.toCountTable.rows).toHaveCount(1); + const row = manageCycleCountPage.toCountTable.row(0); + await expect(row.product).toContainText(productName); + await expect(row.quantity).not.toBeEmpty(); + }); + + await test.step('Select item and start count again', async () => { + const row = manageCycleCountPage.toCountTable.row(0); + await row.checkbox.check(); + await manageCycleCountPage.startCountButton.click(); + await countStepPage.isLoaded(); + }); + + await test.step('Assert previously entered quantities were not lost', async () => { + await expect(async () => { + await page.reload(); + await countStepPage.isLoaded(); + + const ccBin1Row = + countStepPage.countStepTable.rowByBinLocation('CC-BIN-1'); + await expect(ccBin1Row.quantityCountedInput).toHaveValue('0'); + + const ccBin2Row = + countStepPage.countStepTable.rowByBinLocation('CC-BIN-2'); + await expect(ccBin2Row.quantityCountedInput).toHaveValue('10'); + + const receivingBinRow = + countStepPage.countStepTable.rowByBinLocation(receivingBin); + await expect(receivingBinRow.quantityCountedInput).toHaveValue('0'); + }).toPass({ timeout: 30_000, intervals: [2000, 3000, 5000] }); + }); + + await test.step('Save count and choose Not now on the resolve discrepancies dialog', async () => { + await countStepPage.nextButton.click(); + await confirmToCountStepPage.isLoaded(); + await confirmToCountStepPage.saveButton.click(); + + await confirmToCountStepPage.resolveDiscrepancyDialog.isLoaded(); + await confirmToCountStepPage.resolveDiscrepancyDialog.notNowButton.click(); + }); + + await test.step('Assert product present on To Resolve tab with To resolve status', async () => { + await expect(manageCycleCountPage.toResolveTab).toHaveClass('active-tab'); + await expect(manageCycleCountPage.toResolveTable.rows).toHaveCount(1); + + const row = manageCycleCountPage.toResolveTable.row(0); + await expect(row.product).toContainText(productName); + await expect(row.status).toHaveText('To resolve'); + }); + + await test.step('Apply Negative quantity filter on To Resolve tab', async () => { + await manageCycleCountPage.negativeQuantityCheckbox.check(); + await manageCycleCountPage.filterButton.click(); + + await expect(manageCycleCountPage.toResolveTable.rows).toHaveCount(1); + const row = manageCycleCountPage.toResolveTable.row(0); + await expect(row.product).toContainText(productName); + await expect(row.quantity).not.toBeEmpty(); + }); + + await test.step('Select item and start resolution', async () => { + const row = manageCycleCountPage.toResolveTable.row(0); + await row.checkbox.check(); + await manageCycleCountPage.startResolutionButton.click(); + await recountStepPage.isLoaded(); + }); + + await test.step('Assert bin locations and quantity counted on recount step, then fill quantity recounted', async () => { + await expect(recountStepPage.recountStepTable.rows).toHaveCount(3); + + const ccBin1Row = + recountStepPage.recountStepTable.rowByBinLocation('CC-BIN-1'); + await expect(ccBin1Row.binLocation).toContainText('CC-BIN-1'); + await expect(ccBin1Row.quantityCounted).toHaveText('0'); + await expect(ccBin1Row.countDifferenceValue).toHaveText('20'); + await expect(ccBin1Row.countDifferenceValue).toHaveCSS( + 'color', + 'rgb(199, 22, 16)' + ); + await expect(ccBin1Row.countDifferenceIcon).toBeVisible(); + await ccBin1Row.fillQuantityRecounted('0'); + + const ccBin2Row = + recountStepPage.recountStepTable.rowByBinLocation('CC-BIN-2'); + await expect(ccBin2Row.binLocation).toContainText('CC-BIN-2'); + await expect(ccBin2Row.quantityCounted).toHaveText('10'); + await expect(ccBin2Row.countDifferenceValue).toHaveText('EQUAL'); + await expect(ccBin2Row.countDifferenceValue).toHaveCSS( + 'color', + 'rgb(0, 82, 204)' + ); + await expect(ccBin2Row.countDifferenceIcon).toBeHidden(); + await ccBin2Row.fillQuantityRecounted('10'); + + const receivingBinRow = + recountStepPage.recountStepTable.rowByBinLocation(receivingBin); + await expect(receivingBinRow.binLocation).toContainText(receivingBin); + await expect(receivingBinRow.quantityCounted).toHaveText('0'); + await expect(receivingBinRow.countDifferenceValue).toHaveText('20'); + await expect(receivingBinRow.countDifferenceValue).toHaveCSS( + 'color', + 'rgb(19, 173, 96)' + ); + await expect(receivingBinRow.countDifferenceIcon).toBeVisible(); + await receivingBinRow.fillQuantityRecounted('0'); + }); + + await test.step('Click Next, validate empty root cause message, click Next again', async () => { + await recountStepPage.nextButton.click(); + + await expect(recountStepPage.emptyRootCauseAlert).toBeVisible(); + await recountStepPage.closeAlert(); + await recountStepPage.nextButton.click(); + await confirmToRecountStepPage.isLoaded(); + }); + + await test.step('Save recount to finalize the cycle count', async () => { + await confirmToRecountStepPage.saveButton.click(); + await manageCycleCountPage.isLoaded(); + }); + + await test.step('Assert only one inventory item in stock on the stock card', async () => { + await productShowPage.goToPage(productId); + await productShowPage.inStockTab.click(); + await productShowPage.inStockTabSection.isLoaded(); + + await expect(productShowPage.inStockTabSection.rows).toHaveCount(3); + const row = productShowPage.inStockTabSection.row(1); + await expect(row.binLocation).toContainText('CC-BIN-2'); + await expect(row.quantityOnHand).toHaveText('10'); + }); + + await test.step('Assert product no longer shows first on All Products tab', async () => { + await expect(async () => { + await manageCycleCountPage.goToPage(); + await manageCycleCountPage.isLoaded(); + await manageCycleCountPage.openAllProductsTab(); + + const row = manageCycleCountPage.allProductsTable.row(0); + await expect(row.product).not.toContainText(productName); + }).toPass({ timeout: 30_000, intervals: [2000, 3000, 5000] }); + }); + + await test.step('Assert Negative quantity filter now returns an empty table', async () => { + await manageCycleCountPage.negativeQuantityCheckbox.check(); + await manageCycleCountPage.filterButton.click(); + + await expect(manageCycleCountPage.allProductsTable.rows).toHaveCount(0); + }); + }); +}); From 3e547d93b32da4a7f53d3a6953d069401b03d39f Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Thu, 20 Aug 2026 10:55:06 +0200 Subject: [PATCH 5/8] increase timeout for ci --- src/tests/cycleCount/performCC.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/cycleCount/performCC.test.ts b/src/tests/cycleCount/performCC.test.ts index a1d98aae..3582b263 100644 --- a/src/tests/cycleCount/performCC.test.ts +++ b/src/tests/cycleCount/performCC.test.ts @@ -46,7 +46,7 @@ test.describe('Perform cycle count for item', () => { confirmToRecountStepPage, productShowPage, }) => { - test.setTimeout(150_000); + test.setTimeout(240_000); const USER = await mainUserService.getUser(); @@ -78,7 +78,7 @@ test.describe('Perform cycle count for item', () => { await expect(manageCycleCountPage.allProductsTable.rows).toHaveCount( 1 ); - }).toPass({ timeout: 90_000, intervals: [2000, 3000, 5000] }); + }).toPass({ timeout: 180_000, intervals: [2000, 3000, 5000] }); }); await test.step('Assert Last Counted date on All Products tab matches stock card', async () => { From ac6225884850fee30e0535fc99fea8862e8b2cb0 Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Thu, 20 Aug 2026 12:06:36 +0200 Subject: [PATCH 6/8] fix failing test --- src/tests/cycleCount/performCC.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/tests/cycleCount/performCC.test.ts b/src/tests/cycleCount/performCC.test.ts index 3582b263..e452efa9 100644 --- a/src/tests/cycleCount/performCC.test.ts +++ b/src/tests/cycleCount/performCC.test.ts @@ -46,7 +46,7 @@ test.describe('Perform cycle count for item', () => { confirmToRecountStepPage, productShowPage, }) => { - test.setTimeout(240_000); + test.setTimeout(120_000); const USER = await mainUserService.getUser(); @@ -70,15 +70,14 @@ test.describe('Perform cycle count for item', () => { }); await test.step('Search for product on All Products tab', async () => { + await manageCycleCountPage.openAllProductsTab(); + await expect(async () => { - await manageCycleCountPage.goToPage(); - await manageCycleCountPage.isLoaded(); - await manageCycleCountPage.openAllProductsTab(); await manageCycleCountPage.searchProduct(productName); await expect(manageCycleCountPage.allProductsTable.rows).toHaveCount( 1 ); - }).toPass({ timeout: 180_000, intervals: [2000, 3000, 5000] }); + }).toPass({ timeout: 30_000, intervals: [2000, 3000, 5000] }); }); await test.step('Assert Last Counted date on All Products tab matches stock card', async () => { From 8993eb80e087f77612b0ec19157ff434866f218b Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Mon, 24 Aug 2026 11:20:15 +0200 Subject: [PATCH 7/8] improvements after review --- src/constants/applicationUrls.ts | 1 + .../manageCycleCount/ManageCycleCountPage.ts | 4 +- .../components/CountStepTable.ts | 13 ++-- .../components/RecountStepTable.ts | 12 ++-- .../productShow/tabs/InStockTabSection.ts | 12 ++-- src/tests/cycleCount/performCC.test.ts | 3 + ...performCCitemWithNegativeInventory.test.ts | 60 +++++++++++-------- 7 files changed, 67 insertions(+), 38 deletions(-) diff --git a/src/constants/applicationUrls.ts b/src/constants/applicationUrls.ts index 353a4387..52746c61 100644 --- a/src/constants/applicationUrls.ts +++ b/src/constants/applicationUrls.ts @@ -41,6 +41,7 @@ const INVENTORY_URL = { const CYCLE_COUNT_URL = { base: './inventory/cycleCount', + performCycleCount: () => `${CYCLE_COUNT_URL.base}?tab=TO_COUNT`, }; const INVENTORY_ITEM_URL = { diff --git a/src/pages/manageCycleCount/ManageCycleCountPage.ts b/src/pages/manageCycleCount/ManageCycleCountPage.ts index 54f14662..9cea97e3 100644 --- a/src/pages/manageCycleCount/ManageCycleCountPage.ts +++ b/src/pages/manageCycleCount/ManageCycleCountPage.ts @@ -22,7 +22,7 @@ class ManageCycleCountPage extends BasePageModel { } async goToPerformCycleCount() { - await this.page.goto(`${CYCLE_COUNT_URL.base}?tab=TO_COUNT`); + await this.page.goto(CYCLE_COUNT_URL.performCycleCount()); } async isLoaded() { @@ -71,6 +71,8 @@ class ManageCycleCountPage extends BasePageModel { async searchProduct(productName: string) { await this.searchInput.click(); + // select any existing text first, so pressSequentially replaces it + // instead of appending to it await this.page.keyboard.press('ControlOrMeta+A'); await this.searchInput.pressSequentially(productName, { delay: 20 }); await expect(this.searchInput).toHaveValue(productName); diff --git a/src/pages/manageCycleCount/components/CountStepTable.ts b/src/pages/manageCycleCount/components/CountStepTable.ts index 51e30071..16c54c3e 100644 --- a/src/pages/manageCycleCount/components/CountStepTable.ts +++ b/src/pages/manageCycleCount/components/CountStepTable.ts @@ -11,12 +11,16 @@ class CountStepTable extends BasePageModel { return this.table.getByRole('row'); } - row(index: number) { - return new Row(this.page, this.rows.nth(index)); + row(indexOrLocator: number | Locator) { + const locator = + typeof indexOrLocator === 'number' + ? this.rows.nth(indexOrLocator) + : indexOrLocator; + return new Row(this.page, locator); } - rowByBinLocation(binLocationName: string) { - return new Row(this.page, this.rows.filter({ hasText: binLocationName }).first()); + getRowsByBinLocation(binLocationName: string) { + return this.rows.filter({ hasText: binLocationName }); } } @@ -63,7 +67,6 @@ class Row extends BasePageModel { await this.quantityCountedInput.pressSequentially(value, { delay: 100, }); - await this.page.waitForTimeout(300); await this.page.keyboard.press('Tab'); await expect(this.quantityCountedInput).toHaveValue(value, { timeout: 2000, diff --git a/src/pages/manageCycleCount/components/RecountStepTable.ts b/src/pages/manageCycleCount/components/RecountStepTable.ts index f2977493..3d4ba150 100644 --- a/src/pages/manageCycleCount/components/RecountStepTable.ts +++ b/src/pages/manageCycleCount/components/RecountStepTable.ts @@ -11,12 +11,16 @@ class RecountStepTable extends BasePageModel { return this.table.getByRole('row'); } - row(index: number) { - return new Row(this.page, this.rows.nth(index)); + row(indexOrLocator: number | Locator) { + const locator = + typeof indexOrLocator === 'number' + ? this.rows.nth(indexOrLocator) + : indexOrLocator; + return new Row(this.page, locator); } - rowByBinLocation(binLocationName: string) { - return new Row(this.page, this.rows.filter({ hasText: binLocationName }).first()); + getRowsByBinLocation(binLocationName: string) { + return this.rows.filter({ hasText: binLocationName }); } } diff --git a/src/pages/product/productShow/tabs/InStockTabSection.ts b/src/pages/product/productShow/tabs/InStockTabSection.ts index ee8489c1..78f4b0f2 100644 --- a/src/pages/product/productShow/tabs/InStockTabSection.ts +++ b/src/pages/product/productShow/tabs/InStockTabSection.ts @@ -28,12 +28,16 @@ class InStockTabSection extends BasePageModel { return this.table.getByRole('row'); } - row(index: number) { - return new Row(this.page, this.rows.nth(index)); + row(indexOrLocator: number | Locator) { + const locator = + typeof indexOrLocator === 'number' + ? this.rows.nth(indexOrLocator) + : indexOrLocator; + return new Row(this.page, locator); } - rowByBinLocation(binLocationName: string) { - return new Row(this.page, this.rows.filter({ hasText: binLocationName }).first()); + getRowsByBinLocation(binLocationName: string) { + return this.rows.filter({ hasText: binLocationName }); } get stockTransferButton() { diff --git a/src/tests/cycleCount/performCC.test.ts b/src/tests/cycleCount/performCC.test.ts index e452efa9..6a7b505c 100644 --- a/src/tests/cycleCount/performCC.test.ts +++ b/src/tests/cycleCount/performCC.test.ts @@ -46,6 +46,9 @@ test.describe('Perform cycle count for item', () => { confirmToRecountStepPage, productShowPage, }) => { + // openAllProductsTab()'s own internal retry can take up to 45s on a slow + // tab load, and the search/filter step has its own 30s retry on top of + // that - comfortably over the default 60s test timeout in the worst case test.setTimeout(120_000); const USER = await mainUserService.getUser(); diff --git a/src/tests/cycleCount/performCCitemWithNegativeInventory.test.ts b/src/tests/cycleCount/performCCitemWithNegativeInventory.test.ts index b84b6c6f..50918a64 100644 --- a/src/tests/cycleCount/performCCitemWithNegativeInventory.test.ts +++ b/src/tests/cycleCount/performCCitemWithNegativeInventory.test.ts @@ -121,8 +121,9 @@ test.describe('Perform cycle count when item has negative inventory', () => { await productShowPage.inStockTab.click(); await productShowPage.inStockTabSection.isLoaded(); - const row = - productShowPage.inStockTabSection.rowByBinLocation(receivingBin); + const row = productShowPage.inStockTabSection.row( + productShowPage.inStockTabSection.getRowsByBinLocation(receivingBin).first() + ); await row.actionsButton.click(); await productShowPage.inStockTabSection.stockTransferButton.click(); await productShowPage.inStockTabSection.stockTransferDialog.isLoaded(); @@ -140,8 +141,9 @@ test.describe('Perform cycle count when item has negative inventory', () => { await productShowPage.inStockTab.click(); await productShowPage.inStockTabSection.isLoaded(); await expect( - productShowPage.inStockTabSection.rowByBinLocation('CC-BIN-1') - .binLocation + productShowPage.inStockTabSection.row( + productShowPage.inStockTabSection.getRowsByBinLocation('CC-BIN-1').first() + ).binLocation ).toContainText('CC-BIN-1'); }); @@ -168,8 +170,9 @@ test.describe('Perform cycle count when item has negative inventory', () => { await productShowPage.inStockTab.click(); await productShowPage.inStockTabSection.isLoaded(); - const row = - productShowPage.inStockTabSection.rowByBinLocation(receivingBin); + const row = productShowPage.inStockTabSection.row( + productShowPage.inStockTabSection.getRowsByBinLocation(receivingBin).first() + ); await expect(row.quantityOnHand).toHaveText('-20'); }); @@ -226,18 +229,21 @@ test.describe('Perform cycle count when item has negative inventory', () => { await test.step('Assert bin locations on count step and fill quantities', async () => { await expect(countStepPage.countStepTable.rows).toHaveCount(3); - const ccBin1Row = - countStepPage.countStepTable.rowByBinLocation('CC-BIN-1'); + const ccBin1Row = countStepPage.countStepTable.row( + countStepPage.countStepTable.getRowsByBinLocation('CC-BIN-1').first() + ); await expect(ccBin1Row.binLocation).toContainText('CC-BIN-1'); await ccBin1Row.fillQuantityCounted('0'); - const ccBin2Row = - countStepPage.countStepTable.rowByBinLocation('CC-BIN-2'); + const ccBin2Row = countStepPage.countStepTable.row( + countStepPage.countStepTable.getRowsByBinLocation('CC-BIN-2').first() + ); await expect(ccBin2Row.binLocation).toContainText('CC-BIN-2'); await ccBin2Row.fillQuantityCounted('10'); - const receivingBinRow = - countStepPage.countStepTable.rowByBinLocation(receivingBin); + const receivingBinRow = countStepPage.countStepTable.row( + countStepPage.countStepTable.getRowsByBinLocation(receivingBin).first() + ); await expect(receivingBinRow.binLocation).toContainText(receivingBin); await receivingBinRow.fillQuantityCounted('0'); }); @@ -286,16 +292,19 @@ test.describe('Perform cycle count when item has negative inventory', () => { await page.reload(); await countStepPage.isLoaded(); - const ccBin1Row = - countStepPage.countStepTable.rowByBinLocation('CC-BIN-1'); + const ccBin1Row = countStepPage.countStepTable.row( + countStepPage.countStepTable.getRowsByBinLocation('CC-BIN-1').first() + ); await expect(ccBin1Row.quantityCountedInput).toHaveValue('0'); - const ccBin2Row = - countStepPage.countStepTable.rowByBinLocation('CC-BIN-2'); + const ccBin2Row = countStepPage.countStepTable.row( + countStepPage.countStepTable.getRowsByBinLocation('CC-BIN-2').first() + ); await expect(ccBin2Row.quantityCountedInput).toHaveValue('10'); - const receivingBinRow = - countStepPage.countStepTable.rowByBinLocation(receivingBin); + const receivingBinRow = countStepPage.countStepTable.row( + countStepPage.countStepTable.getRowsByBinLocation(receivingBin).first() + ); await expect(receivingBinRow.quantityCountedInput).toHaveValue('0'); }).toPass({ timeout: 30_000, intervals: [2000, 3000, 5000] }); }); @@ -338,8 +347,9 @@ test.describe('Perform cycle count when item has negative inventory', () => { await test.step('Assert bin locations and quantity counted on recount step, then fill quantity recounted', async () => { await expect(recountStepPage.recountStepTable.rows).toHaveCount(3); - const ccBin1Row = - recountStepPage.recountStepTable.rowByBinLocation('CC-BIN-1'); + const ccBin1Row = recountStepPage.recountStepTable.row( + recountStepPage.recountStepTable.getRowsByBinLocation('CC-BIN-1').first() + ); await expect(ccBin1Row.binLocation).toContainText('CC-BIN-1'); await expect(ccBin1Row.quantityCounted).toHaveText('0'); await expect(ccBin1Row.countDifferenceValue).toHaveText('20'); @@ -350,8 +360,9 @@ test.describe('Perform cycle count when item has negative inventory', () => { await expect(ccBin1Row.countDifferenceIcon).toBeVisible(); await ccBin1Row.fillQuantityRecounted('0'); - const ccBin2Row = - recountStepPage.recountStepTable.rowByBinLocation('CC-BIN-2'); + const ccBin2Row = recountStepPage.recountStepTable.row( + recountStepPage.recountStepTable.getRowsByBinLocation('CC-BIN-2').first() + ); await expect(ccBin2Row.binLocation).toContainText('CC-BIN-2'); await expect(ccBin2Row.quantityCounted).toHaveText('10'); await expect(ccBin2Row.countDifferenceValue).toHaveText('EQUAL'); @@ -362,8 +373,9 @@ test.describe('Perform cycle count when item has negative inventory', () => { await expect(ccBin2Row.countDifferenceIcon).toBeHidden(); await ccBin2Row.fillQuantityRecounted('10'); - const receivingBinRow = - recountStepPage.recountStepTable.rowByBinLocation(receivingBin); + const receivingBinRow = recountStepPage.recountStepTable.row( + recountStepPage.recountStepTable.getRowsByBinLocation(receivingBin).first() + ); await expect(receivingBinRow.binLocation).toContainText(receivingBin); await expect(receivingBinRow.quantityCounted).toHaveText('0'); await expect(receivingBinRow.countDifferenceValue).toHaveText('20'); From ac7ccd43c85fa57824e7e5b65d332adfb009563e Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Tue, 25 Aug 2026 08:46:24 +0200 Subject: [PATCH 8/8] improvements after review part2 --- .../components/CountStepTable.ts | 13 +- .../components/RecountStepTable.ts | 13 +- .../productShow/tabs/InStockTabSection.ts | 13 +- ...performCCitemWithNegativeInventory.test.ts | 116 +++++++++++++----- src/utils/tableUtils.ts | 21 +++- 5 files changed, 119 insertions(+), 57 deletions(-) diff --git a/src/pages/manageCycleCount/components/CountStepTable.ts b/src/pages/manageCycleCount/components/CountStepTable.ts index 16c54c3e..c52787a9 100644 --- a/src/pages/manageCycleCount/components/CountStepTable.ts +++ b/src/pages/manageCycleCount/components/CountStepTable.ts @@ -1,6 +1,7 @@ import { expect, Locator, Page } from '@playwright/test'; import BasePageModel from '@/pages/BasePageModel'; +import { findRowIndexByText } from '@/utils/tableUtils'; class CountStepTable extends BasePageModel { get table() { @@ -11,16 +12,12 @@ class CountStepTable extends BasePageModel { return this.table.getByRole('row'); } - row(indexOrLocator: number | Locator) { - const locator = - typeof indexOrLocator === 'number' - ? this.rows.nth(indexOrLocator) - : indexOrLocator; - return new Row(this.page, locator); + row(index: number) { + return new Row(this.page, this.rows.nth(index)); } - getRowsByBinLocation(binLocationName: string) { - return this.rows.filter({ hasText: binLocationName }); + getRowIndexByBinLocation(binLocationName: string) { + return findRowIndexByText(this.rows, binLocationName); } } diff --git a/src/pages/manageCycleCount/components/RecountStepTable.ts b/src/pages/manageCycleCount/components/RecountStepTable.ts index 3d4ba150..ca32209b 100644 --- a/src/pages/manageCycleCount/components/RecountStepTable.ts +++ b/src/pages/manageCycleCount/components/RecountStepTable.ts @@ -1,6 +1,7 @@ import { expect, Locator, Page } from '@playwright/test'; import BasePageModel from '@/pages/BasePageModel'; +import { findRowIndexByText } from '@/utils/tableUtils'; class RecountStepTable extends BasePageModel { get table() { @@ -11,16 +12,12 @@ class RecountStepTable extends BasePageModel { return this.table.getByRole('row'); } - row(indexOrLocator: number | Locator) { - const locator = - typeof indexOrLocator === 'number' - ? this.rows.nth(indexOrLocator) - : indexOrLocator; - return new Row(this.page, locator); + row(index: number) { + return new Row(this.page, this.rows.nth(index)); } - getRowsByBinLocation(binLocationName: string) { - return this.rows.filter({ hasText: binLocationName }); + getRowIndexByBinLocation(binLocationName: string) { + return findRowIndexByText(this.rows, binLocationName); } } diff --git a/src/pages/product/productShow/tabs/InStockTabSection.ts b/src/pages/product/productShow/tabs/InStockTabSection.ts index 78f4b0f2..75cb84d5 100644 --- a/src/pages/product/productShow/tabs/InStockTabSection.ts +++ b/src/pages/product/productShow/tabs/InStockTabSection.ts @@ -3,6 +3,7 @@ import { expect, Locator, Page } from '@playwright/test'; import BasePageModel from '@/pages/BasePageModel'; import EditItemDialog from '@/pages/product/productShow/sections/components/EditItemDialog'; import StockTransferDialog from '@/pages/product/productShow/sections/components/StockTransferDialog'; +import { findRowIndexByText } from '@/utils/tableUtils'; class InStockTabSection extends BasePageModel { stockTransferDialog: StockTransferDialog; @@ -28,16 +29,12 @@ class InStockTabSection extends BasePageModel { return this.table.getByRole('row'); } - row(indexOrLocator: number | Locator) { - const locator = - typeof indexOrLocator === 'number' - ? this.rows.nth(indexOrLocator) - : indexOrLocator; - return new Row(this.page, locator); + row(index: number) { + return new Row(this.page, this.rows.nth(index)); } - getRowsByBinLocation(binLocationName: string) { - return this.rows.filter({ hasText: binLocationName }); + getRowIndexByBinLocation(binLocationName: string) { + return findRowIndexByText(this.rows, binLocationName); } get stockTransferButton() { diff --git a/src/tests/cycleCount/performCCitemWithNegativeInventory.test.ts b/src/tests/cycleCount/performCCitemWithNegativeInventory.test.ts index 50918a64..6f377f9a 100644 --- a/src/tests/cycleCount/performCCitemWithNegativeInventory.test.ts +++ b/src/tests/cycleCount/performCCitemWithNegativeInventory.test.ts @@ -118,32 +118,70 @@ test.describe('Perform cycle count when item has negative inventory', () => { await test.step('Perform Stock Transfer of inventory from receiving bin to CC-BIN-1', async () => { const ccDepot = await ccDepotService.getLocation(); - await productShowPage.inStockTab.click(); - await productShowPage.inStockTabSection.isLoaded(); + // known app bug: selecting an option in the "chosen" location dropdown can + // trigger an unintended form submit that navigates away from this dialog + // back to the product's Stock history tab. Retry the whole flow from the + // top if that happens; checking for CC-BIN-1 upfront makes retries + // idempotent in case the stray submit actually went through. + await expect(async () => { + await productShowPage.inStockTab.click(); + await productShowPage.inStockTabSection.isLoaded(); + + const alreadyTransferred = await productShowPage.inStockTabSection.rows + .filter({ hasText: 'CC-BIN-1' }) + .count(); + if (alreadyTransferred > 0) { + return; + } - const row = productShowPage.inStockTabSection.row( - productShowPage.inStockTabSection.getRowsByBinLocation(receivingBin).first() - ); - await row.actionsButton.click(); - await productShowPage.inStockTabSection.stockTransferButton.click(); - await productShowPage.inStockTabSection.stockTransferDialog.isLoaded(); + const receivingBinRowIndex = + await productShowPage.inStockTabSection.getRowIndexByBinLocation( + receivingBin + ); + const row = productShowPage.inStockTabSection.row(receivingBinRowIndex); + await row.actionsButton.click(); + await productShowPage.inStockTabSection.stockTransferButton.click(); + await productShowPage.inStockTabSection.stockTransferDialog.isLoaded(); + + await productShowPage.inStockTabSection.stockTransferDialog.locationSelect.click(); + // selecting a location triggers an AJAX call (template=transferStock) + // that replaces the whole dialog's markup with fresh bin-location + // options; wait for it to settle before opening the next dropdown, to + // reduce the odds of racing that replacement (bounded - never blocks + // the test on its own if the response doesn't match for some reason) + await Promise.all([ + page + .waitForResponse((res) => res.url().includes('template=transferStock'), { + timeout: 5000, + }) + .catch(() => undefined), + productShowPage.inStockTabSection.stockTransferDialog.selectLocation( + ccDepot.name + ), + ]); + await productShowPage.inStockTabSection.stockTransferDialog.binLocationSelect.click(); + await productShowPage.inStockTabSection.stockTransferDialog.selectLocation( + 'CC-BIN-1' + ); + await productShowPage.inStockTabSection.stockTransferDialog.transferStockButton.click(); - await productShowPage.inStockTabSection.stockTransferDialog.locationSelect.click(); - await productShowPage.inStockTabSection.stockTransferDialog.selectLocation( - ccDepot.name - ); - await productShowPage.inStockTabSection.stockTransferDialog.binLocationSelect.click(); - await productShowPage.inStockTabSection.stockTransferDialog.selectLocation( - 'CC-BIN-1' - ); - await productShowPage.inStockTabSection.stockTransferDialog.transferStockButton.click(); + await productShowPage.inStockTab.click(); + await productShowPage.inStockTabSection.isLoaded(); + await expect( + productShowPage.inStockTabSection.rows.filter({ + hasText: 'CC-BIN-1', + }) + ).toHaveCount(1); + }).toPass({ timeout: 60_000, intervals: [3000, 5000, 8000] }); await productShowPage.inStockTab.click(); await productShowPage.inStockTabSection.isLoaded(); + const ccBin1RowIndex = + await productShowPage.inStockTabSection.getRowIndexByBinLocation( + 'CC-BIN-1' + ); await expect( - productShowPage.inStockTabSection.row( - productShowPage.inStockTabSection.getRowsByBinLocation('CC-BIN-1').first() - ).binLocation + productShowPage.inStockTabSection.row(ccBin1RowIndex).binLocation ).toContainText('CC-BIN-1'); }); @@ -170,9 +208,11 @@ test.describe('Perform cycle count when item has negative inventory', () => { await productShowPage.inStockTab.click(); await productShowPage.inStockTabSection.isLoaded(); - const row = productShowPage.inStockTabSection.row( - productShowPage.inStockTabSection.getRowsByBinLocation(receivingBin).first() - ); + const receivingBinRowIndex = + await productShowPage.inStockTabSection.getRowIndexByBinLocation( + receivingBin + ); + const row = productShowPage.inStockTabSection.row(receivingBinRowIndex); await expect(row.quantityOnHand).toHaveText('-20'); }); @@ -230,19 +270,19 @@ test.describe('Perform cycle count when item has negative inventory', () => { await expect(countStepPage.countStepTable.rows).toHaveCount(3); const ccBin1Row = countStepPage.countStepTable.row( - countStepPage.countStepTable.getRowsByBinLocation('CC-BIN-1').first() + await countStepPage.countStepTable.getRowIndexByBinLocation('CC-BIN-1') ); await expect(ccBin1Row.binLocation).toContainText('CC-BIN-1'); await ccBin1Row.fillQuantityCounted('0'); const ccBin2Row = countStepPage.countStepTable.row( - countStepPage.countStepTable.getRowsByBinLocation('CC-BIN-2').first() + await countStepPage.countStepTable.getRowIndexByBinLocation('CC-BIN-2') ); await expect(ccBin2Row.binLocation).toContainText('CC-BIN-2'); await ccBin2Row.fillQuantityCounted('10'); const receivingBinRow = countStepPage.countStepTable.row( - countStepPage.countStepTable.getRowsByBinLocation(receivingBin).first() + await countStepPage.countStepTable.getRowIndexByBinLocation(receivingBin) ); await expect(receivingBinRow.binLocation).toContainText(receivingBin); await receivingBinRow.fillQuantityCounted('0'); @@ -293,17 +333,23 @@ test.describe('Perform cycle count when item has negative inventory', () => { await countStepPage.isLoaded(); const ccBin1Row = countStepPage.countStepTable.row( - countStepPage.countStepTable.getRowsByBinLocation('CC-BIN-1').first() + await countStepPage.countStepTable.getRowIndexByBinLocation( + 'CC-BIN-1' + ) ); await expect(ccBin1Row.quantityCountedInput).toHaveValue('0'); const ccBin2Row = countStepPage.countStepTable.row( - countStepPage.countStepTable.getRowsByBinLocation('CC-BIN-2').first() + await countStepPage.countStepTable.getRowIndexByBinLocation( + 'CC-BIN-2' + ) ); await expect(ccBin2Row.quantityCountedInput).toHaveValue('10'); const receivingBinRow = countStepPage.countStepTable.row( - countStepPage.countStepTable.getRowsByBinLocation(receivingBin).first() + await countStepPage.countStepTable.getRowIndexByBinLocation( + receivingBin + ) ); await expect(receivingBinRow.quantityCountedInput).toHaveValue('0'); }).toPass({ timeout: 30_000, intervals: [2000, 3000, 5000] }); @@ -348,7 +394,9 @@ test.describe('Perform cycle count when item has negative inventory', () => { await expect(recountStepPage.recountStepTable.rows).toHaveCount(3); const ccBin1Row = recountStepPage.recountStepTable.row( - recountStepPage.recountStepTable.getRowsByBinLocation('CC-BIN-1').first() + await recountStepPage.recountStepTable.getRowIndexByBinLocation( + 'CC-BIN-1' + ) ); await expect(ccBin1Row.binLocation).toContainText('CC-BIN-1'); await expect(ccBin1Row.quantityCounted).toHaveText('0'); @@ -361,7 +409,9 @@ test.describe('Perform cycle count when item has negative inventory', () => { await ccBin1Row.fillQuantityRecounted('0'); const ccBin2Row = recountStepPage.recountStepTable.row( - recountStepPage.recountStepTable.getRowsByBinLocation('CC-BIN-2').first() + await recountStepPage.recountStepTable.getRowIndexByBinLocation( + 'CC-BIN-2' + ) ); await expect(ccBin2Row.binLocation).toContainText('CC-BIN-2'); await expect(ccBin2Row.quantityCounted).toHaveText('10'); @@ -374,7 +424,9 @@ test.describe('Perform cycle count when item has negative inventory', () => { await ccBin2Row.fillQuantityRecounted('10'); const receivingBinRow = recountStepPage.recountStepTable.row( - recountStepPage.recountStepTable.getRowsByBinLocation(receivingBin).first() + await recountStepPage.recountStepTable.getRowIndexByBinLocation( + receivingBin + ) ); await expect(receivingBinRow.binLocation).toContainText(receivingBin); await expect(receivingBinRow.quantityCounted).toHaveText('0'); diff --git a/src/utils/tableUtils.ts b/src/utils/tableUtils.ts index c6f0c0b0..8b30a006 100644 --- a/src/utils/tableUtils.ts +++ b/src/utils/tableUtils.ts @@ -1,4 +1,4 @@ -import { Locator } from '@playwright/test'; +import { expect, Locator } from '@playwright/test'; export const captureRowValues = async ( rowCount: number, @@ -16,3 +16,22 @@ export const captureRowValues = async ( .map((v) => v?.trim()) .filter((v): v is string => Boolean(v)); }; + +// the row list can be briefly stale/empty right after a reload, so retry +// the scan the same way a locator-based lookup would implicitly do via +// expect()'s auto-retrying assertions +export const findRowIndexByText = async ( + rows: Locator, + text: string, + { timeout = 10000 } = {} +): Promise => { + let matchedIndex = -1; + await expect(async () => { + const texts = await rows.allTextContents(); + matchedIndex = texts.findIndex((rowText) => rowText.includes(text)); + if (matchedIndex === -1) { + throw new Error(`Row with text "${text}" not found`); + } + }).toPass({ timeout }); + return matchedIndex; +};