diff --git a/src/api/InventoryService.ts b/src/api/InventoryService.ts index d8b97379..83cdc629 100644 --- a/src/api/InventoryService.ts +++ b/src/api/InventoryService.ts @@ -1,5 +1,6 @@ import BaseServiceModel from '@/api/BaseServiceModel'; import { INVENTORY_IMPORT } from '@/constants/apiUrls'; +import { MIGRATION_URL } from '@/constants/applicationUrls'; import { jsonToCsv } from '@/utils/ServiceUtils'; class InventoryService extends BaseServiceModel { @@ -19,6 +20,24 @@ class InventoryService extends BaseServiceModel { throw new Error(`Problem importing inventories: ${error instanceof Error ? error.message : String(error)}`); } } + + /** + Recalculates product_availability for a location from the transaction + ledger. product_availability is a denormalized cache read by several + features (e.g. putaway candidates), so it can go stale relative to the + ledger after certain operations are rolled back; this is the app's own + admin action for reconciling it. + */ + async refreshProductAvailability(locationId: string) { + const apiResponse = await this.request.post( + MIGRATION_URL.refreshProductAvailability(locationId) + ); + if (!apiResponse.ok()) { + throw new Error( + `Problem refreshing product availability for location: ${locationId}` + ); + } + } } export default InventoryService; diff --git a/src/constants/applicationUrls.ts b/src/constants/applicationUrls.ts index 52746c61..c3a218d4 100644 --- a/src/constants/applicationUrls.ts +++ b/src/constants/applicationUrls.ts @@ -94,6 +94,12 @@ const ORDER_URL = { `${ORDER_URL.list()}?orderType=PUTAWAY_ORDER&status=${status}`, }; +const MIGRATION_URL = { + base: './migration', + refreshProductAvailability: (locationId: string) => + `${MIGRATION_URL.base}/refreshProductAvailability?location.id=${locationId}`, +}; + export { AUTH_URL, CYCLE_COUNT_URL, @@ -103,6 +109,7 @@ export { INVOICE_URL, LOCATION_GROUP_URL, LOCATION_URL, + MIGRATION_URL, ORDER_URL, ORGANIZATION_URL, PERSON_URL, diff --git a/src/fixtures/fixtures.ts b/src/fixtures/fixtures.ts index d721de38..5637738b 100644 --- a/src/fixtures/fixtures.ts +++ b/src/fixtures/fixtures.ts @@ -3,6 +3,7 @@ import { BrowserContext, test as baseTest } from '@playwright/test'; import AuthService from '@/api/AuthService'; import CycleCountService from '@/api/CycleCountService'; import GenericService from '@/api/GenericService'; +import InventoryService from '@/api/InventoryService'; import LocationService from '@/api/LocationService'; import PutawayService from '@/api/PutawayService'; import ReceivingService from '@/api/ReceivingService'; @@ -101,6 +102,7 @@ type Fixtures = { putawayService: PutawayService; transactionService: TransactionService; cycleCountService: CycleCountService; + inventoryService: InventoryService; // LOCATIONS DATA mainLocationService: LocationData; noManageInventoryDepotService: LocationData; @@ -200,6 +202,8 @@ export const test = baseTest.extend({ use(new TransactionService(page.request)), cycleCountService: async ({ page }, use) => use(new CycleCountService(page.request)), + inventoryService: async ({ page }, use) => + use(new InventoryService(page.request)), // LOCATIONS mainLocationService: async ({ page }, use) => use(new LocationData(LOCATION_KEY.MAIN, page.request)), diff --git a/src/pages/product/productShow/sections/components/LineItemsTable.ts b/src/pages/product/productShow/sections/components/LineItemsTable.ts index 1d0f99aa..29955e80 100644 --- a/src/pages/product/productShow/sections/components/LineItemsTable.ts +++ b/src/pages/product/productShow/sections/components/LineItemsTable.ts @@ -38,6 +38,10 @@ class LineItemsTable extends BasePageModel { getRowByLot(lot: string) { return this.table.locator(`tr:has-text("${lot}")`); } + + getRowByBinLocation(binLocation: string) { + return new Row(this.page, this.rows.filter({ hasText: binLocation })); + } } class Row extends BasePageModel { diff --git a/src/pages/receiving/ReceivingPage.ts b/src/pages/receiving/ReceivingPage.ts index b51572c1..6b5a22da 100644 --- a/src/pages/receiving/ReceivingPage.ts +++ b/src/pages/receiving/ReceivingPage.ts @@ -29,7 +29,7 @@ class ReceivingPage extends BasePageModel { } get wizzardTitle() { - return this.page.getByTestId('wizardTitle'); + return this.page.getByTestId('wizard-title'); } async assertHeaderIsVisible({ @@ -54,14 +54,16 @@ class ReceivingPage extends BasePageModel { return this.page.getByRole('tooltip'); } - assertColumnHeaderTooltipOnReceivingStep = async (columnName: string) => { - await this.receivingStep.table.getColumnHeader(columnName).hover(); - await expect(this.tooltip).toContainText(columnName); + assertColumnHeaderIsVisibleOnReceivingStep = async (columnName: string) => { + await expect( + this.receivingStep.table.getColumnHeader(columnName) + ).toBeVisible(); }; - assertColumnHeaderTooltipOnCheckingStep = async (columnName: string) => { - await this.checkStep.table.getColumnHeader(columnName).hover(); - await expect(this.tooltip).toContainText(columnName); + assertColumnHeaderIsVisibleOnCheckingStep = async (columnName: string) => { + await expect( + this.checkStep.table.getColumnHeader(columnName) + ).toBeVisible(); }; } diff --git a/src/pages/receiving/components/AutosaveStatus.ts b/src/pages/receiving/components/AutosaveStatus.ts new file mode 100644 index 00000000..306b210f --- /dev/null +++ b/src/pages/receiving/components/AutosaveStatus.ts @@ -0,0 +1,27 @@ +import { expect, Page } from '@playwright/test'; + +import BasePageModel from '@/pages/BasePageModel'; + +class AutosaveStatus extends BasePageModel { + constructor(page: Page) { + super(page); + } + + get indicator() { + return this.page.getByTestId('receiving-autosave-status'); + } + + async isSaved() { + await expect(this.indicator).toContainText('Your work is auto-saved'); + } + + async isSaving() { + await expect(this.indicator).toContainText('Your work is being saved'); + } + + async hasError() { + await expect(this.indicator).toContainText('Your work was not saved'); + } +} + +export default AutosaveStatus; diff --git a/src/pages/receiving/components/CheckTable.ts b/src/pages/receiving/components/CheckTable.ts index e18af777..66198efe 100644 --- a/src/pages/receiving/components/CheckTable.ts +++ b/src/pages/receiving/components/CheckTable.ts @@ -8,15 +8,18 @@ class CheckTable extends BasePageModel { } get table() { - return this.page.getByTestId('items-table'); + return this.page.getByTestId('confirm-receipt-table'); } get rows() { return this.table.getByRole('row'); } + // rows are 1-indexed by convention (row 1 is the first item). Unlike the + // old table, the header row no longer carries role="row", so it doesn't + // occupy index 0 on its own. row(index: number) { - return new Row(this.page, this.rows.nth(index)); + return new Row(this.page, this.rows.nth(index - 1)); } // rows mixes container rows with item rows (e.g. the first item is at @@ -24,9 +27,7 @@ class CheckTable extends BasePageModel { // by item position. get itemRows() { return this.rows.filter({ - has: this.page - .getByTestId('label-field') - .and(this.page.getByLabel('Code', { exact: true })), + has: this.page.locator('[aria-label="Code"]'), }); } @@ -36,15 +37,12 @@ class CheckTable extends BasePageModel { getColumnHeader(columnName: string) { return this.table - .locator('.table-header') + .locator('.rt-thead') .getByText(columnName, { exact: true }); } getCellValue(row: number, column: string) { - return this.table - .getByRole('row') - .nth(row) - .getByRole('cell', { name: column, exact: true }); + return this.rows.nth(row - 1).locator(`[aria-label="${column}"]`); } } @@ -57,17 +55,17 @@ class Row extends BasePageModel { } getItem(name: string) { - return this.row.getByTestId('label-field').getByText(name); + return this.row.locator('[aria-label="Product"]').getByText(name); } get code() { - return this.row - .getByTestId('label-field') - .and(this.row.getByLabel('Code', { exact: true })); + return this.row.locator('[aria-label="Code"]'); } get cancelRemainingCheckbox() { - return this.row.getByTestId('form-field').getByTestId('checkbox'); + return this.row + .locator('[data-column-id="cancelRemaining"]') + .getByRole('checkbox'); } } diff --git a/src/pages/receiving/components/ReceivingTable.ts b/src/pages/receiving/components/ReceivingTable.ts index 4ecb427b..e3ebb36b 100644 --- a/src/pages/receiving/components/ReceivingTable.ts +++ b/src/pages/receiving/components/ReceivingTable.ts @@ -9,73 +9,51 @@ class ReceivingTable extends BasePageModel { } get table() { - return this.page.getByTestId('items-table'); + return this.page.getByTestId('receiving-table'); } get rows() { return this.table.getByRole('row'); } + // rows are 1-indexed by convention (row 1 is the first item), matching the + // rest of the receiving page objects. Unlike the old table, the header row + // no longer carries role="row", so it doesn't occupy index 0 on its own. row(index: number) { - return new Row(this.page, this.rows.nth(index)); + return new Row(this.page, this.rows.nth(index - 1)); } getColumnHeader(columnName: string) { - return this.table.locator('.table-header').getByText(columnName); + return this.table + .locator('.rt-thead') + .getByText(columnName, { exact: true }); } getCellValue(row: number, column: string) { - return this.table - .getByRole('row') - .nth(row) - .getByRole('cell', { name: column }); + return this.rows.nth(row - 1).locator(`[aria-label="${column}"]`); } } class Row extends BasePageModel { row: Locator; receivingNowField: TextField; - commentField: TextField; constructor(page: Page, row: Locator) { super(page); this.row = row; this.receivingNowField = new TextField(page, 'Receiving now', row); - this.commentField = new TextField(page, 'Comment', row); - } - - get checkbox() { - return this.row.getByRole('checkbox'); } get editButton() { return this.row.getByRole('button', { name: 'Edit' }); } - getItem(name: string) { - return this.row.getByTestId('label-field').getByText(name); - } - - get binLocationSelect() { - return this.row.getByRole('cell', { name: 'Bin Location' }); - } - - getBinLocation(binLocation: string) { - return this.page - .getByTestId('custom-select-dropdown-menu') - .getByRole('listitem') - .getByText(binLocation, { exact: true }); + get commentButton() { + return this.row.getByRole('button', { name: 'Comment' }); } - getZoneLocation(zoneLocation: string) { - return this.page - .getByTestId('custom-select-dropdown-menu') - .locator('.css-5ih5ya-group react-select__group-heading') - .getByText(zoneLocation, { exact: true }); - } - - get recipientField() { - return this.row.getByRole('cell', { name: 'Recipient' }); + getItem(name: string) { + return this.row.locator('[aria-label="Product"]').getByText(name); } } diff --git a/src/pages/receiving/steps/CheckStep.ts b/src/pages/receiving/steps/CheckStep.ts index 8474ad69..761950d6 100644 --- a/src/pages/receiving/steps/CheckStep.ts +++ b/src/pages/receiving/steps/CheckStep.ts @@ -16,30 +16,44 @@ class CheckStep extends BasePageModel { async isLoaded() { await expect(this.table.table).toBeVisible(); + await expect(this.table.table.getByText('Loading...')).toBeHidden({ + timeout: 20_000, + }); } get receiveShipmentButton() { - return this.page.getByRole('button', { name: 'Receive shipment' }); + return this.page + .locator('.submit-buttons') + .getByRole('button', { name: 'Complete Receipt' }); } get shimpentInformation() { - return this.page.locator('.form-title'); + return this.page.getByTestId('confirm-receipt-details'); } get originField() { - return this.page.getByRole('textbox', { name: 'Origin' }); + return this.shimpentInformation + .locator('.item-details__field') + .filter({ hasText: 'Origin:' }) + .locator('.item-details__value'); } get destinationField() { - return this.page.getByRole('textbox', { name: 'Destination' }); + return this.shimpentInformation + .locator('.item-details__field') + .filter({ hasText: 'Destination:' }) + .locator('.item-details__value'); } get shippedOnField() { - return this.page.getByRole('textbox', { name: 'Shipped on' }); + return this.shimpentInformation + .locator('.item-details__field') + .filter({ hasText: 'Shipped on:' }) + .locator('.item-details__value'); } get cancelAllRemainingButton() { - return this.page.getByRole('button', { name: 'Cancel all remaining' }); + return this.page.getByRole('button', { name: 'Cancel All Remaining' }); } get validationOnDeliveredOnPastDatePopup() { diff --git a/src/pages/receiving/steps/ReceivingStep.ts b/src/pages/receiving/steps/ReceivingStep.ts index b4f53093..78215a49 100644 --- a/src/pages/receiving/steps/ReceivingStep.ts +++ b/src/pages/receiving/steps/ReceivingStep.ts @@ -4,6 +4,7 @@ import FileHandler from '@/components/FileHandler'; import NewAlertPopup from '@/components/NewAlertPopup'; import { PARTIAL_RECEIVING_API_PATTERN } from '@/constants/apiUrls'; import BasePageModel from '@/pages/BasePageModel'; +import AutosaveStatus from '@/pages/receiving/components/AutosaveStatus'; import EditModal from '@/pages/receiving/components/EditModal'; import ReceivingTable from '@/pages/receiving/components/ReceivingTable'; @@ -14,6 +15,7 @@ class ReceivingStep extends BasePageModel { updateExpiryDatePopup: NewAlertPopup; fileHandler: FileHandler; + autosaveStatus: AutosaveStatus; constructor(page: Page) { super(page); @@ -21,10 +23,14 @@ class ReceivingStep extends BasePageModel { this.editModal = new EditModal(page); this.updateExpiryDatePopup = new NewAlertPopup(page); this.fileHandler = new FileHandler(page); + this.autosaveStatus = new AutosaveStatus(page); } async isLoaded() { await expect(this.table.table).toBeVisible(); + await expect(this.table.table.getByText('Loading...')).toBeHidden({ + timeout: 20_000, + }); } async waitForData() { @@ -36,7 +42,7 @@ class ReceivingStep extends BasePageModel { } get confirmReceivingDialog() { - return this.page.locator('.react-confirm-alert-body'); + return this.page.getByTestId('zero-lines-confirm-modal'); } get rejectConfirmReceivingDialog() { @@ -47,12 +53,8 @@ class ReceivingStep extends BasePageModel { return this.confirmReceivingDialog.getByRole('button', { name: 'Yes' }); } - get saveButton() { - return this.page.getByRole('button', { name: 'Save', exact: true }); - } - get saveAndExitButton() { - return this.page.getByRole('button').getByText('Save and Exit'); + return this.page.getByRole('button', { name: 'Save & Exit' }); } get exportTemplateButton() { diff --git a/src/pages/stockMovementShow/StockMovementShowPage.ts b/src/pages/stockMovementShow/StockMovementShowPage.ts index f84aba23..5a7c185d 100644 --- a/src/pages/stockMovementShow/StockMovementShowPage.ts +++ b/src/pages/stockMovementShow/StockMovementShowPage.ts @@ -34,7 +34,7 @@ class StockMovementShowPage extends BasePageModel { } async isLoaded() { - await expect(this.summary).toBeVisible(); + await expect(this.summary).toBeVisible({ timeout: 15_000 }); } get summary() { diff --git a/src/setup/validateCleanState.setup.ts b/src/setup/validateCleanState.setup.ts index cb40ea60..2b4897ea 100644 --- a/src/setup/validateCleanState.setup.ts +++ b/src/setup/validateCleanState.setup.ts @@ -58,12 +58,24 @@ function assertNoInbounds( test('validate clean state', async ({ putawayService, stockMovementService, + inventoryService, }) => { // validate there are no putaways waiting at the main location const mainLocation = AppConfig.instance.locations['main']; const { data: putawayCandidates } = await putawayService.getPutawayCandidates(mainLocation.readId()); - assertNoPutawayCandidates(mainLocation, putawayCandidates); + + if (putawayCandidates.length > 0) { + // a candidate can be a stale product_availability row left behind by a + // shipment that was rolled back and deleted after it had already been + // received; refreshing recalculates it from the transaction ledger, so a + // genuine leftover still fails the assertion below while a stale one + // clears on its own + await inventoryService.refreshProductAvailability(mainLocation.readId()); + } + const { data: refreshedPutawayCandidates } = + await putawayService.getPutawayCandidates(mainLocation.readId()); + assertNoPutawayCandidates(mainLocation, refreshedPutawayCandidates); // validate there are no leftover inbound stock movements at any location const locations = Object.values(AppConfig.instance.locations).filter( diff --git a/src/tests/inbound/createInbound/exportItems.test.ts b/src/tests/inbound/createInbound/exportItems.test.ts index a76cd302..cf59dc4d 100644 --- a/src/tests/inbound/createInbound/exportItems.test.ts +++ b/src/tests/inbound/createInbound/exportItems.test.ts @@ -207,7 +207,7 @@ test.describe('Export all incoming items', () => { }); }); - test.skip('Export all incoming items should include shipped items not received items', async ({ + test('Export all incoming items should include shipped items not received items', async ({ inboundListPage, stockMovementShowPage, stockMovementService, @@ -230,7 +230,7 @@ test.describe('Export all incoming items', () => { await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('12'); + .receivingNowField.numberbox.fill('12'); }); await test.step('Go to Check page', async () => { @@ -270,7 +270,7 @@ test.describe('Export all incoming items', () => { }); // TODO: Adjust to rely on the empty database (on empty db it fails, because there is 404 No shipment items found) - test.skip('Export all incoming items should not include received items', async ({ + test('Export all incoming items should not include received items', async ({ inboundListPage, stockMovementShowPage, stockMovementService, @@ -293,10 +293,10 @@ test.describe('Export all incoming items', () => { await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('12'); + .receivingNowField.numberbox.fill('12'); await receivingPage.receivingStep.table .row(2) - .receivingNowField.textbox.fill('12'); + .receivingNowField.numberbox.fill('12'); }); await test.step('Go to Check page', async () => { diff --git a/src/tests/inbound/listPage/receiptStatusFilter.test.ts b/src/tests/inbound/listPage/receiptStatusFilter.test.ts index 9e63d10c..72b3905b 100644 --- a/src/tests/inbound/listPage/receiptStatusFilter.test.ts +++ b/src/tests/inbound/listPage/receiptStatusFilter.test.ts @@ -150,7 +150,7 @@ test.describe('Filter by "Received" status', () => { await test.step('Select all items to receive', async () => { await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('2'); + .receivingNowField.numberbox.fill('2'); }); await test.step('Go to Check page', async () => { @@ -173,7 +173,7 @@ test.describe('Filter by "Received" status', () => { await stockMovementService.deleteStockMovement(STOCK_MOVEMENT.id); }); - test.skip('Only "Received" stock movements should be visible in the table', async ({ + test('Only "Received" stock movements should be visible in the table', async ({ inboundListPage, }) => { await test.step('Go to inbound list page', async () => { @@ -246,7 +246,7 @@ test.describe('Filter by "Receiving" status', () => { await test.step('Select all items to receiv', async () => { await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('2'); + .receivingNowField.numberbox.fill('2'); }); await test.step('Go to Check page', async () => { @@ -269,7 +269,7 @@ test.describe('Filter by "Receiving" status', () => { await stockMovementService.deleteStockMovement(STOCK_MOVEMENT.id); }); - test.skip('Only "Receiving" stock movements should be visible in the table', async ({ + test('Only "Receiving" stock movements should be visible in the table', async ({ inboundListPage, }) => { await test.step('Go to inbound list page', async () => { diff --git a/src/tests/inbound/listPage/searchFilter.test.ts b/src/tests/inbound/listPage/searchFilter.test.ts index aec273b9..66db277f 100644 --- a/src/tests/inbound/listPage/searchFilter.test.ts +++ b/src/tests/inbound/listPage/searchFilter.test.ts @@ -16,7 +16,7 @@ test.describe('Search filter', () => { await stockMovementService.deleteStockMovement(STOCK_MOVEMENT.id); }); - test.skip('Search stock movement by identifier', async ({ inboundListPage }) => { + test('Search stock movement by identifier', async ({ inboundListPage }) => { await test.step('Go to inbound list page', async () => { await inboundListPage.goToPage(); }); diff --git a/src/tests/putaway/rollbackLastReceiptWhenPutawayCreated.test.ts b/src/tests/putaway/rollbackLastReceiptWhenPutawayCreated.test.ts index 0afaaee4..d456c9c9 100644 --- a/src/tests/putaway/rollbackLastReceiptWhenPutawayCreated.test.ts +++ b/src/tests/putaway/rollbackLastReceiptWhenPutawayCreated.test.ts @@ -92,7 +92,7 @@ test.describe('Rollback last receipt behavior when putaway created', () => { } ); - test.skip('Rollback last receipt behavior when putaway created', async ({ + test('Rollback last receipt behavior when putaway created', async ({ stockMovementShowPage, navbar, createPutawayPage, diff --git a/src/tests/putaway/validationOnQtyRemovedFromReceivingBin.test.ts b/src/tests/putaway/validationOnQtyRemovedFromReceivingBin.test.ts index b91ce016..72d04c2a 100644 --- a/src/tests/putaway/validationOnQtyRemovedFromReceivingBin.test.ts +++ b/src/tests/putaway/validationOnQtyRemovedFromReceivingBin.test.ts @@ -1,4 +1,4 @@ -import Navbar from '@/components/Navbar'; +import PutawayService from '@/api/PutawayService'; import AppConfig from '@/config/AppConfig'; import { DASHBOARD_URL } from '@/constants/applicationUrls'; import { ShipmentType } from '@/constants/ShipmentType'; @@ -16,7 +16,44 @@ import { } from '@/utils/shipmentUtils'; import { byNameAsc } from '@/utils/sortUtils'; +/** + "Refresh Caches" only triggers RefreshProductAvailabilityJob (a Quartz job + recomputing the whole location from the transaction ledger) and returns + immediately - it does not wait for that job to finish, and the job's + duration is highly variable (milliseconds to minutes depending on server + load). Polling the putaway's own quantityAvailable is the only reliable way + to know the job has actually caught up with an external stock change. +*/ +async function waitForQuantityAvailable({ + putawayService, + orderId, + productId, + expectedQuantity, +}: { + putawayService: PutawayService; + orderId: string; + productId: string; + expectedQuantity: number; +}) { + await expect + .poll( + async () => { + const putaway = await putawayService.getPutaway(orderId); + const item = putaway?.data.putawayItems?.find( + (it) => it['product.id'] === productId + ); + return item?.quantityAvailable; + }, + { + message: `Waiting for the recorded stock change for product ${productId} to be reflected in putaway ${orderId}`, + timeout: 120_000, + } + ) + .toBe(expectedQuantity); +} + test.describe('Assert validation on qty removed from receiving bin', () => { + test.describe.configure({ timeout: 300_000 }); let STOCK_MOVEMENT: StockMovementResponse; let PUTAWAY_ORDER_IDS: string[] = []; let product: ProductResponse; @@ -119,6 +156,7 @@ test.describe('Assert validation on qty removed from receiving bin', () => { productShowPage, putawayDetailsPage, putawayListPage, + putawayService, browser, navbar, }) => { @@ -189,19 +227,25 @@ test.describe('Assert validation on qty removed from receiving bin', () => { 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) + .getRowByBinLocation(receivingBin) .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('Wait for the recorded qty change to settle', async () => { + await waitForQuantityAvailable({ + putawayService, + orderId: PUTAWAY_ORDER_IDS[0], + productId: product.id, + expectedQuantity: 0, + }); + }); + await test.step('Try to complete putaway and assert error message', async () => { await createPutawayPage.completeStep.isLoaded(); await createPutawayPage.completeStep.completePutawayButton.click(); @@ -245,12 +289,18 @@ test.describe('Assert validation on qty removed from receiving bin', () => { await productShowPage.goToPage(product2.id); await productShowPage.recordStockButton.click(); await productShowPage.recordStock.lineItemsTable - .row(2) + .getRowByBinLocation(receivingBin) .newQuantity.getByRole('textbox') .fill('5'); await productShowPage.recordStock.lineItemsTable.saveButton.click(); - await RefreshCachesUtils.refreshCaches({ - navbar, + }); + + await test.step('Wait for the recorded qty change to settle', async () => { + await waitForQuantityAvailable({ + putawayService, + orderId: PUTAWAY_ORDER_IDS[0], + productId: product2.id, + expectedQuantity: 5, }); }); diff --git a/src/tests/receiving/receiveInbound.test.ts b/src/tests/receiving/receiveInbound.test.ts index 1c88478c..e1a9ac04 100644 --- a/src/tests/receiving/receiveInbound.test.ts +++ b/src/tests/receiving/receiveInbound.test.ts @@ -1,4 +1,5 @@ import AppConfig from '@/config/AppConfig'; +import { DateFormat } from '@/constants/DateFormats'; import { ShipmentType } from '@/constants/ShipmentType'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; @@ -7,7 +8,7 @@ import BinLocationUtils from '@/utils/BinLocationUtils'; import { formatDate, getToday } from '@/utils/DateUtils'; import { deleteShipment } from '@/utils/shipmentUtils'; -test.describe.skip('Receive inbound stock movement', () => { +test.describe('Receive inbound stock movement', () => { let STOCK_MOVEMENT: StockMovementResponse; const description = 'some description'; const dateRequested = getToday(); @@ -85,44 +86,34 @@ test.describe.skip('Receive inbound stock movement', () => { origin: supplierLocation.name, destination: mainLocation.name, description: description, - date: formatDate(dateRequested), + date: formatDate(dateRequested, DateFormat.DISPLAY), }); }); await test.step('Assert table column headers on receiving page', async () => { - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Pack level 1' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep('Code'); + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Product' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Pack level 2' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Shipped' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Code'); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Product'); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Lot/Serial No.' - ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Expiration date' - ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Bin Location' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Receiving now' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Recipient'); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Shipped'); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Received'); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'To receive' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Status' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Receiving now' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Actions' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Comment'); }); await test.step('Assert product in receiving table', async () => { const item = await productService.getProduct(Product.ONE); - await receivingPage.receivingStep.table.row(1).getItem(item.name).hover(); - await expect(receivingPage.tooltip).toContainText(item.name); + await expect( + receivingPage.receivingStep.table.row(1).getItem(item.name) + ).toBeVisible(); }); await test.step('Select all items to receive', async () => { @@ -142,53 +133,48 @@ test.describe.skip('Receive inbound stock movement', () => { origin: supplierLocation.name, destination: mainLocation.name, description: description, - date: formatDate(dateRequested), + date: formatDate(dateRequested, DateFormat.DISPLAY), }); }); await test.step('Assert table column headers on checking page', async () => { - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Pack level 1' - ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Pack level 2' + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep('Code'); + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( + 'Product' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Code'); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Product'); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Lot/Serial No.' + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( + 'Shipped' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Expiration date' + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( + 'Receiving now' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Bin Location' + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep('Status'); + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( + 'Location' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Recipient'); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Receiving now' + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( + 'Actions' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Remaining'); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Cancel remaining' + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( + 'Cancel Remaining' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Comment'); }); await test.step('Assert product in checking table', async () => { const item = await productService.getProduct(Product.ONE); - await receivingPage.checkStep.table.row(1).getItem(item.name).hover(); - await expect(receivingPage.tooltip).toContainText(item.name); + await expect( + receivingPage.checkStep.table.row(1).getItem(item.name) + ).toBeVisible(); }); - await test.step('Assert receiving now and remaining qty on checking table', async () => { + await test.step('Assert receiving now and status on checking table', async () => { await receivingPage.checkStep.isLoaded(); await expect( receivingPage.checkStep.table.getCellValue(1, 'Receiving now') ).toContainText('10'); await expect( - receivingPage.checkStep.table.getCellValue(1, 'Remaining') - ).toContainText('0'); + receivingPage.checkStep.table.getCellValue(1, 'Status') + ).toContainText('Complete'); }); await test.step('Assert shipment information on checking table', async () => { @@ -196,15 +182,14 @@ test.describe.skip('Receive inbound stock movement', () => { const destinationName = (await mainLocationService.getLocation()).name; await receivingPage.checkStep.isLoaded(); await expect(receivingPage.checkStep.shimpentInformation).toBeVisible(); - await expect(receivingPage.checkStep.originField).toHaveValue(originName); - await expect(receivingPage.checkStep.destinationField).toHaveValue( - destinationName + await expect(receivingPage.checkStep.originField).toHaveText( + originName ); - await expect(receivingPage.checkStep.shippedOnField).toHaveValue( - formatDate(TODAY) + await expect(receivingPage.checkStep.destinationField).toHaveText( + destinationName ); - await expect(receivingPage.checkStep.shippedOnField).toHaveValue( - formatDate(TODAY) + await expect(receivingPage.checkStep.shippedOnField).toHaveText( + formatDate(TODAY, DateFormat.DISPLAY) ); }); @@ -232,8 +217,10 @@ test.describe.skip('Receive inbound stock movement', () => { await receivingPage.receivingStep.isLoaded(); }); - await test.step('Check first item to be received', async () => { - await receivingPage.receivingStep.table.row(1).checkbox.check(); + await test.step('Fill quantity for the first item to be received', async () => { + await receivingPage.receivingStep.table + .row(1) + .receivingNowField.numberbox.fill('10'); }); await test.step('Go to check page', async () => { @@ -247,7 +234,7 @@ test.describe.skip('Receive inbound stock movement', () => { }); }); - test('Use Save button in receiving and assert saved qty', async ({ + test('Assert quantities are auto-saved while receiving', async ({ stockMovementShowPage, receivingPage, }) => { @@ -261,17 +248,17 @@ test.describe.skip('Receive inbound stock movement', () => { await receivingPage.receivingStep.isLoaded(); }); - await test.step('Check first item to be received', async () => { + await test.step('Fill quantity to be received', async () => { await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('8'); + .receivingNowField.numberbox.fill('8'); await receivingPage.receivingStep.table .row(2) - .receivingNowField.textbox.fill('8'); + .receivingNowField.numberbox.fill('8'); }); - await test.step('Click on Save button', async () => { - await receivingPage.receivingStep.saveButton.click(); + await test.step('Wait for autosave and leave the page', async () => { + await receivingPage.receivingStep.autosaveStatus.isSaved(); await stockMovementShowPage.goToPage(STOCK_MOVEMENT.id); await stockMovementShowPage.isLoaded(); }); @@ -280,10 +267,10 @@ test.describe.skip('Receive inbound stock movement', () => { await stockMovementShowPage.receiveButton.click(); await receivingPage.receivingStep.isLoaded(); await expect( - receivingPage.receivingStep.table.row(1).receivingNowField.textbox + receivingPage.receivingStep.table.row(1).receivingNowField.numberbox ).toHaveValue('8'); await expect( - receivingPage.receivingStep.table.row(2).receivingNowField.textbox + receivingPage.receivingStep.table.row(2).receivingNowField.numberbox ).toHaveValue('8'); }); }); @@ -302,13 +289,13 @@ test.describe.skip('Receive inbound stock movement', () => { await receivingPage.receivingStep.isLoaded(); }); - await test.step('Check first item to be received', async () => { + await test.step('Fill quantity to be received', async () => { await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('2'); + .receivingNowField.numberbox.fill('2'); await receivingPage.receivingStep.table .row(2) - .receivingNowField.textbox.fill('2'); + .receivingNowField.numberbox.fill('2'); }); await test.step('Click on Save and Exit button', async () => { @@ -320,17 +307,18 @@ test.describe.skip('Receive inbound stock movement', () => { await stockMovementShowPage.receiveButton.click(); await receivingPage.receivingStep.isLoaded(); await expect( - receivingPage.receivingStep.table.row(1).receivingNowField.textbox + receivingPage.receivingStep.table.row(1).receivingNowField.numberbox ).toHaveValue('2'); await expect( - receivingPage.receivingStep.table.row(2).receivingNowField.textbox + receivingPage.receivingStep.table.row(2).receivingNowField.numberbox ).toHaveValue('2'); }); }); - test.skip('Use Save button after removing qty and default to 0', async ({ + test('Clearing a qty field autosaves it as empty, not the original shipped qty', async ({ stockMovementShowPage, receivingPage, + page, }) => { await test.step('Go to stock movement show page', async () => { await stockMovementShowPage.goToPage(STOCK_MOVEMENT.id); @@ -345,28 +333,34 @@ test.describe.skip('Receive inbound stock movement', () => { await test.step('Input qty for an item to be received', async () => { await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('8'); + .receivingNowField.numberbox.fill('8'); await receivingPage.receivingStep.table .row(2) - .receivingNowField.textbox.fill('10'); + .receivingNowField.numberbox.fill('10'); }); - await test.step('Clear qty field and click on Save button', async () => { + await test.step('Clear qty field and let autosave run', async () => { await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.clear(); - await receivingPage.receivingStep.saveButton.click(); + .receivingNowField.numberbox.clear(); + await page.keyboard.press('Tab'); + await receivingPage.receivingStep.autosaveStatus.isSaved(); + }); + + await test.step('Reload and assert the cleared field stayed empty', async () => { + await page.reload(); + await receivingPage.receivingStep.isLoaded(); await expect( - receivingPage.receivingStep.table.row(1).receivingNowField.textbox - ).toHaveValue('0'); + receivingPage.receivingStep.table.row(1).receivingNowField.numberbox + ).toHaveValue(''); await expect( - receivingPage.receivingStep.table.row(2).receivingNowField.textbox + receivingPage.receivingStep.table.row(2).receivingNowField.numberbox ).toHaveValue('10'); }); }); }); -test.describe.skip('Receive from different locations', () => { +test.describe('Receive from different locations', () => { let STOCK_MOVEMENT: StockMovementResponse; const description = 'some description'; const dateRequested = getToday(); diff --git a/src/tests/receiving/receiveInboundWithoutPartialReceiving.test.ts b/src/tests/receiving/receiveInboundWithoutPartialReceiving.test.ts index 6fea8c42..71ea988d 100644 --- a/src/tests/receiving/receiveInboundWithoutPartialReceiving.test.ts +++ b/src/tests/receiving/receiveInboundWithoutPartialReceiving.test.ts @@ -1,4 +1,5 @@ import AppConfig from '@/config/AppConfig'; +import { DateFormat } from '@/constants/DateFormats'; import { ShipmentType } from '@/constants/ShipmentType'; import { expect, test } from '@/fixtures/fixtures'; import { Product } from '@/generated/ProductCodes.generated'; @@ -6,7 +7,7 @@ import { StockMovementResponse } from '@/types'; import { formatDate, getToday } from '@/utils/DateUtils'; import { deleteShipment } from '@/utils/shipmentUtils'; -test.describe.skip('Receive inbound stock movement in location without partial receiving', () => { +test.describe('Receive inbound stock movement in location without partial receiving', () => { let STOCK_MOVEMENT: StockMovementResponse; const description = 'some description'; const dateRequested = getToday(); @@ -81,53 +82,45 @@ test.describe.skip('Receive inbound stock movement in location without partial r origin: supplierLocation.name, destination: depotLocation.name, description: description, - date: formatDate(dateRequested), + date: formatDate(dateRequested, DateFormat.DISPLAY), }); }); await test.step('Assert table column headers on receiving page', async () => { - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Pack level 1' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep('Code'); + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Product' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Pack level 2' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Shipped' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Code'); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Product'); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Lot/Serial No.' - ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Expiration date' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Receiving now' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Bin Location' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Status' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Recipient'); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Shipped'); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Receiving now' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Actions' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Comment'); }); await test.step('Assert product in receiving table', async () => { const item = await productService.getProduct(Product.ONE); const item2 = await productService.getProduct(Product.TWO); - await receivingPage.receivingStep.table.row(1).getItem(item.name).hover(); - await expect(receivingPage.tooltip).toContainText(item.name); - await receivingPage.receivingStep.table - .row(2) - .getItem(item2.name) - .hover(); - await expect(receivingPage.tooltip).toContainText(item2.name); + await expect( + receivingPage.receivingStep.table.row(1).getItem(item.name) + ).toBeVisible(); + await expect( + receivingPage.receivingStep.table.row(2).getItem(item2.name) + ).toBeVisible(); }); await test.step('Select item to receive', async () => { await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('20'); + .receivingNowField.numberbox.fill('20'); }); await test.step('Try to go to next page', async () => { @@ -147,7 +140,7 @@ test.describe.skip('Receive inbound stock movement in location without partial r await test.step('Assert receiving page is visible', async () => { await receivingPage.receivingStep.isLoaded(); await expect( - receivingPage.receivingStep.table.row(1).receivingNowField.textbox + receivingPage.receivingStep.table.row(1).receivingNowField.numberbox ).toHaveValue('20'); }); }); @@ -175,7 +168,7 @@ test.describe.skip('Receive inbound stock movement in location without partial r await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('20'); + .receivingNowField.numberbox.fill('20'); }); await test.step('Try to go to next page', async () => { @@ -204,62 +197,58 @@ test.describe.skip('Receive inbound stock movement in location without partial r origin: supplierLocation.name, destination: depotLocation.name, description: description, - date: formatDate(dateRequested), + date: formatDate(dateRequested, DateFormat.DISPLAY), }); }); await test.step('Assert table column headers on checking page', async () => { - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Pack level 1' + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep('Code'); + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( + 'Product' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Pack level 2' + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( + 'Shipped' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Code'); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Product'); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Lot/Serial No.' + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( + 'Receiving now' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Expiration date' + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep('Status'); + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( + 'Location' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Bin Location' + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( + 'Actions' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Recipient'); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Receiving now' - ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Remaining'); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Comment'); + // locations without partial receiving support auto-resolve every line + // to Complete/Cancelled on receipt, so there's nothing left to cancel + // later and the column doesn't render here. }); await test.step('Assert product in checking table', async () => { const item = await productService.getProduct(Product.ONE); const item2 = await productService.getProduct(Product.TWO); - await receivingPage.checkStep.table.row(1).getItem(item.name).hover(); - await expect(receivingPage.tooltip).toContainText(item.name); - await receivingPage.receivingStep.table - .row(2) - .getItem(item2.name) - .hover(); - await expect(receivingPage.tooltip).toContainText(item2.name); + await expect( + receivingPage.checkStep.table.row(1).getItem(item.name) + ).toBeVisible(); + await expect( + receivingPage.checkStep.table.row(2).getItem(item2.name) + ).toBeVisible(); }); - await test.step('Assert receiving now and remaining qty on checking table', async () => { + await test.step('Assert receiving now and status on checking table', async () => { await receivingPage.checkStep.isLoaded(); await expect( receivingPage.checkStep.table.getCellValue(1, 'Receiving now') ).toContainText('20'); await expect( - receivingPage.checkStep.table.getCellValue(1, 'Remaining') - ).toContainText('0'); + receivingPage.checkStep.table.getCellValue(1, 'Status') + ).toContainText('Complete'); await expect( receivingPage.checkStep.table.getCellValue(2, 'Receiving now') ).toContainText('0'); await expect( - receivingPage.checkStep.table.getCellValue(2, 'Remaining') - ).toContainText('10'); + receivingPage.checkStep.table.getCellValue(2, 'Status') + ).toContainText('10 cancelled'); }); await test.step('Receive shipment', async () => { @@ -289,11 +278,11 @@ test.describe.skip('Receive inbound stock movement in location without partial r await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('15'); + .receivingNowField.numberbox.fill('15'); await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(2) - .receivingNowField.textbox.fill('10'); + .receivingNowField.numberbox.fill('10'); }); await test.step('Try to go to next page', async () => { @@ -310,20 +299,20 @@ test.describe.skip('Receive inbound stock movement in location without partial r await receivingPage.checkStep.isLoaded(); }); - await test.step('Assert receiving now and remaining qty on checking table', async () => { + await test.step('Assert receiving now and status on checking table', async () => { await receivingPage.checkStep.isLoaded(); await expect( receivingPage.checkStep.table.getCellValue(1, 'Receiving now') ).toContainText('15'); await expect( - receivingPage.checkStep.table.getCellValue(1, 'Remaining') - ).toContainText('5'); + receivingPage.checkStep.table.getCellValue(1, 'Status') + ).toContainText('5 cancelled'); await expect( receivingPage.checkStep.table.getCellValue(2, 'Receiving now') ).toContainText('10'); await expect( - receivingPage.checkStep.table.getCellValue(2, 'Remaining') - ).toContainText('0'); + receivingPage.checkStep.table.getCellValue(2, 'Status') + ).toContainText('Complete'); }); await test.step('Receive shipment', async () => { diff --git a/src/tests/receiving/receiveInboundWithoutPickAndPutawayStock.test.ts b/src/tests/receiving/receiveInboundWithoutPickAndPutawayStock.test.ts index 2970ef55..2d1a79da 100644 --- a/src/tests/receiving/receiveInboundWithoutPickAndPutawayStock.test.ts +++ b/src/tests/receiving/receiveInboundWithoutPickAndPutawayStock.test.ts @@ -6,7 +6,7 @@ import { StockMovementResponse } from '@/types'; import { getToday } from '@/utils/DateUtils'; import { deleteShipment } from '@/utils/shipmentUtils'; -test.describe.skip('Receive inbound stock movement in location without pick and putaway stock', () => { +test.describe('Receive inbound stock movement in location without pick and putaway stock', () => { let STOCK_MOVEMENT: StockMovementResponse; const description = 'some description'; const dateRequested = getToday(); @@ -71,30 +71,22 @@ test.describe.skip('Receive inbound stock movement in location without pick and }); await test.step('Assert table column headers on receiving page', async () => { - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Pack level 1' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep('Code'); + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Product' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Pack level 2' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Shipped' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Code'); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Product'); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Lot/Serial No.' - ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Expiration date' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Receiving now' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Recipient'); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Shipped'); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Received'); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'To receive' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Status' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep( - 'Receiving now' + await receivingPage.assertColumnHeaderIsVisibleOnReceivingStep( + 'Actions' ); - await receivingPage.assertColumnHeaderTooltipOnReceivingStep('Comment'); }); await test.step('Autofill receiving qty', async () => { @@ -108,29 +100,23 @@ test.describe.skip('Receive inbound stock movement in location without pick and }); await test.step('Assert table column headers on checking page', async () => { - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Pack level 1' - ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Pack level 2' - ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Code'); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Product'); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Lot/Serial No.' + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep('Code'); + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( + 'Product' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Expiration date' + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( + 'Shipped' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Recipient'); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( 'Receiving now' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Remaining'); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep( - 'Cancel remaining' + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep('Status'); + await receivingPage.assertColumnHeaderIsVisibleOnCheckingStep( + 'Actions' ); - await receivingPage.assertColumnHeaderTooltipOnCheckingStep('Comment'); + // a location without pick/putaway support has no per-line bin + // location concept (everything lands in a single "Default" bin), so + // there's no "Location" or "Cancel Remaining" column here either. }); await test.step('Receive shipment', async () => { diff --git a/src/tests/receiving/receivingStatusChanges.test.ts b/src/tests/receiving/receivingStatusChanges.test.ts index b32b4cfe..a151b6bb 100644 --- a/src/tests/receiving/receivingStatusChanges.test.ts +++ b/src/tests/receiving/receivingStatusChanges.test.ts @@ -9,7 +9,7 @@ import BinLocationUtils from '@/utils/BinLocationUtils'; import { getToday } from '@/utils/DateUtils'; import { deleteShipment } from '@/utils/shipmentUtils'; -test.describe.skip('Status changes on sm view page when receive shipment', () => { +test.describe('Status changes on sm view page when receive shipment', () => { let STOCK_MOVEMENT: StockMovementResponse; const description = 'some description'; const dateRequested = getToday(); @@ -90,7 +90,7 @@ test.describe.skip('Status changes on sm view page when receive shipment', () => await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('10'); + .receivingNowField.numberbox.fill('10'); }); await test.step('Go to check page', async () => { @@ -169,7 +169,7 @@ test.describe.skip('Status changes on sm view page when receive shipment', () => await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('20'); + .receivingNowField.numberbox.fill('20'); }); await test.step('Go to check page', async () => { @@ -248,10 +248,10 @@ test.describe.skip('Status changes on sm view page when receive shipment', () => await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('20'); + .receivingNowField.numberbox.fill('20'); await receivingPage.receivingStep.table .row(2) - .receivingNowField.textbox.fill('5'); + .receivingNowField.numberbox.fill('5'); }); await test.step('Go to check page', async () => { @@ -327,7 +327,7 @@ test.describe.skip('Status changes on sm view page when receive shipment', () => await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(2) - .receivingNowField.textbox.fill('5'); + .receivingNowField.numberbox.fill('5'); }); await test.step('Go to check page and receive shipment', async () => { @@ -395,7 +395,7 @@ test.describe.skip('Status changes on sm view page when receive shipment', () => await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('10'); + .receivingNowField.numberbox.fill('10'); }); await test.step('Go to check page and receive item', async () => { @@ -443,7 +443,7 @@ test.describe.skip('Status changes on sm view page when receive shipment', () => await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('20'); + .receivingNowField.numberbox.fill('20'); }); await test.step('Go to check page and receive item', async () => { @@ -491,10 +491,10 @@ test.describe.skip('Status changes on sm view page when receive shipment', () => await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('20'); + .receivingNowField.numberbox.fill('20'); await receivingPage.receivingStep.table .row(2) - .receivingNowField.textbox.fill('5'); + .receivingNowField.numberbox.fill('5'); }); await test.step('Go to check page and receive item', async () => { @@ -531,7 +531,7 @@ test.describe.skip('Status changes on sm view page when receive shipment', () => await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(2) - .receivingNowField.textbox.fill('5'); + .receivingNowField.numberbox.fill('5'); }); await test.step('Go to check page and receive shipment', async () => { diff --git a/src/tests/receiving/receivingStatusChangesWithoutPartialReceiving.test.ts b/src/tests/receiving/receivingStatusChangesWithoutPartialReceiving.test.ts index 86a6d20b..3147022f 100644 --- a/src/tests/receiving/receivingStatusChangesWithoutPartialReceiving.test.ts +++ b/src/tests/receiving/receivingStatusChangesWithoutPartialReceiving.test.ts @@ -8,7 +8,7 @@ import { StockMovementResponse } from '@/types'; import { getToday } from '@/utils/DateUtils'; import { deleteShipment } from '@/utils/shipmentUtils'; -test.describe.skip('Status changes on sm view page when receive shipment in location without partial receiving', () => { +test.describe('Status changes on sm view page when receive shipment in location without partial receiving', () => { let STOCK_MOVEMENT: StockMovementResponse; const description = 'some description'; const dateRequested = getToday(); @@ -82,7 +82,7 @@ test.describe.skip('Status changes on sm view page when receive shipment in loca await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('10'); + .receivingNowField.numberbox.fill('10'); }); await test.step('Go to next page and accept Confirm receiving dialog', async () => { @@ -160,7 +160,7 @@ test.describe.skip('Status changes on sm view page when receive shipment in loca await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('20'); + .receivingNowField.numberbox.fill('20'); }); await test.step('Go to next page and accept Confirm receiving dialog', async () => { @@ -238,10 +238,10 @@ test.describe.skip('Status changes on sm view page when receive shipment in loca await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('20'); + .receivingNowField.numberbox.fill('20'); await receivingPage.receivingStep.table .row(2) - .receivingNowField.textbox.fill('5'); + .receivingNowField.numberbox.fill('5'); }); await test.step('Go to next page and receive shipment', async () => { @@ -315,10 +315,10 @@ test.describe.skip('Status changes on sm view page when receive shipment in loca await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('20'); + .receivingNowField.numberbox.fill('20'); await receivingPage.receivingStep.table .row(2) - .receivingNowField.textbox.fill('5'); + .receivingNowField.numberbox.fill('5'); }); await test.step('Go to next page and receive shipment', async () => { @@ -417,7 +417,7 @@ test.describe.skip('Status changes on sm view page when receive shipment in loca await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('10'); + .receivingNowField.numberbox.fill('10'); }); await test.step('Go to next page and accept Confirm receiving dialog', async () => { @@ -472,7 +472,7 @@ test.describe.skip('Status changes on sm view page when receive shipment in loca await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('20'); + .receivingNowField.numberbox.fill('20'); }); await test.step('Go to next page and accept Confirm receiving dialog', async () => { @@ -527,10 +527,10 @@ test.describe.skip('Status changes on sm view page when receive shipment in loca await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('20'); + .receivingNowField.numberbox.fill('20'); await receivingPage.receivingStep.table .row(2) - .receivingNowField.textbox.fill('5'); + .receivingNowField.numberbox.fill('5'); }); await test.step('Go to next page and receive shipment', async () => { diff --git a/src/tests/receiving/rollbackStatusChanges.test.ts b/src/tests/receiving/rollbackStatusChanges.test.ts index f3edac36..391cdd2b 100644 --- a/src/tests/receiving/rollbackStatusChanges.test.ts +++ b/src/tests/receiving/rollbackStatusChanges.test.ts @@ -9,7 +9,7 @@ import BinLocationUtils from '@/utils/BinLocationUtils'; import { getToday } from '@/utils/DateUtils'; import { deleteShipment } from '@/utils/shipmentUtils'; -test.describe.skip('Status changes on sm view page when rollback receipts', () => { +test.describe('Status changes on sm view page when rollback receipts', () => { let STOCK_MOVEMENT: StockMovementResponse; const description = 'some description'; const dateRequested = getToday(); @@ -81,7 +81,7 @@ test.describe.skip('Status changes on sm view page when rollback receipts', () = await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('15'); + .receivingNowField.numberbox.fill('15'); }); await test.step('Go to check page and finish receipt', async () => { @@ -140,7 +140,7 @@ test.describe.skip('Status changes on sm view page when rollback receipts', () = await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('20'); + .receivingNowField.numberbox.fill('20'); }); await test.step('Go to check page and finish receipt', async () => { @@ -199,10 +199,10 @@ test.describe.skip('Status changes on sm view page when rollback receipts', () = await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('10'); + .receivingNowField.numberbox.fill('10'); await receivingPage.receivingStep.table .row(2) - .receivingNowField.textbox.fill('10'); + .receivingNowField.numberbox.fill('10'); }); await test.step('Go to check page and finish 1st receipt', async () => { @@ -221,7 +221,7 @@ test.describe.skip('Status changes on sm view page when rollback receipts', () = await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('10'); + .receivingNowField.numberbox.fill('10'); }); await test.step('Go to check page and finish 2nd receipt', async () => { @@ -311,7 +311,7 @@ test.describe.skip('Status changes on sm view page when rollback receipts', () = await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('15'); + .receivingNowField.numberbox.fill('15'); }); await test.step('Go to check page and finish receipt', async () => { @@ -382,7 +382,7 @@ test.describe.skip('Status changes on sm view page when rollback receipts', () = await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('20'); + .receivingNowField.numberbox.fill('20'); }); await test.step('Go to check page and finish receipt', async () => { @@ -453,10 +453,10 @@ test.describe.skip('Status changes on sm view page when rollback receipts', () = await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('10'); + .receivingNowField.numberbox.fill('10'); await receivingPage.receivingStep.table .row(2) - .receivingNowField.textbox.fill('10'); + .receivingNowField.numberbox.fill('10'); }); await test.step('Go to check page and finish 1st receipt', async () => { @@ -475,7 +475,7 @@ test.describe.skip('Status changes on sm view page when rollback receipts', () = await receivingPage.receivingStep.isLoaded(); await receivingPage.receivingStep.table .row(1) - .receivingNowField.textbox.fill('10'); + .receivingNowField.numberbox.fill('10'); }); await test.step('Go to check page and finish 2nd receipt', async () => { diff --git a/src/types.d.ts b/src/types.d.ts index 9bc82713..bb65b221 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -35,10 +35,16 @@ type StockMovementListResponse = { }; // GET /api/putaways/ +// putawayItems rows are flattened into dotted keys (e.g. "product.id"), same +// convention as PutawayCandidate below, not nested objects type PutawayResponse = { id: string; putawayNumber?: string | null; putawayStatus?: string | null; + putawayItems?: { + 'product.id'?: string | null; + quantityAvailable?: number | null; + }[]; }; // GET /api/putaways returns rows flattened into dotted keys