Skip to content

Commit

Permalink
test: page model refactor for dropdown and select components [INFENG-694
Browse files Browse the repository at this point in the history
] (#9362)
  • Loading branch information
JComins000 authored May 14, 2024
1 parent 68b7116 commit 566b6af
Show file tree
Hide file tree
Showing 31 changed files with 603 additions and 413 deletions.
3 changes: 1 addition & 2 deletions webui/react/src/e2e/fixtures/auth.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ export class AuthFixture {
}

async logout(): Promise<void> {
await this.signInPage.nav.sidebar.headerDropdown.pwLocator.click();
await this.signInPage.nav.sidebar.headerDropdown.signOut.pwLocator.click();
await (await this.signInPage.nav.sidebar.headerDropdown.open()).signOut.pwLocator.click();
await this.#page.waitForURL(/login/);
}
}
13 changes: 6 additions & 7 deletions webui/react/src/e2e/fixtures/user.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,7 @@ export class UserFixture {
*/
async editUser(user: User, edit: UserArgs = {}): Promise<User> {
const row = await this.userManagementPage.getRowByUsernameSearch(user.username);
await row.actions.pwLocator.click();
await row.actions.edit.pwLocator.click();
await (await row.actions.open()).edit.pwLocator.click();
await expect(this.userManagementPage.createUserModal.pwLocator).toBeVisible();
await expect(this.userManagementPage.createUserModal.header.title.pwLocator).toContainText(
'Edit User',
Expand Down Expand Up @@ -198,11 +197,10 @@ export class UserFixture {
await this.userManagementPage.table.table.headRow.selectAll.pwLocator.click();
await expect(this.userManagementPage.table.table.headRow.selectAll.pwLocator).toBeChecked();
// open group actions
await this.userManagementPage.actions.pwLocator.click();
await this.userManagementPage.actions.status.pwLocator.click();
await (await this.userManagementPage.actions.open()).status.pwLocator.click();
// deactivate
await this.userManagementPage.changeUserStatusModal.pwLocator.waitFor();
await this.userManagementPage.changeUserStatusModal.status.pwLocator.click();
await this.userManagementPage.changeUserStatusModal.status.openMenu();
await this.userManagementPage.changeUserStatusModal.status.deactivate.pwLocator.click();
await this.userManagementPage.changeUserStatusModal.footer.submit.pwLocator.click();
for (const id of ids) {
Expand All @@ -228,8 +226,9 @@ export class UserFixture {
}
await expect(async () => {
// user table can flake if running in parrallel
const actions = (await this.userManagementPage.getRowByUsernameSearch(user.username)).actions;
await actions.pwLocator.click();
const actions = await (
await this.userManagementPage.getRowByUsernameSearch(user.username)
).actions.open();
if (
(await actions.state.pwLocator.textContent()) !== (activate ? 'Activate' : 'Deactivate')
) {
Expand Down
4 changes: 2 additions & 2 deletions webui/react/src/e2e/models/BaseComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export type NamedComponentArgs =
* @param {CanBeParent} obj.parent - The parent used to locate this BaseComponent
* @param {string} obj.selector - Used as a selector uesd to locate this object
*/
export class BaseComponent implements ModelBasics {
export class BaseComponent implements ComponentBasics {
protected _selector: string;
readonly _parent: CanBeParent;
protected _locator: Locator | undefined;
Expand Down Expand Up @@ -82,7 +82,7 @@ export class BaseComponent implements ModelBasics {
* @param {object} obj
* @param {CanBeParent} obj.parent - The parent used to locate this BaseComponent
*/
export class BaseReactFragment implements ModelBasics {
export class BaseReactFragment implements ComponentBasics {
readonly _parent: CanBeParent;

constructor({ parent }: ComponentArgBasics) {
Expand Down
89 changes: 89 additions & 0 deletions webui/react/src/e2e/models/ant/Dropdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { BaseComponent, ComponentBasics } from 'e2e/models/BaseComponent';
import { BasePage } from 'e2e/models/BasePage';

interface requiresRoot {
root: BasePage;
}

interface DropdownArgsWithoutChildNode extends requiresRoot {
childNode?: never;
openMethod: () => Promise<void>;
}

interface DropdownArgsWithChildNode extends requiresRoot {
childNode: ComponentBasics;
openMethod?: () => Promise<void>;
}

type DropdownArgs = DropdownArgsWithoutChildNode | DropdownArgsWithChildNode;

/**
* Returns a representation of the Dropdown component from Ant.
* Until the dropdown component supports test ids, this model will match any open dropdown.
* This constructor represents the contents in antd/es/dropdown/index.d.ts.
*
* The dropdown can be opened by calling the open method. By default, the open method clicks on the child node. Sometimes you might even need to provide both optional arguments, like when a child node is present but impossible to click on due to canvas behavior.
* @param {object} obj
* @param {BasePage} obj.root - root of the page
* @param {ComponentBasics} [obj.childNode] - optional if `openMethod` is present. It's the element we click on to open the dropdown.
* @param {Function} [obj.openMethod] - optional if `childNode` is present. It's the method to open the dropdown.
*/
export class Dropdown extends BaseComponent {
readonly openMethod: () => Promise<void>;
readonly childNode: ComponentBasics | undefined;
constructor({ root, childNode, openMethod }: DropdownArgs) {
super({
parent: root,
selector: '.ant-dropdown ul.ant-dropdown-menu:visible',
});
if (childNode !== undefined) {
this.childNode = childNode;
}
this.openMethod =
openMethod ||
(async () => {
if (this.childNode === undefined) {
// We should never be able to throw this error. In the constructor, we
// either provide a childNode or replace this method.
throw new Error('This dropdown does not have a child node to click on.');
}
await this.childNode.pwLocator.click();
});
}

/**
* Opens the dropdown.
* @returns {Promise<this>} - the dropdown for further actions
*/
async open(): Promise<this> {
await this.openMethod();
return this;
}

/**
* Returns a representation of a dropdown menu item with the specified id.
* @param {string} id - the id of the menu item
*/
menuItem(id: string): BaseComponent {
return new BaseComponent({
parent: this,
selector: `li.ant-dropdown-menu-item[data-menu-id$="${id}"]`,
});
}

/**
* Returns a representation of a dropdown menu item. Since order is not
* guaranteed, make sure to verify the contents of the menu item.
*
* It's better to prefer the menuItem method and to fall back on this.
* For example, there are some dropdowns which populate with dynamic data. Or
* maybe we could enter some sort of search filter and select the first item.
* @param {number} n - the number of the menu item
*/
nthMenuItem(n: number): BaseComponent {
return new BaseComponent({
parent: this,
selector: `li.ant-dropdown-menu-item:nth-of-type(${n})`,
});
}
}
38 changes: 38 additions & 0 deletions webui/react/src/e2e/models/ant/Modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { BaseComponent, NamedComponent } from 'e2e/models/BaseComponent';

/**
* Returns a representation of the Modal component from Ant.
* This constructor represents the contents in antd/es/modal/index.d.ts.
* @param {object} obj
* @param {CanBeParent} obj.parent - The parent used to locate this Modal
* @param {string} obj.selector - Used instead of `defaultSelector`
*/
export class Modal extends NamedComponent {
readonly defaultSelector = '.ant-modal-content';
readonly header = new ModalHeader({ parent: this, selector: '.ant-modal-header' });
readonly body = new BaseComponent({ parent: this, selector: '.ant-modal-body' });
readonly footer = new ModalFooter({ parent: this, selector: '.ant-modal-footer' });
}

/**
* Returns a representation of the Modal's Footer component from Ant.
* This constructor represents the footer in antd/es/modal/index.d.ts..
* @param {object} obj
* @param {CanBeParent} obj.parent - The parent used to locate this Modal
* @param {string} obj.selector - Used instead of `defaultSelector`
*/
class ModalHeader extends BaseComponent {
readonly title = new BaseComponent({ parent: this, selector: '.ant-modal-title' });
}

/**
* Returns a representation of the Modal's Footer component from Ant.
* This constructor represents the footer in antd/es/modal/index.d.ts..
* @param {object} obj
* @param {implementsGetLocator} obj.parent - The parent used to locate this Modal
* @param {string} obj.selector - Used instead of `defaultSelector`
*/
class ModalFooter extends BaseComponent {
readonly submit = new BaseComponent({ parent: this, selector: '[type="submit"]' });
readonly cancel = new BaseComponent({ parent: this, selector: '[type="cancel"]' });
}
28 changes: 28 additions & 0 deletions webui/react/src/e2e/models/ant/Notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BaseComponent, NamedComponent } from 'e2e/models/BaseComponent';

/**
* Returns a representation of the Notification component from Ant.
* This constructor represents the contents in antd/es/notification/index.d.ts.
* @param {object} obj
* @param {CanBeParent} obj.parent - The parent used to locate this Notification
* @param {string} obj.selector - Used instead of `defaultSelector`
*/

export class Notification extends NamedComponent {
readonly defaultSelector = '.ant-notification';
static readonly selectorTopRight = '.ant-notification-topRight';
static readonly selectorBottomRight = '.ant-notification-bottomRight';
readonly alert = new BaseComponent({ parent: this, selector: '[role="alert"]' });
readonly message = new BaseComponent({
parent: this.alert,
selector: '.ant-notification-notice-message',
});
readonly description = new BaseComponent({
parent: this.alert,
selector: '.ant-notification-notice-description',
});
readonly close = new BaseComponent({
parent: this,
selector: 'a.ant-notification-notice-close',
});
}
48 changes: 48 additions & 0 deletions webui/react/src/e2e/models/ant/Pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Locator } from '@playwright/test';

import { BaseComponent, NamedComponent } from 'e2e/models/BaseComponent';
import { Select } from 'e2e/models/hew/Select';

/**
* Returns the representation of a Table Pagination from Ant.
* This constructor represents the Table in antd/es/pagination/index.d.ts.
* @param {object} obj
* @param {CanBeParent} obj.parent - The parent used to locate this Pagination
* @param {string} obj.selector - Used as a selector uesd to locate this object
*/
export class Pagination extends NamedComponent {
readonly defaultSelector = '.ant-pagination';
readonly previous = new BaseComponent({
parent: this,
selector: 'li.ant-pagination-prev',
});
readonly next = new BaseComponent({
parent: this,
selector: 'li.ant-pagination-next',
});
readonly #options: BaseComponent = new BaseComponent({
parent: this,
selector: 'li.ant-pagination-options',
});
readonly perPage = new PaginationSelect({
parent: this.#options,
selector: '.ant-pagination-options-size-changer',
});
pageButtonLocator(n: number): Locator {
return this.pwLocator.locator(`.ant-pagination-item.ant-pagination-item-${n}`);
}
}

/**
* Returns the representation of a Table Pagination.
* This constructor represents the Table in src/components/Table/Table.tsx.
* @param {object} obj
* @param {parentTypes} obj.parent - The parent used to locate this Pagination
* @param {string} obj.selector - Used as a selector uesd to locate this object
*/
class PaginationSelect extends Select {
readonly perPage10 = this.menuItem('10 / page');
readonly perPage20 = this.menuItem('20 / page');
readonly perPage50 = this.menuItem('50 / page');
readonly perPage100 = this.menuItem('100 / page');
}
61 changes: 61 additions & 0 deletions webui/react/src/e2e/models/ant/Popover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { BaseComponent, ComponentBasics } from 'e2e/models/BaseComponent';
import { BasePage } from 'e2e/models/BasePage';

interface requiresRoot {
root: BasePage;
}

interface PopoverArgsWithoutChildNode extends requiresRoot {
childNode?: never;
openMethod: () => Promise<void>;
}

interface PopoverArgsWithChildNode extends requiresRoot {
childNode: ComponentBasics;
openMethod?: () => Promise<void>;
}

type PopoverArgs = PopoverArgsWithoutChildNode | PopoverArgsWithChildNode;
/**
* Returns a representation of the Popover component from Ant.
* Until the popover component supports test ids, this model will match any open popover.
* This constructor represents the contents in antd/es/popover/index.d.ts.
*
* The popover can be opened by calling the open method. By default, the open method clicks on the child node. Sometimes you might even need to provide both optional arguments, like when a child node is present but impossible to click on due to canvas behavior.
* @param {object} obj
* @param {BasePage} obj.root - root of the page
* @param {ComponentBasics} [obj.childNode] - optional if `openMethod` is present. It's the element we click on to open the dropdown.
* @param {Function} [obj.openMethod] - optional if `childNode` is present. It's the method to open the dropdown.
*/
export class Popover extends BaseComponent {
readonly openMethod: () => Promise<void>;
readonly childNode: ComponentBasics | undefined;
constructor({ root, childNode, openMethod }: PopoverArgs) {
super({
parent: root,
selector: '.ant-popover .ant-popover-content .ant-popover-inner-content:visible',
});
if (childNode !== undefined) {
this.childNode = childNode;
}
this.openMethod =
openMethod ||
(async () => {
if (this.childNode === undefined) {
// We should never be able to throw this error. In the constructor, we
// either provide a childNode or replace this method.
throw new Error('This popover does not have a child node to click on.');
}
await this.childNode.pwLocator.click();
});
}

/**
* Opens the popover.
* @returns {Promise<this>} - the popover for further actions
*/
async open(): Promise<this> {
await this.openMethod();
return this;
}
}
Loading

0 comments on commit 566b6af

Please sign in to comment.