Skip to content

Commit

Permalink
Fix/hananma/extends dropdown capabilities (#2751)
Browse files Browse the repository at this point in the history
  • Loading branch information
hanan-monday authored Feb 5, 2025
1 parent 745917d commit 0d49950
Show file tree
Hide file tree
Showing 21 changed files with 253 additions and 229 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ModalTopActionsButtonColor, ModalTopActionsTheme, ModalTopActionsProps
import IconButton from "../../IconButton/IconButton";
import { CloseMedium } from "@vibe/icons";
import { ButtonColor } from "../../Button/ButtonConstants";
import { ComponentDefaultTestId } from "../../../tests/constants";

const colorToButtonColor: Record<ModalTopActionsTheme, ModalTopActionsButtonColor> = {
dark: ButtonColor.ON_INVERTED_BACKGROUND,
Expand All @@ -17,6 +18,7 @@ const ModalTopActions = ({ renderAction, theme, closeButtonAriaLabel, onClose }:
<div className={styles.actions} data-no-autofocus={true}>
{typeof renderAction === "function" ? renderAction(buttonColor) : renderAction}
<IconButton
data-testid={ComponentDefaultTestId.MODAL_NEXT_CLOSE_BUTTON}
icon={CloseMedium}
onClick={onClose}
size="small"
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/tests/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export enum ComponentDefaultTestId {
MODAL_NEXT = "modal",
MODAL_NEXT_OVERLAY = "modal-overlay",
MODAL_NEXT_HEADER = "modal-header",
MODAL_NEXT_CLOSE_BUTTON = "modal-close-button",
MODAL_NEXT_CONTENT = "modal-content",
MODAL_NEXT_FOOTER = "modal-footer",
MODAL_NEXT_MEDIA = "modal-media",
Expand Down
16 changes: 3 additions & 13 deletions packages/testkit/__tests__/buttonGroup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,13 @@ test.describe("ButtonGroup Class with Storybook", () => {

test("should initialize buttons if needed", async () => {
// Initialize the buttons inside the ButtonGroup
await buttonGroup.initializeButtonsIfNeeded();

// Verify that buttons are initialized
expect(buttonGroup.buttonsInitialized).toBe(true);

const buttons = await buttonGroup.getAllButtons();
// Verify that buttons exist in the items array
expect(buttonGroup.items.length).toBeGreaterThan(0); // Ensure that buttons were initialized
expect(buttons.length).toBeGreaterThan(0);
});

test("should retrieve a button by name and click it", async ({ page }) => {
// Initialize the buttons inside the ButtonGroup
await buttonGroup.initializeButtonsIfNeeded();

const button = await buttonGroup.getButtonByName("Beta"); // Adjust the button name as needed

// Add a listener for console logs to capture the click event
Expand All @@ -58,16 +52,12 @@ test.describe("ButtonGroup Class with Storybook", () => {
});

// Click the button
await buttonGroup.click("Beta");
await buttonGroup.clickButton("Beta");

// eslint-disable-next-line playwright/no-wait-for-timeout -- Wait a bit to ensure the console log is captured
await page.waitForTimeout(500);

// Verify the console message contains the expected log
expect(consoleMessage).toContain("Button clicked: Beta");
});

test("should throw an error if trying to click a non-existent button", async () => {
await expect(buttonGroup.click("NonExistentButton")).rejects.toThrow("Invalid button name provided");
});
});
104 changes: 77 additions & 27 deletions packages/testkit/components/BaseElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,33 +27,6 @@ export class BaseElement {
return this.locator;
}

/**
* Wait for the list elements to stabilize (i.e., the count of items remains constant for a specified duration).
* @returns {Promise<void>}
*/
async waitForElementsGroup(locator: Locator, elementReportName: string): Promise<void> {
await test.step(`Wait for ${elementReportName} items to stabilize`, async () => {
let previousCount = 0;
let stableCountTime = 0;
const stabilizationTimeMs = 500;
// eslint-disable-next-line no-constant-condition
while (true) {
const currentCount = await this.locator.locator(locator).count();

if (currentCount === previousCount) {
stableCountTime += 100; // Increase stable time by the check interval (100ms)
} else {
stableCountTime = 0; // Reset if the count changes
}
if (stableCountTime >= stabilizationTimeMs) {
break; // Break the loop if count has been stable for the desired duration
}
previousCount = currentCount;
await this.page.waitForTimeout(100); // Polling interval (100ms)
}
});
}

/**
* Check if the element is enabled.
* @returns {Promise<boolean>} - Returns true if the element is enabled, otherwise false.
Expand All @@ -67,6 +40,26 @@ export class BaseElement {
return isEnabled;
}

/**
* Hover the element.
* @returns {Promise<void>}
*/
async hover(): Promise<void> {
await test.step(`Hover ${this.elementReportName}`, async () => {
await this.locator.hover();
});
}

/**
* Click the element.
* @returns {Promise<void>}
*/
async click(): Promise<void> {
await test.step(`Click ${this.elementReportName}`, async () => {
await this.locator.click();
});
}

/**
* Scroll the element into view if needed.
* @returns {Promise<void>}
Expand Down Expand Up @@ -119,6 +112,12 @@ export class BaseElement {
});
}

async waitForVisible(): Promise<void> {
await test.step(`Wait for ${this.elementReportName}`, async () => {
await this.locator.waitFor({ state: "visible" });
});
}

async waitForAbsence(): Promise<void> {
await test.step(`Wait for ${this.elementReportName} to be absent`, async () => {
await this.waitFor({ state: "detached" });
Expand All @@ -140,4 +139,55 @@ export class BaseElement {
});
return isVisible;
}

/**
* Wait for the list elements to stabilize (i.e., the count of items remains constant for a specified duration).
* @param {Locator} locator - The locator for the elements.
* @returns {Promise<void>}
*/
protected async waitForAndVerifyElements(locator: Locator): Promise<void> {
await test.step(`Wait for ${this.elementReportName} items to stabilize and verify existence`, async () => {
let previousCount = 0;
let stableCountTime = 0;
const stabilizationTimeMs = 500;

// eslint-disable-next-line no-constant-condition
while (true) {
const currentCount = await locator.count();

// Verify we have at least one element
if (currentCount === 0) {
await this.page.waitForTimeout(100);
continue;
}

// Check if all elements are visible
const elements = await locator.all();
const visibleStates = await Promise.all(elements.map(el => el.isVisible()));
const allVisible = visibleStates.every(state => state === true);

if (!allVisible) {
await this.page.waitForTimeout(100);
continue;
}

if (currentCount === previousCount) {
stableCountTime += 100;
} else {
stableCountTime = 0;
}

if (stableCountTime >= stabilizationTimeMs) {
break;
}

previousCount = currentCount;
await this.page.waitForTimeout(100);
}

if ((await locator.count()) === 0) {
throw new Error(`No ${this.elementReportName} elements found after stabilization`);
}
});
}
}
6 changes: 0 additions & 6 deletions packages/testkit/components/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import { BaseElement } from "./BaseElement";
* Extends the BaseElement class.
*/
export class Button extends BaseElement {
override page: Page;
override locator: Locator;
override elementReportName: string;
/**
* Create a Button.
* @param {Page} page - The Playwright page object.
Expand All @@ -17,9 +14,6 @@ export class Button extends BaseElement {
*/
constructor(page: Page, locator: Locator, elementReportName: string) {
super(page, locator, elementReportName);
this.page = page;
this.locator = locator;
this.elementReportName = elementReportName;
}

/**
Expand Down
84 changes: 32 additions & 52 deletions packages/testkit/components/ButtonGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,79 +7,59 @@ import { Button } from "./Button";
* Extends the BaseElement class.
*/
export class ButtonGroup extends BaseElement {
override page: Page;
override locator: Locator;
override elementReportName: string;
items: Button[];
buttonsInitialized: boolean;

constructor(page: Page, locator: Locator, elementReportName: string) {
super(page, locator, elementReportName);
this.page = page;
this.locator = locator;
this.elementReportName = elementReportName;
this.items = [];
this.buttonsInitialized = false;
}

/**
* Initialize buttons if they are not already initialized.
* @returns {Promise<void>}
*/
async initializeButtonsIfNeeded() {
await test.step(`Initialize ${this.elementReportName} if needed`, async () => {
if (!this.buttonsInitialized) {
await this.initializeButtons();
this.buttonsInitialized = true;
}
});
}

/**
* Initialize the buttons by locating all button elements.
* @returns {Promise<void>}
* Get all buttons in the button group.
* @returns {Promise<Button[]>} An array of Button objects.
*/
async initializeButtons() {
await test.step(`Initialize ${this.elementReportName}`, async () => {
await this.waitForElementsGroup(this.locator.locator("button"), this.elementReportName);
const buttonElements = await this.locator.locator("button").all();
this.items = await Promise.all(
buttonElements.map(async locator => {
const buttonName = await locator.innerText();
return new Button(this.page, locator.getByText(`${buttonName}`), `Button: ${buttonName}`);
})
async getAllButtons(): Promise<Button[]> {
let buttons: Button[] = [];
await test.step(`Get all buttons in ${this.elementReportName}`, async () => {
const buttonsLocators = await this.locator.locator("button").all();
const buttonPromises = buttonsLocators.map(
async buttonLocator => new Button(this.page, buttonLocator, await buttonLocator.innerText())
);
buttons = await Promise.all(buttonPromises);
});
return buttons;
}

/**
* Get a button by its name.
* @param {string} buttonName - The name of the button to retrieve.
* @returns {Button} The button with the specified name.
*/
getButtonByName(buttonName: string): Button | undefined {
if (!buttonName) {
throw new Error("Invalid button name provided");
}

return this.items.find(item => item.elementReportName.includes(buttonName));
async getButtonByName(buttonName: string): Promise<Button | undefined> {
let button: Button | undefined;
await test.step(`Get button by name ${buttonName} in ${this.elementReportName}`, async () => {
button = new Button(
this.page,
this.locator.locator("button").filter({ hasText: buttonName }),
`Button: ${buttonName}`
);
});
return button;
}

/**
* Click a button by its name.
* @param {string} buttonName - The name of the button to click.
* @returns {Promise<void>}
*/
async click(buttonName: string): Promise<void> {
await this.initializeButtonsIfNeeded();

const button = this.getButtonByName(buttonName);

// Throw an error if the button is not found
if (!button) {
throw new Error(`Invalid button name provided: ${buttonName}`);
}

await button.click();
async clickButton(buttonName: string): Promise<void> {
await test.step(`Click button ${buttonName} in ${this.elementReportName}`, async () => {
const button = new Button(
this.page,
this.locator.locator("button").filter({ hasText: buttonName }),
`Button: ${buttonName}`
);
if (!button) {
throw new Error(`Invalid button name provided: ${buttonName}`);
}
await button.click();
});
}
}
13 changes: 9 additions & 4 deletions packages/testkit/components/Dialog.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Page, Locator } from "@playwright/test";
import { Button } from "./Button";
import { BaseElement } from "./BaseElement";

/**
* Class representing a Dialog element.
Expand Down Expand Up @@ -27,8 +27,13 @@ export class Dialog {
* @param {string} item - The name of the item to select.
* @returns {Promise<void>}
*/
async selectItem(item: string): Promise<void> {
const menuItem = new Button(this.page, this.locator.getByText(`${item}`), `Select Menu Item: ${item}`);
await menuItem.click();
async selectItem(itmeName: string): Promise<void> {
const dialogItem = new BaseElement(
this.page,
this.locator.getByText(`${itmeName}`).first(),
`Select Menu Item: ${itmeName}`
);
await dialogItem.hover();
await dialogItem.click();
}
}
Loading

0 comments on commit 0d49950

Please sign in to comment.