Skip to content

Commit 206ecb4

Browse files
authored
OBPIH-8025 Fix receiving workflow tests (#105)
* OBPIH-8025 unskip assert bin location field test * OBPIH-8025 Unskip assert creation of goods receipt note * OBPIH-8025 Unskip assert creation of receiving bin * OBPIH-8025 Unskip assert quantity inputs * OBPIH-8025 Unskip cancel remaining quantity * OBPIH-8025 Unskip edit bin location when receive * OBPIH-8025 Unskip edit original line quantity to 0 * OBPIH-8025 Unskip edit in receiving * OBPIH-8025 Unskip lot and expiry system update on receiving * OBPIH-8025 Unskip receive to hold bin * OBPIH-8025 Unskip sort by alphabetical order and remain inputs * OBPIH-8025 Unskip validations on edit and receive * OBPIH-8025 Fix failing tests * OBPIH-8025 Fix failing tests * OBPIH-8025 Fix for tooltip wrapper * OBPIH-8025 Usage new data-testid and remove force clicks * OBPIH-8025 Unskip assert recipient field * OBPIH-8025 Unskip validations on deliver on date * OBPIH-8025 Fix pdf warnings
1 parent eea6a7b commit 206ecb4

27 files changed

Lines changed: 909 additions & 650 deletions

‎src/components/DatePicker.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
import { expect } from '@playwright/test';
2+
13
import FormField from '@/components/FormField';
24
import { formatDate } from '@/utils/DateUtils';
35

6+
const ordinal = (day: number) => {
7+
if (day % 100 >= 11 && day % 100 <= 13) {
8+
return `${day}th`;
9+
}
10+
return `${day}${{ 1: 'st', 2: 'nd', 3: 'rd' }[day % 10] ?? 'th'}`;
11+
};
12+
413
class DatePicker extends FormField {
514
get datePickerPopup() {
615
return this.page.locator('.react-datepicker');
@@ -15,6 +24,37 @@ class DatePicker extends FormField {
1524
await this.page.keyboard.press('Enter');
1625
}
1726

27+
/**
28+
For date fields that open a calendar instead of taking typed input (the
29+
field itself is a button). Picks the day in the calendar, which keeps the
30+
time that was selected before.
31+
*/
32+
async selectDate(date: Date) {
33+
await this.field.getByRole('button').first().click();
34+
await expect(this.page.getByRole('option').first()).toBeVisible();
35+
// e.g. "Choose Saturday, October 10th, 2026"
36+
const day = this.page.getByRole('option', {
37+
name: `Choose ${formatDate(date, 'dddd, MMMM')} ${ordinal(
38+
date.getDate()
39+
)}, ${date.getFullYear()}`,
40+
});
41+
// the calendar opens on the selected month, so move to the month of the
42+
// date when it isn't shown (days of the neighbouring months are listed too)
43+
if (!(await day.isVisible())) {
44+
const monthButton = date > new Date() ? 'Next Month' : 'Previous Month';
45+
await this.page.getByRole('button', { name: monthButton }).click();
46+
}
47+
await day.click();
48+
}
49+
50+
// fields that don't show the error under them show it in a tooltip on hover
51+
async assertErrorTooltip(message: string) {
52+
await this.field.hover();
53+
await expect(
54+
this.page.getByRole('tooltip').getByText(message)
55+
).toBeVisible();
56+
}
57+
1858
async fillWithFormat(date: Date, format: string) {
1959
// TODO: This is temporary solution until we figure out the sluggishness of the date picker
2060
await this.page.waitForTimeout(1000);

‎src/components/FormField.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ class FormField extends BasePageModel {
4040
}
4141

4242
async assertHasError() {
43-
expect(await this.hasError()).toBeTruthy();
43+
// the error can show up a moment after the value is entered, so retry
44+
await expect.poll(() => this.hasError()).toBeTruthy();
4445
}
4546

4647
async assertFieldWithErrorIsVisible(error: string) {

‎src/components/NewAlertPopup.ts‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,25 @@ import { Page } from '@playwright/test';
33
import BasePageModel from '@/pages/BasePageModel';
44

55
class NewAlertPopup extends BasePageModel {
6-
constructor(page: Page) {
6+
private confirmLabel: string;
7+
private cancelLabel: string;
8+
9+
constructor(page: Page, confirmLabel = 'Yes', cancelLabel = 'No') {
710
super(page);
11+
this.confirmLabel = confirmLabel;
12+
this.cancelLabel = cancelLabel;
813
}
914

1015
get tableDialog() {
1116
return this.page.getByTestId('modal-with-table');
1217
}
1318

14-
get yesButton() {
15-
return this.tableDialog.getByRole('button', { name: 'Yes' });
19+
get confirmButton() {
20+
return this.tableDialog.getByRole('button', { name: this.confirmLabel });
1621
}
1722

18-
get noButton() {
19-
return this.tableDialog.getByRole('button', { name: 'No' });
23+
get cancelButton() {
24+
return this.tableDialog.getByRole('button', { name: this.cancelLabel });
2025
}
2126
}
2227

‎src/pages/receiving/components/CheckTable.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,22 @@ class CheckTable extends BasePageModel {
3535
return new Row(this.page, this.itemRows.nth(index));
3636
}
3737

38+
// the order of the items isn't guaranteed to match the receiving step, so
39+
// find the row by its product code instead of its position
40+
rowByProductCode(productCode: string) {
41+
return new Row(
42+
this.page,
43+
this.rows.filter({
44+
has: this.page
45+
.locator('[aria-label="Code"]')
46+
.getByText(productCode, { exact: true }),
47+
})
48+
);
49+
}
50+
3851
getColumnHeader(columnName: string) {
3952
return this.table
40-
.locator('.rt-thead')
53+
.getByTestId('table-header')
4154
.getByText(columnName, { exact: true });
4255
}
4356

@@ -62,6 +75,14 @@ class Row extends BasePageModel {
6275
return this.row.locator('[aria-label="Code"]');
6376
}
6477

78+
get status() {
79+
return this.row.locator('[aria-label="Status"]');
80+
}
81+
82+
get recipient() {
83+
return this.row.locator('[aria-label="Recipient"]');
84+
}
85+
6586
get cancelRemainingCheckbox() {
6687
return this.row
6788
.locator('[data-column-id="cancelRemaining"]')

‎src/pages/receiving/components/EditModal.ts‎

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,23 @@ class EditModal extends BasePageModel {
1212
}
1313

1414
get modal() {
15-
return this.page.locator('.ReactModal__Content');
15+
return this.page.getByTestId('receiving-edit-line-item-modal');
1616
}
1717

1818
async isLoaded() {
1919
await expect(this.modal).toBeVisible();
2020
}
2121

2222
get saveButton() {
23-
return this.modal.getByRole('button', { name: 'Save' });
23+
return this.modal.getByRole('button', { name: 'Save', exact: true });
2424
}
2525

2626
get cancelButton() {
2727
return this.modal.getByRole('button', { name: 'Cancel' });
2828
}
2929

3030
get addLineButton() {
31-
return this.modal.getByRole('button', { name: 'Add line' });
32-
}
33-
34-
get informationAboutEditedQtyNotMatchingShippedQty() {
35-
return this.modal
36-
.locator('.font-weight-bold font-red-ob')
37-
.getByText(
38-
'The total edited quantity does not match the original quantity shipped.'
39-
);
31+
return this.modal.getByTestId('add-new-record');
4032
}
4133
}
4234

‎src/pages/receiving/components/EditModalTable.ts‎

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,45 +10,60 @@ class EditModalTable extends BasePageModel {
1010
}
1111

1212
get table() {
13-
return this.page.locator('#modalForm');
13+
return this.page.getByTestId('edit-modal-receiving-table');
1414
}
1515

1616
get rows() {
1717
return this.table.getByRole('row');
1818
}
1919

20+
// rows are 1-indexed by convention (row 1 is the first item), matching the
21+
// rest of the receiving page objects. The header row doesn't carry
22+
// role="row", so it doesn't occupy index 0 on its own.
2023
row(index: number) {
21-
return new Row(this.page, this.rows.nth(index));
24+
return new Row(this.page, this.rows.nth(index - 1));
2225
}
2326
}
2427

2528
class Row extends BasePageModel {
2629
row: Locator;
2730
lotNumberField: TextField;
2831
expiryDatePickerField: DatePicker;
29-
quantityShippedField: TextField;
32+
receivingNowField: TextField;
3033

3134
constructor(page: Page, row: Locator) {
3235
super(page);
3336
this.row = row;
34-
this.lotNumberField = new TextField(page, 'Lot', row);
35-
this.expiryDatePickerField = new DatePicker(page, 'Expiry', row);
36-
this.quantityShippedField = new TextField(page, 'Quantity shipped', row);
37+
this.lotNumberField = new TextField(page, 'Lot/SN', row);
38+
this.expiryDatePickerField = new DatePicker(page, 'Exp Date', row);
39+
this.receivingNowField = new TextField(page, 'Receiving now', row);
40+
}
41+
42+
get binLocationSelect() {
43+
return this.row.locator('[aria-label="Location"]');
44+
}
45+
46+
get clearBinLocationButton() {
47+
return this.binLocationSelect.getByTestId('custom-select-clear');
48+
}
49+
50+
get productSelect() {
51+
return this.row.locator('[aria-label="Product"]');
3752
}
3853

3954
get clearProductSelect() {
40-
return this.row.locator('.react-select__clear-indicator');
55+
return this.productSelect.getByTestId('custom-select-clear');
4156
}
4257

4358
async getProductSelect(name: string) {
44-
await this.row
59+
await this.productSelect
4560
.getByTestId('custom-select-element')
4661
.getByRole('textbox')
4762
.fill(name);
4863
await this.page
4964
.getByTestId('custom-select-dropdown-menu')
50-
.locator('.react-select__option')
51-
.nth(0)
65+
.getByText(name)
66+
.first()
5267
.click();
5368
}
5469
}

‎src/pages/receiving/components/ReceivingTable.ts‎

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,53 @@ class ReceivingTable extends BasePageModel {
2323
return new Row(this.page, this.rows.nth(index - 1));
2424
}
2525

26+
// the order of the items depends on the sorting, so find the row by its
27+
// product code instead of its position
28+
rowByProductCode(productCode: string) {
29+
return new Row(
30+
this.page,
31+
this.rows.filter({
32+
has: this.page
33+
.locator('[aria-label="Code"]')
34+
.getByText(productCode, { exact: true }),
35+
})
36+
);
37+
}
38+
2639
getColumnHeader(columnName: string) {
2740
return this.table
28-
.locator('.rt-thead')
41+
.getByTestId('table-header')
2942
.getByText(columnName, { exact: true });
3043
}
3144

3245
getCellValue(row: number, column: string) {
3346
return this.rows.nth(row - 1).locator(`[aria-label="${column}"]`);
3447
}
48+
49+
// "Autofill" select in the Location column header (visible only when the
50+
// "Show Putaway" switch is on)
51+
get locationAutofillSelect() {
52+
return this.table.getByTestId('location-autofill');
53+
}
54+
55+
// shown when the autofill would overwrite a location the user has changed
56+
get autofillLocationConfirmDialog() {
57+
return this.page.getByTestId('confirm-modal');
58+
}
59+
60+
get acceptAutofillLocationConfirmDialog() {
61+
return this.autofillLocationConfirmDialog.getByRole('button', {
62+
name: 'Yes',
63+
});
64+
}
65+
66+
async autofillLocation(option: string) {
67+
await this.locationAutofillSelect.click();
68+
await this.page
69+
.getByTestId('custom-select-dropdown-menu')
70+
.getByText(option, { exact: true })
71+
.click();
72+
}
3573
}
3674

3775
class Row extends BasePageModel {
@@ -52,9 +90,42 @@ class Row extends BasePageModel {
5290
return this.row.getByRole('button', { name: 'Comment' });
5391
}
5492

93+
// visible only when the "Show Putaway" switch is on
94+
get binLocationSelect() {
95+
return this.row.locator('[aria-label="Location"]');
96+
}
97+
98+
// the selected value only, without the screen reader text react-select
99+
// renders next to it while focused
100+
get selectedBinLocation() {
101+
return this.binLocationSelect.getByTestId('custom-select-value');
102+
}
103+
104+
get clearBinLocationButton() {
105+
return this.binLocationSelect.getByTestId('custom-select-clear');
106+
}
107+
108+
getBinLocation(binLocation: string) {
109+
return this.page
110+
.getByTestId('custom-select-dropdown-menu')
111+
.getByText(binLocation, { exact: true });
112+
}
113+
114+
async selectBinLocation(binLocation: string) {
115+
await this.binLocationSelect.click();
116+
await this.page.keyboard.type(binLocation);
117+
await this.getBinLocation(binLocation).click();
118+
}
119+
55120
getItem(name: string) {
56121
return this.row.locator('[aria-label="Product"]').getByText(name);
57122
}
123+
124+
// shown only when at least one item has a recipient, and read-only (it can
125+
// only be changed in the edit modal)
126+
get recipient() {
127+
return this.row.locator('[aria-label="Recipient"]');
128+
}
58129
}
59130

60131
export default ReceivingTable;

‎src/pages/receiving/steps/CheckStep.ts‎

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,49 +21,38 @@ class CheckStep extends BasePageModel {
2121
});
2222
}
2323

24+
// the buttons are shown both above the table and at the bottom of the page,
25+
// so use the ones at the bottom
26+
get wizardButtons() {
27+
return this.page.getByTestId('wizard-page-buttons');
28+
}
29+
2430
get receiveShipmentButton() {
25-
return this.page
26-
.locator('.submit-buttons')
27-
.getByRole('button', { name: 'Complete Receipt' });
31+
return this.wizardButtons.getByRole('button', { name: 'Complete Receipt' });
2832
}
2933

3034
get shimpentInformation() {
3135
return this.page.getByTestId('confirm-receipt-details');
3236
}
3337

3438
get originField() {
35-
return this.shimpentInformation
36-
.locator('.item-details__field')
37-
.filter({ hasText: 'Origin:' })
38-
.locator('.item-details__value');
39+
return this.shimpentInformation.getByTestId('confirm-receipt-origin');
3940
}
4041

4142
get destinationField() {
42-
return this.shimpentInformation
43-
.locator('.item-details__field')
44-
.filter({ hasText: 'Destination:' })
45-
.locator('.item-details__value');
43+
return this.shimpentInformation.getByTestId('confirm-receipt-destination');
4644
}
4745

4846
get shippedOnField() {
49-
return this.shimpentInformation
50-
.locator('.item-details__field')
51-
.filter({ hasText: 'Shipped on:' })
52-
.locator('.item-details__value');
47+
return this.shimpentInformation.getByTestId('confirm-receipt-shipped-on');
5348
}
5449

5550
get cancelAllRemainingButton() {
5651
return this.page.getByRole('button', { name: 'Cancel All Remaining' });
5752
}
5853

59-
get validationOnDeliveredOnPastDatePopup() {
60-
return this.page
61-
.locator('.s-alert-box-inner')
62-
.getByText('Must occur on or after Actual Shipping Date');
63-
}
64-
6554
get backToEditButton() {
66-
return this.page.getByRole('button', { name: 'Back to edit' });
55+
return this.wizardButtons.getByRole('button', { name: 'Back to Receive' });
6756
}
6857
}
6958

0 commit comments

Comments
 (0)