Skip to content
Merged
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
23 changes: 22 additions & 1 deletion src/api/CycleCountService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import BaseServiceModel from '@/api/BaseServiceModel';
import { CYCLE_COUNT_BY_ID } from '@/constants/apiUrls';
import {
CYCLE_COUNT_BY_ID,
CYCLE_COUNT_REQUESTS_BATCH,
} from '@/constants/apiUrls';

class CycleCountService extends BaseServiceModel {
/**
Expand All @@ -16,6 +19,24 @@ class CycleCountService extends BaseServiceModel {
);
return apiResponse.ok();
}

/**
Deletes pending cycle count requests (candidates marked as "To Count" via
the batch-create requests endpoint, but not yet started). Used to clean up
when a test fails before reaching Start Count, since those requests don't
cascade-delete with anything else.
*/
async deletePendingRequests(
facilityId: string,
requestIds: string[]
): Promise<boolean> {
const params = new URLSearchParams();
requestIds.forEach((id) => params.append('id', id));
const apiResponse = await this.request.delete(
`${CYCLE_COUNT_REQUESTS_BATCH(facilityId)}?${params.toString()}`
);
return apiResponse.ok();
}
}

export default CycleCountService;
2 changes: 2 additions & 0 deletions src/constants/apiUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,5 @@ export const INVENTORY_IMPORT = (facilityId: string) =>
// CYCLE COUNT
export const CYCLE_COUNT_BY_ID = (facilityId: string, cycleCountId: string) =>
`${API}/facilities/${facilityId}/cycle-counts/${cycleCountId}`;
export const CYCLE_COUNT_REQUESTS_BATCH = (facilityId: string) =>
`${API}/facilities/${facilityId}/cycle-counts/requests/batch`;
1 change: 1 addition & 0 deletions src/setup/dataImport/cycleCountInventory.csv
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
Product code,Product,Lot number,Expiration date,Bin location,Quantity,Comment
7,CC-product-one,,,CC-BIN-1,100,
9,CC-product-three,,,CC-BIN-2,10,
10,CC-product-four,,,CC-BIN-3,30,
1 change: 1 addition & 0 deletions src/setup/dataImport/products.csv
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ Id,Active,ProductCode,ProductType,Name,ProductFamily,Category,GLAccount,Descript
,true,7,Default,CC-product-one,,Anesteshia,,,,,,,,,,,,,,,,,,,,,
,true,8,Default,CC-product-two,,Anesteshia,,,,,,,,,,,,,,,,,,,,,
,true,9,Default,CC-product-three,,ARVS,,,,,,,,,,,,,,,,,,,,,
,true,10,Default,CC-product-four,,Anesteshia,,,,,,,,,,,,,,,,,,,,,

166 changes: 166 additions & 0 deletions src/tests/cycleCount/useHyperlinksOnCCLists.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { Locator } from '@playwright/test';

import AppConfig from '@/config/AppConfig';
import { expect, test } from '@/fixtures/fixtures';

