From 41e2e74f3b7778ccda4a17318eded3d21ca1d82c Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Fri, 13 Feb 2026 13:05:30 +0100 Subject: [PATCH 1/5] add new pages --- src/fixtures/fixtures.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/fixtures/fixtures.ts b/src/fixtures/fixtures.ts index 3c7cf0f..ede5d0b 100644 --- a/src/fixtures/fixtures.ts +++ b/src/fixtures/fixtures.ts @@ -8,10 +8,7 @@ import StockMovementService from '@/api/StockMovementService'; import ImpersonateBanner from '@/components/ImpersonateBanner'; import LocationChooser from '@/components/LocationChooser'; import Navbar from '@/components/Navbar'; -import AppConfig, { - LOCATION_KEY, - USER_KEY, -} from '@/config/AppConfig'; +import AppConfig, { LOCATION_KEY, USER_KEY } from '@/config/AppConfig'; import CreateInbound from '@/pages/inbound/create/CreateInboundPage'; import InboundListPage from '@/pages/inbound/list/InboundListPage'; import CreateInvoicePage from '@/pages/invoice/CreateInvoicePage'; @@ -28,6 +25,7 @@ import OrganizationListPage from '@/pages/oranization/OrganizationListPage'; import CreatePersonPage from '@/pages/people/CreatePersonPage'; import PersonsListPage from '@/pages/people/PersonsListPage'; import CreateProductPage from '@/pages/product/CreateProductPage'; +import ProductEditPage from '@/pages/product/productEdit/ProductEditPage'; import ProductShowPage from '@/pages/product/productShow/ProductShowPage'; import CreatePutawayPage from '@/pages/putaway/CreatePutawayPage'; import PutawayListPage from '@/pages/putaway/list/PutawayListPage'; @@ -72,6 +70,7 @@ type Fixtures = { transactionListPage: TransactionListPage; oldViewShipmentPage: OldViewShipmentPage; putawayListPage: PutawayListPage; + productEditPage: ProductEditPage; // COMPONENTS navbar: Navbar; locationChooser: LocationChooser; @@ -145,6 +144,7 @@ export const test = baseTest.extend({ oldViewShipmentPage: async ({ page }, use) => use(new OldViewShipmentPage(page)), putawayListPage: async ({ page }, use) => use(new PutawayListPage(page)), + productEditPage: async ({ page }, use) => use(new ProductEditPage(page)), // COMPONENTS navbar: async ({ page }, use) => use(new Navbar(page)), locationChooser: async ({ page }, use) => use(new LocationChooser(page)), @@ -179,8 +179,7 @@ export const test = baseTest.extend({ internalLocation2Service: async ({ page }, use) => use(new LocationData(LOCATION_KEY.BIN_LOCATION2, page.request)), // PRODUCTS - productService: async ({ page }, use) => - use(new ProductData(page.request)), + productService: async ({ page }, use) => use(new ProductData(page.request)), // USERS mainUserService: async ({ page }, use) => use(new UserData(USER_KEY.MAIN, page.request)), From f2f2b0db7636c8c08362a14f690ab1849755da9e Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Fri, 13 Feb 2026 13:06:46 +0100 Subject: [PATCH 2/5] add new elements to product view page --- src/pages/product/productShow/ProductShowPage.ts | 4 ++++ src/pages/product/productShow/tabs/InStockTabSection.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/pages/product/productShow/ProductShowPage.ts b/src/pages/product/productShow/ProductShowPage.ts index 960f33b..2f8b695 100644 --- a/src/pages/product/productShow/ProductShowPage.ts +++ b/src/pages/product/productShow/ProductShowPage.ts @@ -34,6 +34,10 @@ class ProductShowPage extends BasePageModel { get stockHistoryTab() { return this.page.getByRole('link', { name: 'Stock History' }); } + + get editProductkButton() { + return this.page.getByRole('link', { name: 'Edit Product' }); + } } export default ProductShowPage; diff --git a/src/pages/product/productShow/tabs/InStockTabSection.ts b/src/pages/product/productShow/tabs/InStockTabSection.ts index 2241788..0311401 100644 --- a/src/pages/product/productShow/tabs/InStockTabSection.ts +++ b/src/pages/product/productShow/tabs/InStockTabSection.ts @@ -44,6 +44,10 @@ class Row extends BasePageModel { .getByRole('link'); } + get defaultBinLocation() { + return this.row.locator('.line').getByText('Default'); + } + get zoneLocation() { return this.row.locator('.line').locator('.line-base').getByRole('link'); } From 3b8bfbfcf69e71759f24347f585b056754998513 Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Fri, 13 Feb 2026 13:07:41 +0100 Subject: [PATCH 3/5] add elements to putaway pages --- src/pages/putaway/components/StartPutawayTable.ts | 4 ++++ src/pages/putaway/steps/CompleteStep.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/pages/putaway/components/StartPutawayTable.ts b/src/pages/putaway/components/StartPutawayTable.ts index 3f4acf7..40339c5 100644 --- a/src/pages/putaway/components/StartPutawayTable.ts +++ b/src/pages/putaway/components/StartPutawayTable.ts @@ -59,6 +59,10 @@ class Row extends BasePageModel { return this.row.getByTestId('cell-0-currentBin').getByText(currentBin); } + getPreferredBin(rowIndex: number) { + return this.row.getByTestId(`cell-${rowIndex}-preferredBin`); + } + get quantityField() { return this.row.getByTestId('cell-0-quantity').getByRole('spinbutton'); } diff --git a/src/pages/putaway/steps/CompleteStep.ts b/src/pages/putaway/steps/CompleteStep.ts index e60b96e..a907e03 100644 --- a/src/pages/putaway/steps/CompleteStep.ts +++ b/src/pages/putaway/steps/CompleteStep.ts @@ -19,6 +19,18 @@ class CompleteStep extends BasePageModel { get completePutawayButton() { return this.page.getByTestId('complete-putaway-button').nth(1); } + + get confirmPutawayDialog() { + return this.page.locator('.react-confirm-alert'); + } + + get yesButtonOnConfirmPutawayDialog() { + return this.confirmPutawayDialog.getByRole('button', { name: 'Yes' }); + } + + get noButtonOnConfirmPutawayDialog() { + return this.confirmPutawayDialog.getByRole('button', { name: 'No' }); + } } export default CompleteStep; From ac52c7bc4e2a026e9f8b92c748b408697047fd90 Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Fri, 13 Feb 2026 13:08:23 +0100 Subject: [PATCH 4/5] add product edit page with elements --- .../product/productEdit/ProductEditPage.ts | 27 +++++++++++ .../components/createStockLevelModal.ts | 45 +++++++++++++++++++ .../tabs/InventoryLevelsTabSection.ts | 45 +++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 src/pages/product/productEdit/ProductEditPage.ts create mode 100644 src/pages/product/productEdit/components/createStockLevelModal.ts create mode 100644 src/pages/product/productEdit/tabs/InventoryLevelsTabSection.ts diff --git a/src/pages/product/productEdit/ProductEditPage.ts b/src/pages/product/productEdit/ProductEditPage.ts new file mode 100644 index 0000000..acc12dd --- /dev/null +++ b/src/pages/product/productEdit/ProductEditPage.ts @@ -0,0 +1,27 @@ +import { Page } from '@playwright/test'; + +import BasePageModel from '@/pages/BasePageModel'; +import InventoryLevelsTabSection from '@/pages/product/productEdit/tabs/InventoryLevelsTabSection'; + +class ProductEditPage extends BasePageModel { + inventoryLevelsTabSection: InventoryLevelsTabSection; + + constructor(page: Page) { + super(page); + this.inventoryLevelsTabSection = new InventoryLevelsTabSection(page); + } + + async goToPage(id: string) { + await this.page.goto(`./product/edit/${id}`); + } + + get detailskTab() { + return this.page.getByRole('link', { name: 'Details' }); + } + + get inventoryLevelsTab() { + return this.page.getByRole('link', { name: 'Inventory Levels' }); + } +} + +export default ProductEditPage; diff --git a/src/pages/product/productEdit/components/createStockLevelModal.ts b/src/pages/product/productEdit/components/createStockLevelModal.ts new file mode 100644 index 0000000..f6a2795 --- /dev/null +++ b/src/pages/product/productEdit/components/createStockLevelModal.ts @@ -0,0 +1,45 @@ +import { Page } from '@playwright/test'; + +import BasePageModel from '@/pages/BasePageModel'; + +class CreateStockLevelModal extends BasePageModel { + constructor(page: Page) { + super(page); + } + + get modal() { + return this.page.getByRole('dialog'); + } + + get receivingTab() { + return this.page.getByRole('link', { name: 'Receiving' }); + } + + get defaultPutawayLocation() { + return this.modal.locator( + 'select[name="preferredBinLocation"] + .chosen-container' + ); + } + + getDefaultPutawayLocation(putawayLocation: string) { + return this.defaultPutawayLocation + .locator('.chosen-results') + .getByRole('listitem') + .getByText(putawayLocation, { exact: true }); + } + + get createButton() { + return this.modal.getByRole('button', { name: 'Create' }); + } + + get deleteInventoryLevelButton() { + return this.modal.getByRole('link', { name: 'Delete' }); + } + + async clickDeleteInventoryLevel() { + this.page.once('dialog', (dialog) => dialog.accept()); + await this.deleteInventoryLevelButton.click(); + } +} + +export default CreateStockLevelModal; diff --git a/src/pages/product/productEdit/tabs/InventoryLevelsTabSection.ts b/src/pages/product/productEdit/tabs/InventoryLevelsTabSection.ts new file mode 100644 index 0000000..30b2f50 --- /dev/null +++ b/src/pages/product/productEdit/tabs/InventoryLevelsTabSection.ts @@ -0,0 +1,45 @@ +import { Locator, Page } from '@playwright/test'; + +import BasePageModel from '@/pages/BasePageModel'; +import CreateStockLevelModal from '@/pages/product/productEdit/components/createStockLevelModal'; + +class InventoryLevelsTabSection extends BasePageModel { + createStockLevelModal: CreateStockLevelModal; + + constructor(page: Page) { + super(page); + this.createStockLevelModal = new CreateStockLevelModal(page); + } + + get createStockLevelButton() { + return this.page.getByRole('link', { name: 'Create stock level' }); + } + + get table() { + return this.page.locator('#ui-tabs-3').getByRole('table'); + } + + get rows() { + return this.table.getByRole('row'); + } + + row(index: number) { + return new Row(this.page, this.rows.nth(index)); + } +} + +class Row extends BasePageModel { + row: Locator; + constructor(page: Page, row: Locator) { + super(page); + this.row = row; + } + + get editInventoryLevelButton() { + return this.row + .getByRole('cell') + .locator('a.btn-show-dialog', { hasText: 'Edit' }); + } +} + +export default InventoryLevelsTabSection; From 57144bef0b47452f2a9a7eb58e4bf043127d74fb Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Fri, 13 Feb 2026 13:08:58 +0100 Subject: [PATCH 5/5] add tests for putaway to preferred and default bins --- .../putaway/putawayToPreferredBin.test.ts | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 src/tests/putaway/putawayToPreferredBin.test.ts diff --git a/src/tests/putaway/putawayToPreferredBin.test.ts b/src/tests/putaway/putawayToPreferredBin.test.ts new file mode 100644 index 0000000..b4998c3 --- /dev/null +++ b/src/tests/putaway/putawayToPreferredBin.test.ts @@ -0,0 +1,304 @@ +import AppConfig from '@/config/AppConfig'; +import { ShipmentType } from '@/constants/ShipmentType'; +import { expect, test } from '@/fixtures/fixtures'; +import { StockMovementResponse } from '@/types'; +import { getShipmentId, getShipmentItemId } from '@/utils/shipmentUtils'; + +test.describe('Putaway to preferred bin and default bin', () => { + let STOCK_MOVEMENT: StockMovementResponse; + + test.beforeEach( + async ({ + supplierLocationService, + stockMovementService, + productService, + receivingService, + productShowPage, + productEditPage, + internalLocationService, + }) => { + const supplierLocation = await supplierLocationService.getLocation(); + STOCK_MOVEMENT = await stockMovementService.createInbound({ + originId: supplierLocation.id, + }); + + productService.setProduct('5'); + const product = await productService.getProduct(); + productService.setProduct('4'); + const product2 = await productService.getProduct(); + + await stockMovementService.addItemsToInboundStockMovement( + STOCK_MOVEMENT.id, + [ + { productId: product.id, quantity: 10 }, + { productId: product2.id, quantity: 10 }, + ] + ); + + await stockMovementService.sendInboundStockMovement(STOCK_MOVEMENT.id, { + shipmentType: ShipmentType.AIR, + }); + + const { data: stockMovement } = + await stockMovementService.getStockMovement(STOCK_MOVEMENT.id); + const shipmentId = getShipmentId(stockMovement); + const { data: receipt } = await receivingService.getReceipt(shipmentId); + const receivingBin = + AppConfig.instance.receivingBinPrefix + STOCK_MOVEMENT.identifier; + + await receivingService.createReceivingBin(shipmentId, receipt); + + await receivingService.updateReceivingItems(shipmentId, [ + { + shipmentItemId: getShipmentItemId(receipt, 0, 0), + quantityReceiving: 10, + binLocationName: receivingBin, + }, + { + shipmentItemId: getShipmentItemId(receipt, 0, 1), + quantityReceiving: 10, + binLocationName: receivingBin, + }, + ]); + await receivingService.completeReceipt(shipmentId); + + await productShowPage.goToPage(product2.id); + await productShowPage.editProductkButton.click(); + await productEditPage.inventoryLevelsTab.click(); + await productEditPage.inventoryLevelsTabSection.createStockLevelButton.click(); + await productEditPage.inventoryLevelsTabSection.createStockLevelModal.receivingTab.click(); + const internalLocation = await internalLocationService.getLocation(); + await productEditPage.inventoryLevelsTabSection.createStockLevelModal.defaultPutawayLocation.click(); + await productEditPage.inventoryLevelsTabSection.createStockLevelModal + .getDefaultPutawayLocation(internalLocation.name) + .click(); + await productEditPage.inventoryLevelsTabSection.createStockLevelModal.createButton.click(); + } + ); + + test.afterEach( + async ({ + stockMovementShowPage, + stockMovementService, + navbar, + transactionListPage, + oldViewShipmentPage, + productService, + productShowPage, + productEditPage, + }) => { + 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(); + await stockMovementShowPage.isLoaded(); + await stockMovementShowPage.rollbackButton.click(); + + await stockMovementService.deleteStockMovement(STOCK_MOVEMENT.id); + productService.setProduct('4'); + const product2 = await productService.getProduct(); + await productShowPage.goToPage(product2.id); + await productShowPage.editProductkButton.click(); + await productEditPage.inventoryLevelsTab.click(); + await productEditPage.inventoryLevelsTabSection + .row(1) + .editInventoryLevelButton.click(); + await expect( + productEditPage.inventoryLevelsTabSection.table + ).toBeVisible(); + await productEditPage.inventoryLevelsTabSection.createStockLevelModal.clickDeleteInventoryLevel(); + } + ); + + test('Create putaway for product with preferred bin assigned and without it', async ({ + stockMovementShowPage, + navbar, + createPutawayPage, + internalLocationService, + productShowPage, + putawayDetailsPage, + productService, + }) => { + const receivingBin = + AppConfig.instance.receivingBinPrefix + STOCK_MOVEMENT.identifier; + productService.setProduct('5'); + const product = await productService.getProduct(); + productService.setProduct('4'); + const product2 = await productService.getProduct(); + const internalLocation = await internalLocationService.getLocation(); + + await test.step('Go to create putaway page', async () => { + await stockMovementShowPage.goToPage(STOCK_MOVEMENT.id); + await stockMovementShowPage.isLoaded(); + await navbar.profileButton.click(); + await navbar.refreshCachesButton.click(); + await navbar.inbound.click(); + await navbar.createPutaway.click(); + await createPutawayPage.isLoaded(); + }); + + await test.step('Start putaway', async () => { + await createPutawayPage.table + .row(0) + .getExpandBinLocation(receivingBin) + .click(); + await createPutawayPage.table.row(1).checkbox.click(); + await createPutawayPage.table.row(2).checkbox.click(); + await createPutawayPage.startPutawayButton.click(); + await createPutawayPage.startStep.isLoaded(); + }); + + await test.step('Assert assignment of preferred and putaway bins', async () => { + await expect( + createPutawayPage.startStep.table.row(1).getPreferredBin(0) + ).toHaveText(''); + await expect( + createPutawayPage.startStep.table.row(2).getPreferredBin(1) + ).toContainText(internalLocation.name); + await expect( + createPutawayPage.startStep.table.row(1).putawayBinSelect + ).toBeEmpty(); + await expect( + createPutawayPage.startStep.table.row(2).putawayBinSelect + ).toContainText(internalLocation.name); + }); + + await test.step('Assert confirm complete putaway dialog when empty putaway bin', async () => { + await createPutawayPage.startStep.nextButton.click(); + await createPutawayPage.completeStep.isLoaded(); + await createPutawayPage.completeStep.completePutawayButton.click(); + await expect( + createPutawayPage.completeStep.confirmPutawayDialog + ).toBeVisible(); + await expect( + createPutawayPage.completeStep.confirmPutawayDialog + ).toContainText( + 'Are you sure you want to putaway? There are some lines with empty bin locations.' + ); + await createPutawayPage.completeStep.noButtonOnConfirmPutawayDialog.click(); + await createPutawayPage.completeStep.isLoaded(); + }); + + await test.step('Complete putaway', async () => { + await createPutawayPage.completeStep.completePutawayButton.click(); + await expect( + createPutawayPage.completeStep.confirmPutawayDialog + ).toBeVisible(); + await createPutawayPage.completeStep.yesButtonOnConfirmPutawayDialog.click(); + }); + + await test.step('Assert completing putaway', async () => { + await putawayDetailsPage.isLoaded(); + await expect(putawayDetailsPage.statusTag).toHaveText('Completed'); + }); + + await test.step('Assert putaway bin on stock card', async () => { + await productShowPage.goToPage(product2.id); + await productShowPage.inStockTab.click(); + await productShowPage.inStockTabSection.isLoaded(); + await expect( + productShowPage.inStockTabSection.row(2).binLocation + ).toHaveText(internalLocation.name); + await productShowPage.goToPage(product.id); + await productShowPage.inStockTab.click(); + await productShowPage.inStockTabSection.isLoaded(); + await expect( + productShowPage.inStockTabSection.row(1).defaultBinLocation + ).toBeVisible(); + }); + }); + + test('Edit putaway bin when preferred bin assigned automatically', async ({ + stockMovementShowPage, + navbar, + createPutawayPage, + internalLocationService, + internalLocation2Service, + productShowPage, + putawayDetailsPage, + productService, + }) => { + const receivingBin = + AppConfig.instance.receivingBinPrefix + STOCK_MOVEMENT.identifier; + productService.setProduct('4'); + const product2 = await productService.getProduct(); + const internalLocation = await internalLocationService.getLocation(); + const internalLocation2 = await internalLocation2Service.getLocation(); + + await test.step('Go to create putaway page', async () => { + await stockMovementShowPage.goToPage(STOCK_MOVEMENT.id); + await stockMovementShowPage.isLoaded(); + await navbar.profileButton.click(); + await navbar.refreshCachesButton.click(); + await navbar.inbound.click(); + await navbar.createPutaway.click(); + await createPutawayPage.isLoaded(); + }); + + await test.step('Start putaway', async () => { + await createPutawayPage.table + .row(0) + .getExpandBinLocation(receivingBin) + .click(); + await createPutawayPage.table.row(2).checkbox.click(); + await createPutawayPage.startPutawayButton.click(); + await createPutawayPage.startStep.isLoaded(); + }); + + await test.step('Assert assignment of preferred and putaway bins', async () => { + await expect( + createPutawayPage.startStep.table.row(1).getPreferredBin(0) + ).toContainText(internalLocation.name); + await expect( + createPutawayPage.startStep.table.row(1).putawayBinSelect + ).toContainText(internalLocation.name); + }); + + await test.step('Edit putaway bin', async () => { + await createPutawayPage.startStep.table.row(1).putawayBinSelect.click(); + await createPutawayPage.startStep.table + .row(1) + .getPutawayBin(internalLocation2.name) + .click(); + }); + + await test.step('Go to next page and assert edited putaway bin', async () => { + await createPutawayPage.startStep.nextButton.click(); + await createPutawayPage.completeStep.isLoaded(); + await expect( + createPutawayPage.completeStep.table.row(2).getputawayBin(0) + ).toContainText(internalLocation2.name); + }); + + await test.step('Complete putaway', async () => { + await createPutawayPage.completeStep.completePutawayButton.click(); + await putawayDetailsPage.isLoaded(); + await expect(putawayDetailsPage.statusTag).toHaveText('Completed'); + }); + + await test.step('Assert content of items status table', async () => { + await putawayDetailsPage.itemStatusTab.click(); + await expect( + putawayDetailsPage.itemStatusTable.row(1).itemStatus + ).toHaveText('COMPLETED'); + await expect( + putawayDetailsPage.itemStatusTable.row(1).originBin + ).toHaveText(receivingBin); + await expect( + putawayDetailsPage.itemStatusTable.row(1).destinationBin + ).toHaveText(internalLocation2.name); + }); + + await test.step('Assert putaway bin on stock card', async () => { + await productShowPage.goToPage(product2.id); + await productShowPage.inStockTab.click(); + await productShowPage.inStockTabSection.isLoaded(); + await expect( + productShowPage.inStockTabSection.row(2).binLocation + ).toHaveText(internalLocation2.name); + }); + }); +});