Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/pages/putaway/components/CreatePutawayTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ class CreatePutawayTable extends BasePageModel {
row(index: number) {
return new Row(this.page, this.rows.nth(index));
}

// Row containing the given product name. Use after flattening the table
// (Show by -> Product) so each item is its own row.
rowByProductName(name: string) {
return new Row(this.page, this.rows.filter({ hasText: name }).first());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is searching the whole row? Is there no way to target the specific column?

}
}

class Row extends BasePageModel {
Expand Down
17 changes: 17 additions & 0 deletions src/pages/putaway/components/StartPutawayTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,23 @@ class StartPutawayTable extends BasePageModel {
return new Row(this.page, this.rows.nth(index));
}

/*
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/*
/**

I don't know how it works for typescript files, but docstrings need to be like this in java for the IDE to be able to resolve them properly (such as when hovering on method names)

Same for all your docstrings in this PR.

Returns the passed product names ordered by their actual vertical position
in the table (top to bottom).
*/
async getProductsOrder(productNames: string[]): Promise<string[]> {
const positions: { name: string; y: number }[] = [];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does "vertical position" and "y" mean here? Is it row index into the table? Or is it using the actual vertical position in the UI?

for (const name of productNames) {
const cell = this.table
.getByTestId('table-cell')
.filter({ hasText: name })
.first();
const box = await cell.boundingBox();
positions.push({ name, y: box?.y ?? Number.MAX_SAFE_INTEGER });
}
return positions.sort((a, b) => a.y - b.y).map((item) => item.name);
}

get qtyValidationTooltip() {
return this.page.getByRole('tooltip');
}
Expand Down
8 changes: 7 additions & 1 deletion src/pages/putaway/steps/StartStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ class StartStep extends BasePageModel {
return this.page.getByTestId('export-button');
}

get sortByCurrentBinButton() {
/*
A single button that cycles the putaway item order through three states:
original -> by current bins -> by preferred bin -> original.
Its label shows the next action in the cycle, i.e. "Sort by current bins",
then "Sort by preferred bin", then "Original order".
*/
get sortButton() {
return this.page.getByTestId('sort-button');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ test.describe('Assert attempt to edit completed putaway', () => {
createPutawayPage.startStep.validationOnEditCompletedPutaway
).toBeVisible();
await createPutawayPage.startStep.closeDisplayedError();
await createPutawayPage.startStep.sortByCurrentBinButton.click();
await createPutawayPage.startStep.sortButton.click();
await expect(
createPutawayPage.startStep.validationOnEditCompletedPutaway
).toBeVisible();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import { expect, test } from '@/fixtures/fixtures';
import { Product } from '@/generated/ProductCodes.generated';
import {
LocationResponse,
ProductResponse,
StockMovementResponse,
} from '@/types';
import { assignPreferredBin } from '@/utils/productUtils';
import RefreshCachesUtils from '@/utils/RefreshCaches';
import { deleteReceivedShipment, receiveInbound } from '@/utils/shipmentUtils';
import { byNameAsc } from '@/utils/sortUtils';

/*
Sort putaway items by current bin, preferred bin and original order.

Data is designed so each sort returns a different order

Orders by backend comparators:
- original -> product name => [A, B, C]
- currentBins -> current bins string asc => [B, C, A]
- preferredBin -> preferred bin name asc => [C, B, A]
*/
test.describe('Sort putaway by current bin, preferred bin and original order', () => {
let inboundOne: StockMovementResponse;
let inboundTwo: StockMovementResponse;

let productA: ProductResponse;
let productB: ProductResponse;
let productC: ProductResponse;

let binOne: LocationResponse;
let binTwo: LocationResponse;

test.beforeEach(
async ({
supplierLocationService,
stockMovementService,
receivingService,
productService,
internalLocationService,
internalLocation2Service,
productShowPage,
productEditPage,
}) => {
[productA, productB, productC] = [
await productService.getProduct(Product.ONE),
await productService.getProduct(Product.TWO),
await productService.getProduct(Product.THREE),
].sort(byNameAsc);

[binOne, binTwo] = [
await internalLocationService.getLocation(),
await internalLocation2Service.getLocation(),
].sort(byNameAsc);

const supplierLocation = await supplierLocationService.getLocation();

// 1st inbound - only item A (will get a current bin via putaway).
inboundOne = await stockMovementService.createInbound({
originId: supplierLocation.id,
});
await stockMovementService.addItemsToInboundStockMovement(inboundOne.id, [
{ productId: productA.id, quantity: 10 },
]);
await receiveInbound(
{ stockMovementService, receivingService },
inboundOne,
[10]
);

// Assign preferred bins: A -> binTwo, B -> binOne (C has none).
await assignPreferredBin(
{ productShowPage, productEditPage },
productA,
binTwo
);
await assignPreferredBin(
{ productShowPage, productEditPage },
productB,
binOne
);
}
);

test.afterEach(
async ({
stockMovementShowPage,
stockMovementService,
oldViewShipmentPage,
navbar,
transactionListPage,
putawayListPage,
productShowPage,
productEditPage,
}) => {
// Remove the pending 2nd putaway
await putawayListPage.goToPage();
await putawayListPage.isLoaded();
await putawayListPage.table.row(1).actionsButton.click();
await putawayListPage.table.clickDeleteOrderButton(1);

// Delete the 3 created transactions
await navbar.configurationButton.click();
await navbar.transactions.click();
for (let i = 0; i < 3; i++) {
await transactionListPage.deleteTransaction(1);
}

await deleteReceivedShipment({
stockMovementShowPage,
oldViewShipmentPage,
stockMovementService,
STOCK_MOVEMENT: inboundTwo,
});
await deleteReceivedShipment({
stockMovementShowPage,
oldViewShipmentPage,
stockMovementService,
STOCK_MOVEMENT: inboundOne,
});

// Remove preferred bins for A and B.
for (const product of [productA, productB]) {
await productShowPage.goToPage(product.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('Sort putaway by current bin, preferred bin and original order', async ({
stockMovementShowPage,
navbar,
createPutawayPage,
putawayDetailsPage,
stockMovementService,
receivingService,
supplierLocationService,
}) => {
// Extended timeout: those pages are loading slowly than others.
test.setTimeout(180_000);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙈


await test.step('create putaway for item A and complete it', async () => {
await stockMovementShowPage.goToPage(inboundOne.id);
await stockMovementShowPage.isLoaded();
await RefreshCachesUtils.refreshCaches({ navbar });
await navbar.inbound.click();
await navbar.createPutaway.click();
await createPutawayPage.isLoaded();

// Flatten the table (Show by -> Product) and select item A by name.
await createPutawayPage.showByStockMovementFilter.click();
await createPutawayPage.table
.rowByProductName(productA.name)
.checkbox.click();
await createPutawayPage.startPutawayButton.click();
await createPutawayPage.startStep.isLoaded();

// Putaway A into binTwo (its preferred bin is auto-suggested).
await createPutawayPage.startStep.table.row(0).putawayBinSelect.click();
await createPutawayPage.startStep.table
.row(0)
.getPutawayBin(binTwo.name)
.click();
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('create and receive 2nd inbound with items A, B and C', async () => {
const supplierLocation = await supplierLocationService.getLocation();
inboundTwo = await stockMovementService.createInbound({
originId: supplierLocation.id,
});
await stockMovementService.addItemsToInboundStockMovement(inboundTwo.id, [
{ productId: productA.id, quantity: 10 },
{ productId: productB.id, quantity: 10 },
{ productId: productC.id, quantity: 10 },
]);
await receiveInbound(
{ stockMovementService, receivingService },
inboundTwo,
[10, 10, 10]
);
});

const allProducts = [productA.name, productB.name, productC.name];

await test.step('go to create putaway page and start putaway for all items', async () => {
await stockMovementShowPage.goToPage(inboundTwo.id);
await stockMovementShowPage.isLoaded();
await RefreshCachesUtils.refreshCaches({ navbar });
await navbar.inbound.click();
await navbar.createPutaway.click();
await createPutawayPage.isLoaded();

// Flatten the table (Show by -> Product) and select items A, B, C by name.
await createPutawayPage.showByStockMovementFilter.click();
await createPutawayPage.table
.rowByProductName(productA.name)
.checkbox.click();
await createPutawayPage.table
.rowByProductName(productB.name)
.checkbox.click();
await createPutawayPage.table
.rowByProductName(productC.name)
.checkbox.click();
await createPutawayPage.startPutawayButton.click();
await createPutawayPage.startStep.isLoaded();
});

await test.step('assert original order of items', async () => {
await expect(createPutawayPage.startStep.sortButton).toContainText(
'Sort by current bins'
);
await expect
.poll(() =>
createPutawayPage.startStep.table.getProductsOrder(allProducts)
)
.toEqual([productA.name, productB.name, productC.name]);
});

await test.step('sort by current bin and assert order', async () => {
await createPutawayPage.startStep.sortButton.click();
await expect(createPutawayPage.startStep.sortButton).toContainText(
'Sort by preferred bin'
);
await expect
.poll(() =>
createPutawayPage.startStep.table.getProductsOrder(allProducts)
)
.toEqual([productB.name, productC.name, productA.name]);
});

await test.step('sort by preferred bin and assert order', async () => {
await createPutawayPage.startStep.sortButton.click();
await expect(createPutawayPage.startStep.sortButton).toContainText(
'Original order'
);
await expect
.poll(() =>
createPutawayPage.startStep.table.getProductsOrder(allProducts)
)
.toEqual([productC.name, productB.name, productA.name]);
});

await test.step('use original order button and assert order', async () => {
await createPutawayPage.startStep.sortButton.click();
await expect(createPutawayPage.startStep.sortButton).toContainText(
'Sort by current bins'
);
await expect
.poll(() =>
createPutawayPage.startStep.table.getProductsOrder(allProducts)
)
.toEqual([productA.name, productB.name, productC.name]);
});
});
});
26 changes: 26 additions & 0 deletions src/utils/productUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import ProductEditPage from '@/pages/product/productEdit/ProductEditPage';
import ProductShowPage from '@/pages/product/productShow/ProductShowPage';
import { LocationResponse, ProductResponse } from '@/types';

export async function assignPreferredBin(
{
productShowPage,
productEditPage,
}: {
productShowPage: ProductShowPage;
productEditPage: ProductEditPage;
},
product: ProductResponse,
bin: LocationResponse
) {
await productShowPage.goToPage(product.id);
await productShowPage.editProductkButton.click();
await productEditPage.inventoryLevelsTab.click();
await productEditPage.inventoryLevelsTabSection.createStockLevelButton.click();
await productEditPage.inventoryLevelsTabSection.createStockLevelModal.receivingTab.click();
await productEditPage.inventoryLevelsTabSection.createStockLevelModal.defaultPutawayLocation.click();
await productEditPage.inventoryLevelsTabSection.createStockLevelModal
.getDefaultPutawayLocation(bin.name)
.click();
await productEditPage.inventoryLevelsTabSection.createStockLevelModal.createButton.click();
}
Loading
Loading