diff --git a/end-to-end-tests/fixtures/pageContext.ts b/end-to-end-tests/fixtures/pageContext.ts
index d68dfd1a13..ac0b5e1794 100644
--- a/end-to-end-tests/fixtures/pageContext.ts
+++ b/end-to-end-tests/fixtures/pageContext.ts
@@ -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
diff --git a/end-to-end-tests/pageObjects/extensionConsole/modsPage.ts b/end-to-end-tests/pageObjects/extensionConsole/modsPage.ts
index 20c4ea48a0..2e51fd7e73 100644
--- a/end-to-end-tests/pageObjects/extensionConsole/modsPage.ts
+++ b/end-to-end-tests/pageObjects/extensionConsole/modsPage.ts
@@ -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/"),
- );
+ .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;
diff --git a/end-to-end-tests/pageObjects/pageEditor/modListingPanel.ts b/end-to-end-tests/pageObjects/pageEditor/modListingPanel.ts
new file mode 100644
index 0000000000..c55b7adff2
--- /dev/null
+++ b/end-to-end-tests/pageObjects/pageEditor/modListingPanel.ts
@@ -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 .
+ */
+
+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`);
+ }
+
+ 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,
+ );
+ }
+}
diff --git a/end-to-end-tests/pageObjects/pageEditorPage.ts b/end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts
similarity index 75%
rename from end-to-end-tests/pageObjects/pageEditorPage.ts
rename to end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts
index 4639893646..cb55e79a6e 100644
--- a/end-to-end-tests/pageObjects/pageEditorPage.ts
+++ b/end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts
@@ -15,22 +15,13 @@
* along with this program. If not, see .
*/
-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
@@ -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",
});
@@ -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();
@@ -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.
*/
@@ -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);
}
@@ -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();
diff --git a/end-to-end-tests/tests/modLifecycle.spec.ts b/end-to-end-tests/tests/modLifecycle.spec.ts
index db305e8e47..077635ca16 100644
--- a/end-to-end-tests/tests/modLifecycle.spec.ts
+++ b/end-to-end-tests/tests/modLifecycle.spec.ts
@@ -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();
diff --git a/end-to-end-tests/tests/pageEditor/saveMod.spec.ts b/end-to-end-tests/tests/pageEditor/saveMod.spec.ts
index f7f100552b..5ef19faa3e 100644
--- a/end-to-end-tests/tests/pageEditor/saveMod.spec.ts
+++ b/end-to-end-tests/tests/pageEditor/saveMod.spec.ts
@@ -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);
@@ -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();
diff --git a/end-to-end-tests/tests/regressions/doNotCloseSidebarOnPageEditorSave.spec.ts b/end-to-end-tests/tests/regressions/doNotCloseSidebarOnPageEditorSave.spec.ts
index eb6a27ac96..818c2015d7 100644
--- a/end-to-end-tests/tests/regressions/doNotCloseSidebarOnPageEditorSave.spec.ts
+++ b/end-to-end-tests/tests/regressions/doNotCloseSidebarOnPageEditorSave.spec.ts
@@ -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);
diff --git a/end-to-end-tests/tests/telemetry/errors.spec.ts b/end-to-end-tests/tests/telemetry/errors.spec.ts
index 234fa69770..b5690515d5 100644
--- a/end-to-end-tests/tests/telemetry/errors.spec.ts
+++ b/end-to-end-tests/tests/telemetry/errors.spec.ts
@@ -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/*";
@@ -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,
diff --git a/knip.mjs b/knip.mjs
index 998aefe540..d1a1960f1f 100644
--- a/knip.mjs
+++ b/knip.mjs
@@ -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:
diff --git a/playwright.config.ts b/playwright.config.ts
index dd68f91587..5e389a39a5 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -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,
},
/* Configure projects for major browsers */
projects: [