Skip to content
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

#8581 refactoring POMs to use the BasePageObject #8727

Merged
merged 5 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
7 changes: 4 additions & 3 deletions end-to-end-tests/pageObjects/basePageObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import { type Locator, type Page } from "playwright";
*
* @example
* class LoginPage extends BasePageObject {
* const usernameInput = this.getByPlaceholder('Username');
* const passwordInput = this.getByPlaceholder('Password');
* // define common locators at the top.
* usernameInput = this.getByPlaceholder('Username');
* passwordInput = this.getByPlaceholder('Password');
*
* async login(username, password) {
* await usernameInput.fill(username);
Expand Down Expand Up @@ -76,7 +77,7 @@ export class BasePageObject {
this.root = rootLocatorOrPage;
this.page = rootLocatorOrPage.page();
} else {
this.root = rootLocatorOrPage.locator("body");
this.root = rootLocatorOrPage.locator("html");
fungairino marked this conversation as resolved.
Show resolved Hide resolved
this.page = rootLocatorOrPage;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@

import { expect, type Page } from "@playwright/test";
import { getBaseExtensionConsoleUrl } from "../constants";
import { BasePageObject } from "../basePageObject";

export class LocalIntegrationsPage {
export class LocalIntegrationsPage extends BasePageObject {
private readonly extensionConsoleUrl?: string;

constructor(
private readonly page: Page,
extensionId?: string,
) {
constructor(page: Page, extensionId?: string) {
super(page);
this.extensionConsoleUrl =
extensionId && getBaseExtensionConsoleUrl(extensionId);
}
Expand All @@ -34,30 +33,26 @@ export class LocalIntegrationsPage {
await this.page.goto(this.extensionConsoleUrl);
}

await this.page
.getByRole("link", {
name: "Local Integrations",
})
.click();
await this.getByRole("link", {
name: "Local Integrations",
}).click();

await expect(
this.page.getByRole("heading", { name: "Local Integrations" }),
this.getByRole("heading", { name: "Local Integrations" }),
).toBeVisible();

await expect(this.page.getByTestId("loader")).toBeHidden();
await expect(this.getByTestId("loader")).toBeHidden();
}

async createNewIntegration(integrationName: string) {
await this.page
.getByRole("button", { name: "Add Local Integration" })
.click();
await this.getByRole("button", { name: "Add Local Integration" }).click();

await this.page
.getByPlaceholder("Start typing to find results")
.fill(integrationName);
await this.getByPlaceholder("Start typing to find results").fill(
integrationName,
);

await this.page.getByText(integrationName).first().click();
await this.getByText(integrationName).first().click();

await this.page.getByTestId(`${integrationName} detail button`).click();
await this.getByTestId(`${integrationName} detail button`).click();
}
}
97 changes: 42 additions & 55 deletions end-to-end-tests/pageObjects/extensionConsole/modsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@
import { expect, type Page } from "@playwright/test";
import { getBaseExtensionConsoleUrl } from "../constants";
import { ensureVisibility } from "../../utils";
import { BasePageObject } from "../basePageObject";

export class ModsPage {
export class ModsPage extends BasePageObject {
private readonly extensionConsoleUrl: string;

constructor(
private readonly page: Page,
extensionId: string,
) {
modTableItems = this.getByRole("table").locator(".list-group-item");
searchModsInput = this.getByTestId("blueprints-search-input");

constructor(page: Page, extensionId: string) {
super(page);
this.extensionConsoleUrl = getBaseExtensionConsoleUrl(extensionId);
}

Expand All @@ -40,39 +42,30 @@ export class ModsPage {
request.url().includes("/api/registry/bricks/"),
);
await this.page.goto(this.extensionConsoleUrl);
await expect(this.page.getByText("Extension Console")).toBeVisible();
await expect(this.getByText("Extension Console")).toBeVisible();
await registryPromise;

// Check that the page is stable, and that the content has finished loading
const activeModsHeading = this.page.getByRole("heading", {
const activeModsHeading = this.getByRole("heading", {
name: "Active Mods",
});
await ensureVisibility(activeModsHeading, { timeout: 10_000 });
const modTableItems = this.modTableItems();
const contentLoadedLocator = this.page
.getByText("Welcome to PixieBrix!")
.or(modTableItems.nth(0));
const contentLoadedLocator = this.getByText("Welcome to PixieBrix!").or(
this.modTableItems.nth(0),
);
await expect(contentLoadedLocator).toBeVisible();
}

async viewAllMods() {
await this.page.getByTestId("all-mods-mod-tab").click();
await this.getByTestId("all-mods-mod-tab").click();
}

async viewActiveMods() {
await this.page.getByTestId("active-mod-tab").click();
}

modTableItems() {
return this.page.getByRole("table").locator(".list-group-item");
await this.getByTestId("active-mod-tab").click();
}

modTableItemById(modId: string) {
return this.modTableItems().filter({ hasText: modId });
}

searchModsInput() {
return this.page.getByTestId("blueprints-search-input");
return this.modTableItems.filter({ hasText: modId });
}

/**
Expand All @@ -83,10 +76,10 @@ export class ModsPage {
*/
async actionForModByName(modName: string, actionName: string): Promise<void> {
await this.page.bringToFront();
await this.searchModsInput().fill(modName);
await expect(this.page.getByText(`results for "${modName}`)).toBeVisible();
await this.searchModsInput.fill(modName);
await expect(this.getByText(`results for "${modName}`)).toBeVisible();

const modSearchResult = this.page.locator(".list-group-item", {
const modSearchResult = this.locator(".list-group-item", {
hasText: modName,
});
await expect(modSearchResult).toBeVisible();
Expand All @@ -95,7 +88,7 @@ export class ModsPage {
await modSearchResult.locator(".dropdown").click();

// Click the delete button in the delete confirmation modal
await this.page.getByRole("button", { name: actionName }).click();
await this.getByRole("button", { name: actionName }).click();
}

/**
Expand All @@ -106,9 +99,9 @@ export class ModsPage {
*/
async deleteModByName(modName: string) {
await this.page.bringToFront();
await this.searchModsInput().fill(modName);
await expect(this.page.getByText(`results for "${modName}`)).toBeVisible();
const modToDelete = this.page.locator(".list-group-item", {
await this.searchModsInput.fill(modName);
await expect(this.getByText(`results for "${modName}`)).toBeVisible();
const modToDelete = this.locator(".list-group-item", {
hasText: modName,
});
await expect(modToDelete).toBeVisible();
Expand All @@ -124,9 +117,7 @@ export class ModsPage {
await deactivateOption.click({
timeout: 3000,
});
await expect(
this.page.getByText(`Deactivated mod: ${modName}`),
).toBeVisible();
await expect(this.getByText(`Deactivated mod: ${modName}`)).toBeVisible();
// Re-open the dropdown action menu to stay in the same state
await modToDelete.locator(".dropdown").click();
}
Expand All @@ -137,23 +128,33 @@ export class ModsPage {
});

// Click the delete button in the delete confirmation modal
await this.page.getByRole("button", { name: "Delete" }).click();
await this.getByRole("button", { name: "Delete" }).click();
await expect(
// Exact text varies by standalone mod vs. mod package
this.page.getByText(`Deleted mod ${modName}`),
this.getByText(`Deleted mod ${modName}`),
).toBeVisible();
}
}

export class ActivateModPage {
export class ActivateModPage extends BasePageObject {
private readonly baseConsoleUrl: string;
private readonly activateModUrl: string;

activateButton = this.getByRole("button", { name: "Activate" });
keyboardShortcutDocumentationLink = this.getByRole("link", {
name: "configuring keyboard shortcuts",
});

configureQuickbarShortcutLink = this.getByRole("link", {
name: "configured your Quick Bar",
});

constructor(
private readonly page: Page,
page: Page,
private readonly extensionId: string,
private readonly modId: string,
) {
super(page);
this.baseConsoleUrl = getBaseExtensionConsoleUrl(extensionId);
this.activateModUrl = `${
this.baseConsoleUrl
Expand All @@ -163,35 +164,21 @@ export class ActivateModPage {
async goto() {
await this.page.goto(this.activateModUrl);

await expect(this.page.getByText("Activate Mod")).toBeVisible();
await expect(this.getByText("Activate Mod")).toBeVisible();
// Loading the mod details may take more than 5 seconds
await expect(this.page.getByText(this.modId)).toBeVisible({
await expect(this.getByText(this.modId)).toBeVisible({
timeout: 10_000,
});
}

activateButton() {
return this.page.getByRole("button", { name: "Activate" });
}

configureQuickbarShortcutLink() {
return this.page.getByRole("link", { name: "configured your Quick Bar" });
}

keyboardShortcutDocumentationLink() {
return this.page.getByRole("link", {
name: "configuring keyboard shortcuts",
});
}

/** Successfully activating the mod will navigate to the "All Mods" page. */
async clickActivateAndWaitForModsPageRedirect() {
await this.activateButton().click();
await this.activateButton.click();
await this.page.waitForURL(`${this.baseConsoleUrl}#/mods`);
const modsPage = new ModsPage(this.page, this.extensionId);
await modsPage.viewActiveMods();
// Loading mods sometimes takes upwards of 5s
await expect(modsPage.modTableItems().getByText(this.modId)).toBeVisible({
// Loading mods sometimes takes upwards of 10s
await expect(modsPage.modTableItems.getByText(this.modId)).toBeVisible({
timeout: 10_000,
});
return modsPage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { type Locator, type Page } from "@playwright/test";
import { WorkshopModEditor } from "./modEditor";
import { BasePageObject } from "../../basePageObject";

export class CreateWorkshopModPage {
readonly editor: WorkshopModEditor;
readonly createBrickButton: Locator;

constructor(private readonly page: Page) {
this.editor = new WorkshopModEditor(this.page);
this.createBrickButton = this.page.getByRole("button", {
name: "Create Brick",
});
}
export class CreateWorkshopModPage extends BasePageObject {
editor = new WorkshopModEditor(this.getByLabel("Editor"));
createBrickButton = this.getByRole("button", {
name: "Create Brick",
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { type Page } from "@playwright/test";
import { WorkshopModEditor } from "./modEditor";
import { BasePageObject } from "end-to-end-tests/pageObjects/basePageObject";

export class EditWorkshopModPage {
readonly editor: WorkshopModEditor;
constructor(private readonly page: Page) {
this.editor = new WorkshopModEditor(this.page);
}
export class EditWorkshopModPage extends BasePageObject {
editor = new WorkshopModEditor(this.getByLabel("Editor"));

async updateBrick() {
await this.page.getByRole("button", { name: "Update Brick" }).click();
await this.getByRole("button", { name: "Update Brick" }).click();
}

async deleteBrick() {
await this.page.getByRole("button", { name: "Delete Brick" }).click();
await this.page.getByRole("button", { name: "Permanently Delete" }).click();
await this.getByRole("button", { name: "Delete Brick" }).click();
await this.getByRole("button", { name: "Permanently Delete" }).click();
// eslint-disable-next-line playwright/no-networkidle -- for some reason, can't assert on the "Brick deleted" notice
await this.page.waitForLoadState("networkidle");
}
Expand Down
26 changes: 10 additions & 16 deletions end-to-end-tests/pageObjects/extensionConsole/workshop/modEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { type Locator, type Page, expect } from "@playwright/test";
import { expect } from "@playwright/test";
import fs from "node:fs/promises";
import path from "node:path";
import { uuidv4 } from "@/types/helpers";
import { BasePageObject } from "../../basePageObject";

export class WorkshopModEditor {
readonly textArea: Locator;
readonly content: Locator;
readonly baseLocator: Locator;

constructor(private readonly page: Page) {
this.baseLocator = this.page.getByLabel("Editor");
this.content = this.baseLocator.locator(".ace_content");
this.textArea = this.baseLocator.getByRole("textbox");
}
export class WorkshopModEditor extends BasePageObject {
content = this.locator(".ace_content");
textArea = this.getByRole("textbox");

async waitForLoad() {
await expect(this.textArea).toBeVisible();
await expect(this.baseLocator.getByText("Loading editor...")).toBeHidden();
await expect(this.getByText("Loading editor...")).toBeHidden();
}

async getValue() {
Expand All @@ -50,14 +44,14 @@ export class WorkshopModEditor {
async findText(text: string) {
await this.content.click(); // Focus on the visible editor
await this.page.keyboard.press("ControlOrMeta+f");
await this.page.getByPlaceholder("Search for").fill(text);
await this.getByPlaceholder("Search for").fill(text);
}

async findAndReplaceText(findText: string, replaceText: string) {
await this.findText(findText);
await this.page.getByText("+", { exact: true }).click();
await this.page.getByPlaceholder("Replace with").fill(replaceText);
await this.page.getByText("Replace").click();
await this.getByText("+", { exact: true }).click();
await this.getByPlaceholder("Replace with").fill(replaceText);
await this.getByText("Replace").click();
}

// Loads the corresponding yaml from the fixtures/modDefinitions directory
Expand Down
Loading
Loading