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 mod listing page object model and organizing page editor POMs #8742

Merged
merged 6 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion end-to-end-tests/fixtures/pageContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
launchPersistentContextWithExtension,
} from "./utils";
import { ModsPage } from "../pageObjects/extensionConsole/modsPage";
import { PageEditorPage } from "../pageObjects/pageEditorPage";
import { PageEditorPage } from "../pageObjects/pageEditor/pageEditorPage";

// This environment variable is used to attach the browser sidepanel window that opens automatically to Playwright.
// See https://github.com/microsoft/playwright/issues/26693
Expand Down
7 changes: 4 additions & 3 deletions end-to-end-tests/pageObjects/extensionConsole/modsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ export class ModsPage extends BasePageObject {
// TODO: remove once fixed: https://github.com/pixiebrix/pixiebrix-extension/issues/8458
const registryPromise = this.page
.context()
.waitForEvent("requestfinished", (request) =>
request.url().includes("/api/registry/bricks/"),
);
Comment on lines -42 to -43
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not directly related to this PR, but I had to add this timeout after defining a default timeout for actions

.waitForEvent("requestfinished", {
predicate: (request) => request.url().includes("/api/registry/bricks/"),
timeout: 10_000,
});
await this.page.goto(this.extensionConsoleUrl);
await expect(this.getByText("Extension Console")).toBeVisible();
await registryPromise;
Expand Down
76 changes: 76 additions & 0 deletions end-to-end-tests/pageObjects/pageEditor/modListingPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2024 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { BasePageObject } from "../basePageObject";
import { uuidv4 } from "@/types/helpers";
import { type Locator } from "@playwright/test";

export type StarterBrickUIName =
| "Context Menu"
| "Trigger"
| "Button"
| "Quick Bar Action"
| "Dynamic Quick Bar"
| "Sidebar Panel";

export class ModListItem extends BasePageObject {
saveButton = this.locator("[data-icon=save]");
get menuButton() {
return this.getByLabel(`${this.modComponentName} - Ellipsis`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, I didn't realize we had that aria label on those buttons

}

constructor(
root: Locator,
readonly modComponentName: string,
) {
super(root);
}

async activate() {
return this.root.click();
}
}

export class ModListingPanel extends BasePageObject {
addButton = this.getByRole("button", { name: "Add", exact: true });

/**
* Adds a starter brick in the Page Editor. Generates a unique mod name to prevent
* test collision.
*
* @param starterBrickName the starter brick name to add, corresponding to the name shown in the Page Editor UI,
* not the underlying type
* @returns modName the generated mod name
*/
async addStarterBrick(starterBrickName: StarterBrickUIName) {
const modUuid = uuidv4();
const modComponentName = `Test ${starterBrickName} ${modUuid}`;
await this.addButton.click();
await this.locator("[role=button].dropdown-item", {
hasText: starterBrickName,
}).click();

return { modComponentName, modUuid };
}

getModListItemByName(modName: string) {
return new ModListItem(
this.locator(".list-group-item", { hasText: modName }).first(),
modName,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,13 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { getBasePageEditorUrl } from "./constants";
import { getBasePageEditorUrl } from "../constants";
import { type Page, expect } from "@playwright/test";
import { uuidv4 } from "@/types/helpers";
import { ModsPage } from "./extensionConsole/modsPage";
import { WorkshopPage } from "end-to-end-tests/pageObjects/extensionConsole/workshop/workshopPage";
import { ModsPage } from "../extensionConsole/modsPage";
import { WorkshopPage } from "../extensionConsole/workshop/workshopPage";
import { type UUID } from "@/types/stringTypes";
import { BasePageObject } from "./basePageObject";

// Starter brick names as shown in the Page Editor UI
export type StarterBrickName =
| "Context Menu"
| "Trigger"
| "Button"
| "Quick Bar Action"
| "Dynamic Quick Bar"
| "Sidebar Panel";
import { BasePageObject } from "../basePageObject";
import { ModListingPanel } from "./modListingPanel";

/**
* Page object for the Page Editor. Prefer the newPageEditorPage fixture in testBase.ts to directly creating an
Expand All @@ -44,6 +35,8 @@ export class PageEditorPage extends BasePageObject {
private readonly savedStandaloneModNames: string[] = [];
private readonly savedPackageModIds: string[] = [];

modListingPanel = new ModListingPanel(this.getByTestId("modListingPanel"));

templateGalleryButton = this.getByRole("button", {
name: "Launch Template Gallery",
});
Expand Down Expand Up @@ -78,25 +71,6 @@ export class PageEditorPage extends BasePageObject {
await this.page.waitForTimeout(500);
}

/**
* Adds a starter brick in the Page Editor. Generates a unique mod name to prevent
* test collision.
*
* @param starterBrickName the starter brick name to add, corresponding to the name shown in the Page Editor UI,
* not the underlying type
* @returns modName the generated mod name
*/
async addStarterBrick(starterBrickName: StarterBrickName) {
const modUuid = uuidv4();
const modComponentName = `Test ${starterBrickName} ${modUuid}`;
await this.getByRole("button", { name: "Add", exact: true }).click();
await this.locator("[role=button].dropdown-item", {
hasText: starterBrickName,
}).click();

return { modComponentName, modUuid };
}

async setStarterBrickName(modComponentName: string) {
await this.fillInBrickField("Name", modComponentName);
await this.waitForReduxUpdate();
Expand Down Expand Up @@ -142,12 +116,6 @@ export class PageEditorPage extends BasePageObject {
await this.waitForReduxUpdate();
}

getModListItemByName(modName: string) {
return this.locator(".list-group-item")
.locator("span", { hasText: modName })
.first();
}

/**
* Save a selected packaged mod. Prefer saveStandaloneMod for standalone mods.
*/
Expand All @@ -170,15 +138,11 @@ export class PageEditorPage extends BasePageObject {
}

async saveStandaloneMod(modName: string) {
// We need to wait at least 500ms to permit the page editor to persist the mod changes to redux before saving.
// https://github.com/pixiebrix/pixiebrix-extension/blob/277eab74d2c85c2d16053bbcd27023d2612f9e31/src/pageEditor/panes/EditorPane.tsx#L48
// eslint-disable-next-line playwright/no-wait-for-timeout -- see above
await this.page.waitForTimeout(600);
const modListItem = this.locator(".list-group-item", {
hasText: modName,
});
await modListItem.click();
await modListItem.locator("[data-icon=save]").click();
// Wait for redux to persist the page editor mod changes before saving.
await this.waitForReduxUpdate();
const modListItem = this.modListingPanel.getModListItemByName(modName);
await modListItem.activate();
await modListItem.saveButton.click();
await expect(this.getByText("Saved Mod")).toBeVisible();
this.savedStandaloneModNames.push(modName);
}
Expand All @@ -194,7 +158,9 @@ export class PageEditorPage extends BasePageObject {
}) {
const modName = `${modNameRoot} ${modUuid}`;

await this.getByLabel(`${modComponentName} - Ellipsis`).click();
const modListItem =
this.modListingPanel.getModListItemByName(modComponentName);
await modListItem.menuButton.click();
await this.getByRole("button", { name: "Add to mod" }).click();

await this.getByText("Select...Choose a mod").click();
Expand Down
2 changes: 1 addition & 1 deletion end-to-end-tests/tests/modLifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ test("create, run, package, and update mod", async ({
const pageEditorPage = await newPageEditorPage(page.url());

const { modComponentName, modUuid } =
await pageEditorPage.addStarterBrick("Button");
await pageEditorPage.modListingPanel.addStarterBrick("Button");

await test.step("Configure the Button brick", async () => {
await page.bringToFront();
Expand Down
8 changes: 5 additions & 3 deletions end-to-end-tests/tests/pageEditor/saveMod.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ test("can save a standalone trigger mod", async ({
}) => {
await page.goto("/");
const pageEditorPage = await newPageEditorPage(page.url());
const { modComponentName } = await pageEditorPage.addStarterBrick("Trigger");
const { modComponentName } =
await pageEditorPage.modListingPanel.addStarterBrick("Trigger");
await pageEditorPage.setStarterBrickName(modComponentName);
await pageEditorPage.saveStandaloneMod(modComponentName);
const modsPage = new ModsPage(page, extensionId);
Expand All @@ -52,8 +53,9 @@ test("shows error notification when updating a public mod without incrementing t
await modActivationPage.clickActivateAndWaitForModsPageRedirect();
await page.goto("/");
const pageEditorPage = await newPageEditorPage(page.url());
const modListItem = pageEditorPage.getModListItemByName(modName);
await modListItem.click();
const modListItem =
pageEditorPage.modListingPanel.getModListItemByName(modName);
await modListItem.activate();
await pageEditorPage.fillInBrickField("Name", "8203 Repro Updated");
await pageEditorPage.saveSelectedPackagedMod();
await expect(pageEditorPage.getIncrementVersionErrorToast()).toBeVisible();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ test("#8104: Do not automatically close the sidebar when saving in the Page Edit
const pageEditorPage = await newPageEditorPage(page.url());

const { modComponentName } =
await pageEditorPage.addStarterBrick("Sidebar Panel");
await pageEditorPage.modListingPanel.addStarterBrick("Sidebar Panel");
await pageEditorPage.setStarterBrickName(modComponentName);

const sidebar = await getSidebarPage(page, extensionId);
Expand Down
10 changes: 6 additions & 4 deletions end-to-end-tests/tests/telemetry/errors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ async function waitForBackgroundPageRequest(

expect(offscreenPage?.url()).toBeDefined();
}).toPass({ timeout: 5000 });
return offscreenPage?.waitForRequest(errorServiceEndpoint);
return offscreenPage?.waitForRequest(errorServiceEndpoint, {
// TODO: due to Datadog SDK implementation, it will take ~30 seconds for the
// request to be sent. We should figure out a way to induce the request to be sent sooner.
// See this datadog support request: https://help.datadoghq.com/hc/en-us/requests/1754158
timeout: 35_000,
});
}

const ERROR_SERVICE_ENDPOINT = "https://browser-intake-datadoghq.com/api/v2/*";
Expand All @@ -33,9 +38,6 @@ async function getErrorsFromRequest(
extensionId: string,
context: BrowserContext,
) {
// TODO: due to Datadog SDK implementation, it will take ~30 seconds for the
// request to be sent. We should figure out a way to induce the request to be sent sooner.
// See this datadog support request: https://help.datadoghq.com/hc/en-us/requests/1754158
const request = await waitForBackgroundPageRequest(
context,
extensionId,
Expand Down
1 change: 1 addition & 0 deletions knip.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const knipConfig = {
"src/development/hooks/**",
// Type-only strictNullChecks helper
"src/types/typeOnlyMessengerRegistration.ts",
"end-to-end-tests/**",

// https://knip.dev/reference/jsdoc-tsdoc-tags/#tags-cli
// Instead of adding files to this list, prefer adding a @knip JSDoc comment with explanation, like:
Expand Down
6 changes: 6 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export default defineConfig<{ chromiumChannel: string }>({

/* Collect trace when retrying the failed test in CI, and always on failure when running locally. See https://playwright.dev/docs/trace-viewer */
trace: CI ? "on-first-retry" : "retain-on-failure",

/* Set the default timeout for actions such as `click` */
actionTimeout: 5_000,

/* Set the default timeout for page navigations */
navigationTimeout: 10_000,
Comment on lines +40 to +45
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

waiting the whole test timeout for a failed click action is annoying so I defined a default action and nav timeouts.

},
/* Configure projects for major browsers */
projects: [
Expand Down
Loading