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: [