diff --git a/src/fixtures/fixtures.ts b/src/fixtures/fixtures.ts index ede5d0b..ff883b5 100644 --- a/src/fixtures/fixtures.ts +++ b/src/fixtures/fixtures.ts @@ -33,6 +33,7 @@ import PutawayDetailsPage from '@/pages/putaway/putawayDetails/PutawayDetailsPag import ReceivingPage from '@/pages/receiving/ReceivingPage'; import OldViewShipmentPage from '@/pages/stockMovementShow/OldViewShipmentPage'; import StockMovementShowPage from '@/pages/stockMovementShow/StockMovementShowPage'; +import EditTransactionPage from '@/pages/transactions/EditTransactionPgage'; import TransactionListPage from '@/pages/transactions/TransactionListPage'; import CreateUserPage from '@/pages/user/CreateUserPage'; import EditUserPage from '@/pages/user/editUser/EditUserPage'; @@ -71,6 +72,7 @@ type Fixtures = { oldViewShipmentPage: OldViewShipmentPage; putawayListPage: PutawayListPage; productEditPage: ProductEditPage; + editTransactionPage: EditTransactionPage; // COMPONENTS navbar: Navbar; locationChooser: LocationChooser; @@ -145,6 +147,8 @@ export const test = baseTest.extend({ use(new OldViewShipmentPage(page)), putawayListPage: async ({ page }, use) => use(new PutawayListPage(page)), productEditPage: async ({ page }, use) => use(new ProductEditPage(page)), + editTransactionPage: async ({ page }, use) => + use(new EditTransactionPage(page)), // COMPONENTS navbar: async ({ page }, use) => use(new Navbar(page)), locationChooser: async ({ page }, use) => use(new LocationChooser(page)), diff --git a/src/pages/product/productShow/sections/RecordStockSection.ts b/src/pages/product/productShow/sections/RecordStockSection.ts index 3bb3142..811ddfd 100644 --- a/src/pages/product/productShow/sections/RecordStockSection.ts +++ b/src/pages/product/productShow/sections/RecordStockSection.ts @@ -2,13 +2,16 @@ import { Page } from '@playwright/test'; import BasePageModel from '@/pages/BasePageModel'; import LineItemsTable from '@/pages/product/productShow/sections/components/LineItemsTable'; +import RecordStockTable from '@/pages/product/productShow/sections/components/RecortStockTable'; class RecordStockSection extends BasePageModel { lineItemsTable: LineItemsTable; + recordStockTable: RecordStockTable; constructor(page: Page) { super(page); this.lineItemsTable = new LineItemsTable(page); + this.recordStockTable = new RecordStockTable(page); } } diff --git a/src/pages/product/productShow/sections/components/RecortStockTable.ts b/src/pages/product/productShow/sections/components/RecortStockTable.ts new file mode 100644 index 0000000..c26daf8 --- /dev/null +++ b/src/pages/product/productShow/sections/components/RecortStockTable.ts @@ -0,0 +1,35 @@ +import { Locator, Page } from '@playwright/test'; + +import BasePageModel from '@/pages/BasePageModel'; + +class RecordStockTable extends BasePageModel { + constructor(page: Page) { + super(page); + } + + get section() { + return this.page.getByRole('region', { name: 'Record Stock' }); + } + + get table() { + return this.section.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; + } +} + +export default RecordStockTable; diff --git a/src/pages/putaway/steps/CompleteStep.ts b/src/pages/putaway/steps/CompleteStep.ts index e4a838e..c7bc321 100644 --- a/src/pages/putaway/steps/CompleteStep.ts +++ b/src/pages/putaway/steps/CompleteStep.ts @@ -41,6 +41,14 @@ class CompleteStep extends BasePageModel { get editButton() { return this.page.getByTestId('edit-button').first(); } + + get validationOnQtyInReceivingBin() { + return this.page + .locator('[class*="alert"]') + .getByText( + /Quantity available 0 is less than quantity to putaway \d+ for product/ + ); + } } export default CompleteStep; diff --git a/src/pages/transactions/EditTransactionPgage.ts b/src/pages/transactions/EditTransactionPgage.ts new file mode 100644 index 0000000..bb16057 --- /dev/null +++ b/src/pages/transactions/EditTransactionPgage.ts @@ -0,0 +1,19 @@ +import { Page } from '@playwright/test'; + +import BasePageModel from '@/pages/BasePageModel'; +import TransactionDetailsHeaderTab from '@/pages/transactions/components/TransactionDetailsHeaderTab'; + +class EditTransactionPage extends BasePageModel { + transactionDetailsHeaderTab: TransactionDetailsHeaderTab; + + constructor(page: Page) { + super(page); + this.transactionDetailsHeaderTab = new TransactionDetailsHeaderTab(page); + } + + get saveButton() { + return this.page.getByRole('button', { name: 'Save' }); + } +} + +export default EditTransactionPage; diff --git a/src/pages/transactions/components/TransactionDetailsHeaderTab.ts b/src/pages/transactions/components/TransactionDetailsHeaderTab.ts new file mode 100644 index 0000000..842b9f5 --- /dev/null +++ b/src/pages/transactions/components/TransactionDetailsHeaderTab.ts @@ -0,0 +1,47 @@ +import { Locator, Page } from '@playwright/test'; + +import BasePageModel from '@/pages/BasePageModel'; + +class TransactionDetailsHeaderTab extends BasePageModel { + constructor(page: Page) { + super(page); + } + + get section() { + return this.page.locator('#transaction-header'); + } + + get table() { + return this.section.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 transactionDateMinuteSelect() { + return this.row.locator('select[name="transactionDate_minute"]'); + } + + async decreaseMinute() { + const selectedValue = await this.transactionDateMinuteSelect.inputValue(); + const parsedValue = parseInt(selectedValue || '0', 10); + const nextMinute = (parsedValue - 1 + 60) % 60; + const optionToSelect = String(nextMinute).padStart(2, '0'); + await this.transactionDateMinuteSelect.selectOption(optionToSelect); + } +} + +export default TransactionDetailsHeaderTab; diff --git a/src/pages/transactions/components/TransactionTable.ts b/src/pages/transactions/components/TransactionTable.ts index 470f944..57a77bb 100644 --- a/src/pages/transactions/components/TransactionTable.ts +++ b/src/pages/transactions/components/TransactionTable.ts @@ -24,6 +24,12 @@ class TransactionTable extends BasePageModel { .locator('.action-menu-item') .getByRole('link', { name: 'Delete' }); } + + get editButton() { + return this.page + .locator('.action-menu-item') + .getByRole('link', { name: 'Edit' }); + } } class Row extends BasePageModel { diff --git a/src/tests/putaway/validationOnQtyRemovedFromReceivingBin.test.ts b/src/tests/putaway/validationOnQtyRemovedFromReceivingBin.test.ts new file mode 100644 index 0000000..124703b --- /dev/null +++ b/src/tests/putaway/validationOnQtyRemovedFromReceivingBin.test.ts @@ -0,0 +1,277 @@ +import Navbar from '@/components/Navbar'; +import AppConfig from '@/config/AppConfig'; +import { ShipmentType } from '@/constants/ShipmentType'; +import { expect, test } from '@/fixtures/fixtures'; +import ProductShowPage from '@/pages/product/productShow/ProductShowPage'; +import { StockMovementResponse } from '@/types'; +import RefreshCachesUtils from '@/utils/RefreshCaches'; +import { getShipmentId, getShipmentItemId } from '@/utils/shipmentUtils'; + +test.describe('Assert validation on qty removed from receiving bin', () => { + let STOCK_MOVEMENT: StockMovementResponse; + + test.beforeEach( + async ({ + supplierLocationService, + stockMovementService, + productService, + receivingService, + }) => { + 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); + } + ); + + test.afterEach( + async ({ + stockMovementShowPage, + stockMovementService, + navbar, + transactionListPage, + oldViewShipmentPage, + }) => { + await navbar.configurationButton.click(); + await navbar.transactions.click(); + for (let n = 1; n < 4; n++) { + 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); + } + ); + + test('Assert validation on qty removed from receiving bin', async ({ + page, + transactionListPage, + editTransactionPage, + stockMovementShowPage, + createPutawayPage, + internalLocationService, + productShowPage, + putawayDetailsPage, + productService, + putawayListPage, + browser, + navbar, + }) => { + 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('Edit transaction date of transfer in', async () => { + await page.goto('./dashboard'); + await navbar.configurationButton.click(); + await navbar.transactions.click(); + // eslint-disable-next-line playwright/no-networkidle + await page.waitForLoadState('networkidle'); + await transactionListPage.table.row(1).actionsButton.click(); + await transactionListPage.table.editButton.click(); + await editTransactionPage.transactionDetailsHeaderTab + .row(2) + .transactionDateMinuteSelect.click(); + await editTransactionPage.transactionDetailsHeaderTab + .row(2) + .decreaseMinute(); + await editTransactionPage.saveButton.click(); + }); + + await test.step('Go to create putaway page', async () => { + await stockMovementShowPage.goToPage(STOCK_MOVEMENT.id); + await stockMovementShowPage.isLoaded(); + await RefreshCachesUtils.refreshCaches({ + navbar, + }); + 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 expect( + createPutawayPage.table.row(1).getProductName(product.name) + ).toBeVisible(); + await expect( + createPutawayPage.table.row(2).getProductName(product2.name) + ).toBeVisible(); + 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('Select bin to putaway and go to next page', async () => { + await createPutawayPage.startStep.table.row(1).putawayBinSelect.click(); + await createPutawayPage.startStep.table + .row(1) + .getPutawayBin(internalLocation.name) + .click(); + await createPutawayPage.startStep.table.row(2).putawayBinSelect.click(); + await createPutawayPage.startStep.table + .row(2) + .getPutawayBin(internalLocation.name) + .click(); + await createPutawayPage.startStep.nextButton.click(); + await createPutawayPage.completeStep.isLoaded(); + }); + + await test.step('Open new tab and edit qty to 0 in receiving bin on stock card', async () => { + const newPage = await browser.newPage(); + const newProductShowPage = new ProductShowPage(newPage); + const newNavbar = new Navbar(newPage); + await newProductShowPage.goToPage(product.id); + await newProductShowPage.recordStockButton.click(); + await newProductShowPage.recordStock.lineItemsTable + .row(1) + .newQuantity.getByRole('textbox') + .fill('0'); + await newProductShowPage.recordStock.lineItemsTable.saveButton.click(); + await newNavbar.profileButton.click(); + await newNavbar.refreshCachesButton.click(); + await newPage.close(); + }); + + await test.step('Try to complete putaway and assert error message', async () => { + await createPutawayPage.completeStep.isLoaded(); + await createPutawayPage.completeStep.completePutawayButton.click(); + await expect( + createPutawayPage.completeStep.validationOnQtyInReceivingBin + ).toBeVisible(); + }); + + await test.step('Go backward and assert validation', async () => { + await createPutawayPage.completeStep.editButton.click(); + await createPutawayPage.startStep.isLoaded(); + await expect(createPutawayPage.startStep.saveButton).toBeDisabled(); + await expect(createPutawayPage.startStep.nextButton).toBeDisabled(); + await createPutawayPage.startStep.table.row(1).editButton.click(); + await createPutawayPage.startStep.table.row(1).quantityInput.hover(); + await createPutawayPage.startStep.table.assertValidationOnQtyField( + 'Quantity cannot be greater than original putaway item quantity' + ); + }); + + await test.step('Delete invalid line', async () => { + await expect(createPutawayPage.startStep.table.rows).toHaveCount(3); + await createPutawayPage.startStep.table.row(1).deleteButton.click(); + await expect(createPutawayPage.startStep.table.rows).toHaveCount(2); + await expect(createPutawayPage.startStep.saveButton).toBeEnabled(); + await expect(createPutawayPage.startStep.nextButton).toBeEnabled(); + await createPutawayPage.startStep.saveButton.click(); + }); + + await test.step('Edit qty to lower in receiving bin when putaway started', async () => { + await productShowPage.goToPage(product2.id); + await productShowPage.recordStockButton.click(); + await productShowPage.recordStock.lineItemsTable + .row(2) + .newQuantity.getByRole('textbox') + .fill('5'); + await productShowPage.recordStock.lineItemsTable.saveButton.click(); + await RefreshCachesUtils.refreshCaches({ + navbar, + }); + }); + + await test.step('Go to putaway list page and edit created pending putaway', async () => { + await putawayListPage.goToPage(); + await putawayListPage.isLoaded(); + const row = putawayListPage.table.row(1); + await row.actionsButton.click(); + await row.viewOrderDetails.click(); + await putawayDetailsPage.isLoaded(); + }); + + await test.step('Edit pending putaway and assert validation on qty', async () => { + await putawayDetailsPage.editButton.click(); + await createPutawayPage.startStep.isLoaded(); + await expect(createPutawayPage.startStep.saveButton).toBeDisabled(); + await expect(createPutawayPage.startStep.nextButton).toBeDisabled(); + await createPutawayPage.startStep.table.row(1).editButton.click(); + await createPutawayPage.startStep.table.row(1).quantityInput.hover(); + await createPutawayPage.startStep.table.assertValidationOnQtyField( + 'Quantity cannot be greater than original putaway item quantity' + ); + }); + + await test.step('Edit qty in pending putaway', async () => { + await createPutawayPage.startStep.table.row(1).quantityInput.fill('5'); + await expect(createPutawayPage.startStep.saveButton).toBeEnabled(); + await expect(createPutawayPage.startStep.nextButton).toBeEnabled(); + }); + + await test.step('Complete putaway', async () => { + await createPutawayPage.startStep.nextButton.click(); + await createPutawayPage.completeStep.isLoaded(); + await createPutawayPage.completeStep.completePutawayButton.click(); + await putawayDetailsPage.isLoaded(); + await expect(putawayDetailsPage.statusTag).toHaveText('Completed'); + }); + + await test.step('Assert putaway bin and qty 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 expect( + productShowPage.inStockTabSection.row(2).quantityOnHand + ).toHaveText('5'); + }); + }); +});