test.describe('Use hyperlinks on CC lists', () => {
const productCode = '10';
const productName = AppConfig.instance.products[productCode].name;
const facilityId = AppConfig.instance.locations.ccDepot.readId();

let cycleCountId: string | undefined;
// ids of the "mark as To Count" request(s), set as soon as they're
// created; only needed as a cleanup fallback if the test fails before
// Start Count turns them into an actual cycle count
let pendingRequestIds: string[] = [];

test.afterEach(async ({ cycleCountService }) => {
if (cycleCountId) {
await cycleCountService.deleteCycleCount(facilityId, cycleCountId);
return;
}
if (pendingRequestIds.length) {
await cycleCountService.deletePendingRequests(
facilityId,
pendingRequestIds
);
}
});

test('Use hyperlinks on CC lists', async ({
page,
navbar,
manageCycleCountPage,
countStepPage,
confirmToCountStepPage,
productShowPage,
}) => {
// several candidates-reload retries on top of the normal step time,
// comfortably over the default 60s test timeout
test.setTimeout(120_000);

const productId = AppConfig.instance.products[productCode].readId();
const stockCardLinkPattern = new RegExp(`showStockCard/${productId}`);

const getAllProductsRow = () =>
manageCycleCountPage.allProductsTable.rows.filter({
hasText: productName,
});

const openAllProductsTabAndSearch = async () => {
await expect(async () => {
await manageCycleCountPage.goToPage();
await manageCycleCountPage.isLoaded();
await manageCycleCountPage.openAllProductsTab();
await manageCycleCountPage.searchProduct(productName);
await expect(getAllProductsRow()).toHaveCount(1);
}).toPass({ timeout: 30_000, intervals: [2000, 3000, 5000] });
};

const assertNavigatesToStockCard = async (productLink: Locator) => {
await productLink.click();
await expect(page).toHaveURL(stockCardLinkPattern);
await expect(productShowPage.recordStockButton).toBeVisible();
};

await test.step('Search product on All Products tab and use its hyperlink to open the stock card', async () => {
await openAllProductsTabAndSearch();

const row = manageCycleCountPage.allProductsTable.row(0);
await expect(row.product).toHaveAttribute('href', stockCardLinkPattern);

await assertNavigatesToStockCard(row.product);
});

await test.step('Mark product as To Count', async () => {
await openAllProductsTabAndSearch();

const row = manageCycleCountPage.allProductsTable.row(0);
await row.checkbox.check();

const [requestsResponse] = await Promise.all([
page.waitForResponse(
(res) =>
res.url().includes('/requests/batch') &&
res.request().method() === 'POST'
),
manageCycleCountPage.markAsToCountButton.click(),
]);
const { data: requestsData } = await requestsResponse.json();
pendingRequestIds = requestsData.map((item: { id: string }) => item.id);

await expect(manageCycleCountPage.toCountTab).toHaveClass('active-tab');
});

await test.step('Open Perform Cycle Count from the menu and assert product, hyperlink, status and unchecked checkbox on To Count tab', async () => {
await navbar.inventory.click();
await expect(navbar.performCycleCount).toBeVisible();
await page.keyboard.press('Escape');
await page.mouse.move(0, 0);

await expect(async () => {
await manageCycleCountPage.goToPerformCycleCount();
await manageCycleCountPage.isLoaded();
await expect(manageCycleCountPage.toCountTab).toHaveClass(
'active-tab'
);
await manageCycleCountPage.toCountTable.isLoaded();
await manageCycleCountPage.searchProduct(productName);
await expect(manageCycleCountPage.toCountTable.rows).toHaveCount(1);
}).toPass({ timeout: 30_000, intervals: [2000, 3000, 5000] });

const row = manageCycleCountPage.toCountTable.row(0);
await expect(row.product).toContainText(productName);
await expect(row.product).toHaveAttribute('href', stockCardLinkPattern);
await expect(row.status).toHaveText('To count');
await expect(row.checkbox).not.toBeChecked();
});

await test.step('Use the hyperlink on To Count tab to open the stock card', async () => {
const row = manageCycleCountPage.toCountTable.row(0);
await assertNavigatesToStockCard(row.product);
});

await test.step('Go back, start count with a discrepancy and choose Not now', async () => {
await expect(async () => {
await manageCycleCountPage.goToPerformCycleCount();
await manageCycleCountPage.isLoaded();
await manageCycleCountPage.toCountTable.isLoaded();
await manageCycleCountPage.searchProduct(productName);
await expect(manageCycleCountPage.toCountTable.rows).toHaveCount(1);
}).toPass({ timeout: 30_000, intervals: [2000, 3000, 5000] });
await manageCycleCountPage.toCountTable.row(0).checkbox.check();

const [response] = await Promise.all([
page.waitForResponse((res) => res.url().includes('/start/batch')),
manageCycleCountPage.startCountButton.click(),
]);
const { data } = await response.json();
cycleCountId = data[0].id;
await countStepPage.isLoaded();
await expect(async () => {
await countStepPage.countStepTable.row(0).fillQuantityCounted('40');
await countStepPage.nextButton.click();
await confirmToCountStepPage.isLoaded();
}).toPass({ timeout: 20_000, intervals: [1000, 2000, 3000] });

await confirmToCountStepPage.saveButton.click();
const resolveDialog = confirmToCountStepPage.resolveDiscrepancyDialog;
await resolveDialog.isLoaded();
await resolveDialog.notNowButton.click();
await expect(manageCycleCountPage.toResolveTab).toHaveClass(
'active-tab'
);
});

await test.step('Open To Resolve tab and use its hyperlink to open the stock card', async () => {
await manageCycleCountPage.toResolveTable.isLoaded();
await expect(manageCycleCountPage.toResolveTable.rows).toHaveCount(1);

const row = manageCycleCountPage.toResolveTable.row(0);
await expect(row.product).toHaveAttribute('href', stockCardLinkPattern);

await assertNavigatesToStockCard(row.product);
});
});
});
15 changes: 15 additions & 0 deletions src/tests/receiving/lotExpirySystemUpdateOnReceiving.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ test.describe('Lot number system expiry date modification on receiving workflow'
);

test('Edit lot expiration date on a new lot does not render a confirmation modal', async ({
page,
stockMovementShowPage,
receivingPage,
stockMovementService,
Expand Down Expand Up @@ -122,6 +123,10 @@ test.describe('Lot number system expiry date modification on receiving workflow'
await receivingPage.receivingStep.editModal.table
.row(1)
.receivingNowField.numberbox.fill('10');
// the quantity cell only commits its typed value to form state on
// blur, so it must be blurred before the date field is touched, or
// the uncommitted value is lost
await page.keyboard.press('Tab');
await receivingPage.receivingStep.editModal.table
.row(1)
.expiryDatePickerField.fillWithFormat(
Expand Down Expand Up @@ -276,6 +281,7 @@ test.describe('Lot number system expiry date modification on receiving workflow'
);

test('Should not update expiration date on an existing lot when canceling popup', async ({
page,
stockMovementShowPage,
receivingPage,
stockMovementService,
Expand Down Expand Up @@ -339,6 +345,10 @@ test.describe('Lot number system expiry date modification on receiving workflow'
await receivingPage.receivingStep.editModal.table
.row(1)
.receivingNowField.numberbox.fill('10');
// the quantity cell only commits its typed value to form state on
// blur, so it must be blurred before the date field is touched, or
// the uncommitted value is lost
await page.keyboard.press('Tab');
await receivingPage.receivingStep.editModal.table
.row(1)
.expiryDatePickerField.fillWithFormat(
Expand Down Expand Up @@ -402,6 +412,7 @@ test.describe('Lot number system expiry date modification on receiving workflow'
});

test('Should update expiration date on an existing lot when confirming popup', async ({
page,
stockMovementShowPage,
receivingPage,
stockMovementService,
Expand Down Expand Up @@ -465,6 +476,10 @@ test.describe('Lot number system expiry date modification on receiving workflow'
await receivingPage.receivingStep.editModal.table
.row(1)
.receivingNowField.numberbox.fill('10');
// the quantity cell only commits its typed value to form state on
// blur, so it must be blurred before the date field is touched, or
// the uncommitted value is lost
await page.keyboard.press('Tab');
await receivingPage.receivingStep.editModal.table
.row(1)
.expiryDatePickerField.fillWithFormat(
Expand Down
Loading