-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HP-2382 Migrate PartCreateCest.php Codeception Test to Playwright and remove Legacy Test #185
Changes from 11 commits
0254577
70ab712
76baf83
5c1c839
dfc47f1
275f019
ed71b18
054d9cf
d97a608
7d1325d
0a54c35
bdf19bc
d82437a
3202572
5595d68
0a731cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { test } from "@hipanel-core/fixtures"; | ||
import {expect} from "@playwright/test"; | ||
import PartCreateView from "@hipanel-module-stock/page/PartCreateView"; | ||
import UniqueId from "@hipanel-core/helper/UniqueId"; | ||
import PartIndexView from "@hipanel-module-stock/page/PartIndexView"; | ||
import PartView from "@hipanel-module-stock/page/PartView"; | ||
|
||
function getPartData() { | ||
return { | ||
partno: 'CHASSIS EPYC 7402P', | ||
src_id: 'TEST-DS-01', | ||
dst_id: 'TEST-DS-02', | ||
serials: UniqueId.generate(`MG_TEST_PART`), | ||
move_descr: 'MG TEST MOVE', | ||
price: 200, | ||
currency: '$', | ||
company_id: 'Other', | ||
}; | ||
} | ||
|
||
test.describe('Part Management', () => { | ||
test('Ensure part management buttons work @hipanel-module-stock @manager', async ({ managerPage }) => { | ||
const partView = new PartCreateView(managerPage); | ||
await partView.navigate(); | ||
|
||
let n = await managerPage.locator('div.item').count(); | ||
expect(n).toBe(1); | ||
|
||
await partView.addPart(); | ||
expect(await managerPage.locator('div.item').count()).toBe(++n); | ||
|
||
await partView.addPart(); | ||
expect(await managerPage.locator('div.item').count()).toBe(++n); | ||
|
||
await partView.copyPart(); | ||
expect(await managerPage.locator('div.item').count()).toBe(++n); | ||
|
||
await partView.removePart(); | ||
expect(await managerPage.locator('div.item').count()).toBe(--n); | ||
|
||
await partView.removePart(); | ||
expect(await managerPage.locator('div.item').count()).toBe(--n); | ||
|
||
await partView.removePart(); | ||
expect(await managerPage.locator('div.item').count()).toBe(--n); | ||
}); | ||
|
||
test('Ensure part cannot be created without data @hipanel-module-stock @manager', async ({ managerPage }) => { | ||
const partView = new PartCreateView(managerPage); | ||
await partView.navigate(); | ||
await partView.save(); | ||
|
||
const errorMessages = [ | ||
'Part No. cannot be blank.', | ||
'Source cannot be blank.', | ||
'Destination cannot be blank.', | ||
'Serials cannot be blank.', | ||
'Move description cannot be blank.', | ||
'Purchase price cannot be blank.', | ||
'Currency cannot be blank.', | ||
]; | ||
|
||
for (const message of errorMessages) { | ||
await expect(managerPage.locator(`text=${message}`)).toBeVisible(); | ||
} | ||
}); | ||
|
||
test('Ensure a part can be created @hipanel-module-stock @manager', async ({ managerPage }) => { | ||
const partView = new PartCreateView(managerPage); | ||
const partIndexView = new PartIndexView(managerPage); | ||
await partView.navigate(); | ||
await partView.fillPartFields(getPartData()); | ||
await partView.save(); | ||
|
||
await partIndexView.seePartWasCreated(); | ||
}); | ||
|
||
test('Ensure multiple parts can be created @hipanel-module-stock @manager', async ({ managerPage }) => { | ||
const partView = new PartCreateView(managerPage); | ||
const partIndexView = new PartIndexView(managerPage); | ||
await partView.navigate(); | ||
await partView.fillPartFields(getPartData()); | ||
await partView.addPart(); | ||
await partView.fillPartFields(getPartData(), 1); | ||
await partView.save(); | ||
|
||
await partIndexView.seePartWasCreated(); | ||
}); | ||
|
||
test('Ensure a part can be created and then deleted @hipanel-module-stock @manager', async ({ managerPage }) => { | ||
const partCreateView = new PartCreateView(managerPage); | ||
const partIndexView = new PartIndexView(managerPage); | ||
|
||
await partCreateView.navigate(); | ||
await partCreateView.fillPartFields(getPartData()); | ||
await partCreateView.save(); | ||
|
||
await partIndexView.seePartWasCreated(); | ||
|
||
const partView = new PartView(managerPage); | ||
await partView.deletePart(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { test } from "@hipanel-core/fixtures"; | ||
import PartIndexView from "@hipanel-module-stock/page/PartIndexView"; | ||
import PartReplaceView from "@hipanel-module-stock/page/PartReplaceView"; | ||
import UniqueId from "@hipanel-core/helper/UniqueId"; | ||
|
||
const data = { | ||
filters: [ | ||
{ | ||
name: "move_descr_ilike", | ||
value: "test description" | ||
}, | ||
{ | ||
name: "model_types", | ||
value: "cpu" | ||
}, | ||
], | ||
replaceData: [ | ||
{ serialno: UniqueId.generate(`test`) }, | ||
{ serialno: UniqueId.generate(`test`) } | ||
], | ||
}; | ||
|
||
test.describe("Part Replacement", () => { | ||
test("Ensure parts can be replaced @hipanel-module-stock @manager", async ({ managerPage }) => { | ||
const partIndexView = new PartIndexView(managerPage); | ||
const partReplaceView = new PartReplaceView(managerPage); | ||
|
||
await partIndexView.navigate(); | ||
await partIndexView.applyFilters(data.filters); | ||
await partIndexView.selectPartsToReplace(1, data.replaceData.length); | ||
|
||
await partReplaceView.fillReplaceForm(data.replaceData); | ||
await partReplaceView.save(); | ||
|
||
await partIndexView.confirmReplacement(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import {Page} from "@playwright/test"; | ||
import Select2 from "@hipanel-core/input/Select2"; | ||
import PriceWithCurrency from "@hipanel-module-stock/input/PriceWithCurrency"; | ||
|
||
export default class PartForm { | ||
private page: Page; | ||
|
||
public constructor(page: Page) { | ||
this.page = page; | ||
} | ||
|
||
public async fillPartFields(partData: any, index: number = 0) { | ||
await Select2.field(this.page, this.selector('select', 'partno', index)).setValue(partData.partno); | ||
await Select2.field(this.page, this.selector('select', 'src_id', index)).setValue(partData.src_id); | ||
await Select2.field(this.page, this.selector('select', 'dst_ids', index)).setValue(partData.dst_id); | ||
|
||
await this.fillSerials(partData.serials, index); | ||
await this.page.fill(this.selector('input', 'move_descr', index), partData.move_descr); | ||
|
||
await PriceWithCurrency.field(this.page, 'part', index).setSumAndCurrency(partData.price, partData.currency); | ||
|
||
await this.page.selectOption(this.selector('select', 'company_id', index), partData.company_id); | ||
} | ||
|
||
private selector(type: string, name: string, index: number = 0): string { | ||
return `${type}[id=part-${index}-${name}]`; | ||
} | ||
|
||
public async fillSerials(serial: string, index: number = 0) { | ||
await this.page.fill(this.selector('input', 'serials', index), serial); | ||
} | ||
|
||
/** | ||
* It is strange, but in the same form on the /stock/part/replace page "serials" input has "serial" name | ||
*/ | ||
public async fillSerial(serial: string, index: number = 0) { | ||
await this.page.fill(this.selector('input', 'serial', index), serial); | ||
} | ||
|
||
public async save() { | ||
await this.page.click('button:has-text("Save")'); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import InputWithCurrency from "@hipanel-core/input/InputWithCurrency"; | ||
import {Page} from "@playwright/test"; | ||
|
||
export default class PriceWithCurrency extends InputWithCurrency{ | ||
static field(page: Page, formId: string, k: number): InputWithCurrency { | ||
return new PriceWithCurrency(page, `${formId}-${k}-price`); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import {Page} from "@playwright/test"; | ||
import PartForm from "@hipanel-module-stock/helper/PartForm"; | ||
|
||
export default class PartCreateView { | ||
private page: Page; | ||
private partForm: PartForm; | ||
|
||
public constructor(page: Page) { | ||
this.page = page; | ||
this.partForm = new PartForm(page); | ||
} | ||
|
||
public async navigate() { | ||
await this.page.goto('/stock/part/create'); | ||
} | ||
|
||
public async fillPartFields(partData: any, index: number = 0) { | ||
await this.partForm.fillPartFields(partData, index); | ||
} | ||
|
||
public async save() { | ||
await this.partForm.save(); | ||
} | ||
|
||
public async addPart() { | ||
await this.page.click("div.item:last-child button.add-item"); | ||
} | ||
|
||
public async removePart() { | ||
await this.page.click("div.item:last-child button.remove-item"); | ||
} | ||
|
||
public async copyPart() { | ||
await this.page.click("div.item:last-child button.copy"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,62 @@ | ||||||||||||||||||||||||||||||||||
import {expect, Page} from "@playwright/test"; | ||||||||||||||||||||||||||||||||||
import Index from "@hipanel-core/page/Index"; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
export default class PartIndexView { | ||||||||||||||||||||||||||||||||||
private page: Page; | ||||||||||||||||||||||||||||||||||
private index: Index; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
constructor(page: Page) { | ||||||||||||||||||||||||||||||||||
this.page = page; | ||||||||||||||||||||||||||||||||||
this.index = new Index(page); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
async navigate() { | ||||||||||||||||||||||||||||||||||
await this.page.goto("/stock/part/index?representation=common"); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
async applyFilters(filters: Array<{ name: string; value: string }>) { | ||||||||||||||||||||||||||||||||||
for (const filter of filters) { | ||||||||||||||||||||||||||||||||||
await this.index.setFilter(filter.name, filter.value); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
await this.index.submitSearchButton(); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
public async selectPartsToReplace(start: number, end: number) { | ||||||||||||||||||||||||||||||||||
await this.index.chooseRangeOfRowsOnTable(start, end); | ||||||||||||||||||||||||||||||||||
await this.index.clickDropdownBulkButton('Bulk actions', 'Replace'); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
public async confirmReplacement() { | ||||||||||||||||||||||||||||||||||
await this.index.hasNotification('Part has been replaced'); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
Comment on lines
+38
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainConsider handling single vs. multiple part replacements. The notification message only checks for a singular form ("Part has been replaced"). If multiple parts are replaced, the message might use a plural form. 🏁 Script executed: #!/bin/bash
# Description: Check if there are different notification messages for single vs multiple part replacements
# Look for notification strings in the codebase
echo "Searching for part replacement notification strings in the codebase..."
rg -i "part(s?)( have| has) been replaced" --type php --type js --type ts Length of output: 642 Action Required: Update Notification Handling for Multiple Part Replacements After searching the codebase, it’s confirmed that all part replacement notifications currently use the singular message ("Part has been replaced")—across controllers, messages, and tests. If the application supports replacing multiple parts at once, please consider implementing conditional logic (or pluralized messages) in both the notification generation and corresponding tests. If single replacements are the only supported scenario, adding a clarifying comment regarding this design decision would be beneficial. |
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
public async deleteItemOnTable(number: number) { | ||||||||||||||||||||||||||||||||||
await this.chooseNumberRowOnTable(number); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
await this.page.getByRole('button', { name: 'Delete' }).click(); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
this.page.on('dialog', async dialog => await dialog.accept()); | ||||||||||||||||||||||||||||||||||
await this.index.hasNotification('Part has been deleted'); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
Comment on lines
+42
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix event handler registration timing in deleteItemOnTable method. The dialog event handler is registered after clicking the delete button, which could lead to race conditions. The event handler should be registered before triggering the action. public async deleteItemOnTable(number: number) {
await this.chooseNumberRowOnTable(number);
+ this.page.on('dialog', async dialog => await dialog.accept());
await this.page.getByRole('button', { name: 'Delete' }).click();
-
- this.page.on('dialog', async dialog => await dialog.accept());
await this.index.hasNotification('Part has been deleted');
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
public async chooseNumberRowOnTable(number: number) { | ||||||||||||||||||||||||||||||||||
await this.index.chooseNumberRowOnTable(number); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
public async seePartWasCreated() { | ||||||||||||||||||||||||||||||||||
const rowNumber = 1; | ||||||||||||||||||||||||||||||||||
await this.index.hasNotification('Part has been created'); | ||||||||||||||||||||||||||||||||||
await this.index.closeNotification(); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// Ensure the current URL matches expected Move index URL | ||||||||||||||||||||||||||||||||||
await expect(this.page).toHaveURL(/\/stock\/move\/index\?MoveSearch%5Bid%5D=/); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// Get first row move ID from the index table | ||||||||||||||||||||||||||||||||||
const moveId = await this.index.getRowDataKeyByNumber(rowNumber); | ||||||||||||||||||||||||||||||||||
expect(moveId).not.toBeNull(); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// Wait /stock/part/view page to load | ||||||||||||||||||||||||||||||||||
await this.index.clickColumnOnTable('Parts', rowNumber); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { Page } from "@playwright/test"; | ||
import PartForm from "@hipanel-module-stock/helper/PartForm"; | ||
|
||
export default class PartReplaceView { | ||
private page: Page; | ||
private partForm: PartForm; | ||
|
||
constructor(page: Page) { | ||
this.page = page; | ||
this.partForm = new PartForm(page); | ||
} | ||
|
||
public async fillReplaceForm(replaceData: { serialno: string }[]) { | ||
let key = 0; | ||
for (const data of replaceData) { | ||
await this.partForm.fillSerial(data.serialno, key); | ||
key++; | ||
} | ||
} | ||
|
||
public async save() { | ||
await this.partForm.save(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import {Page} from "@playwright/test"; | ||
import Notification from "@hipanel-core/helper/Notification"; | ||
import DetailMenu from "@hipanel-core/helper/DetailMenu"; | ||
|
||
Comment on lines
+1
to
+4
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainVerify imports consistency with other page objects. Ensure that the import style is consistent with other page objects in the codebase. 🏁 Script executed: #!/bin/bash
# Check imports in other page objects for consistency
echo "Checking import styles in other page object files:"
fd -e ts -p "tests/playwright/page/.*View\.ts" --exec head -n 5 | grep -v "PartView.ts" Length of output: 876 Action: Unify Import Formatting Across Page Objects The file import {Page} from "@playwright/test";
import Notification from "@hipanel-core/helper/Notification";
import DetailMenu from "@hipanel-core/helper/DetailMenu"; A quick survey of similar page object files shows variations in style:
While the imports in |
||
export default class PartView { | ||
private page: Page; | ||
private notification: Notification; | ||
private detailMenu: DetailMenu; | ||
|
||
public constructor(page: Page) { | ||
this.page = page; | ||
this.notification = new Notification(page); | ||
this.detailMenu = new DetailMenu(page); | ||
this.registerAcceptDeleteDialogHandler(); | ||
} | ||
|
||
private registerAcceptDeleteDialogHandler() { | ||
// By default, dialogs are auto-dismissed by Playwright, so you don't have to handle them | ||
this.page.on('dialog', async dialog => await dialog.accept()); | ||
} | ||
|
||
public async deletePart() | ||
{ | ||
await this.detailMenu.clickDetailMenuItem("Delete"); | ||
await this.notification.hasNotification('Part has been deleted'); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add assertions to verify the replacement succeeded.
The test confirms that the form can be submitted, but doesn't verify that the replacement was actually successful. Consider adding assertions to check the results.
📝 Committable suggestion