From aa116197936f8724fe2907188058cc02db762304 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Tue, 2 Jul 2024 20:27:59 -0400 Subject: [PATCH 1/3] brick action panel --- .../pageEditor/brickActionsPanel.ts | 34 +++++++++++++++++++ .../pageObjects/pageEditor/pageEditorPage.ts | 18 +++------- end-to-end-tests/tests/modLifecycle.spec.ts | 11 +++--- 3 files changed, 45 insertions(+), 18 deletions(-) create mode 100644 end-to-end-tests/pageObjects/pageEditor/brickActionsPanel.ts diff --git a/end-to-end-tests/pageObjects/pageEditor/brickActionsPanel.ts b/end-to-end-tests/pageObjects/pageEditor/brickActionsPanel.ts new file mode 100644 index 0000000000..5d0034ca47 --- /dev/null +++ b/end-to-end-tests/pageObjects/pageEditor/brickActionsPanel.ts @@ -0,0 +1,34 @@ +/* + * 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"; + +export class BrickActionsPanel extends BasePageObject { + getAddBrickButton(n: number) { + return this.getByTestId(/icon-button-.*-add-brick/).nth(n); + } + + async addBrick(brickName: string, { index = 0 }: { index?: number } = {}) { + await this.getAddBrickButton(index).click(); + + // Add brick modal + await this.page.getByTestId("tag-search-input").fill(brickName); + await this.page.getByRole("button", { name: brickName }).click(); + + await this.page.getByRole("button", { name: "Add brick" }).click(); + } +} diff --git a/end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts b/end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts index cb55e79a6e..90f078c8e3 100644 --- a/end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts +++ b/end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts @@ -22,6 +22,7 @@ import { WorkshopPage } from "../extensionConsole/workshop/workshopPage"; import { type UUID } from "@/types/stringTypes"; import { BasePageObject } from "../basePageObject"; import { ModListingPanel } from "./modListingPanel"; +import { BrickActionsPanel } from "./brickActionsPanel"; /** * Page object for the Page Editor. Prefer the newPageEditorPage fixture in testBase.ts to directly creating an @@ -36,6 +37,9 @@ export class PageEditorPage extends BasePageObject { private readonly savedPackageModIds: string[] = []; modListingPanel = new ModListingPanel(this.getByTestId("modListingPanel")); + brickActionsPanel = new BrickActionsPanel( + this.getByTestId("brickActionsPanel"), + ); templateGalleryButton = this.getByRole("button", { name: "Launch Template Gallery", @@ -81,20 +85,6 @@ export class PageEditorPage extends BasePageObject { await this.waitForReduxUpdate(); } - async addBrickToModComponent( - brickName: string, - { index = 0 }: { index?: number } = {}, - ) { - await this.getByTestId(/icon-button-.*-add-brick/) - .nth(index) - .click(); - - await this.getByTestId("tag-search-input").fill(brickName); - await this.getByRole("button", { name: brickName }).click(); - - await this.getByRole("button", { name: "Add brick" }).click(); - } - async selectConnectedPageElement(connectedPage: Page) { // Without focusing first, the click doesn't enable selection tool ¯\_(ツ)_/¯ await this.getByLabel("Select element").focus(); diff --git a/end-to-end-tests/tests/modLifecycle.spec.ts b/end-to-end-tests/tests/modLifecycle.spec.ts index 077635ca16..a7eb096320 100644 --- a/end-to-end-tests/tests/modLifecycle.spec.ts +++ b/end-to-end-tests/tests/modLifecycle.spec.ts @@ -44,7 +44,7 @@ test("create, run, package, and update mod", async ({ }); await test.step("Add the Extract from Page brick and configure it", async () => { - await pageEditorPage.addBrickToModComponent("extract from page"); + await pageEditorPage.brickActionsPanel.addBrick("extract from page"); await pageEditorPage.getByPlaceholder("Property name").fill("searchText"); await expect(pageEditorPage.getByPlaceholder("Property name")).toHaveValue( @@ -55,9 +55,12 @@ test("create, run, package, and update mod", async ({ }); await test.step("Add the YouTube search brick and configure it", async () => { - await pageEditorPage.addBrickToModComponent("YouTube search in new tab", { - index: 1, - }); + await pageEditorPage.brickActionsPanel.addBrick( + "YouTube search in new tab", + { + index: 1, + }, + ); await pageEditorPage.getByLabel("Query").click(); await pageEditorPage.fillInBrickField( From b553e814abdec6c9502005ac7e16b8de40abed08 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Wed, 3 Jul 2024 14:21:44 -0400 Subject: [PATCH 2/3] adds more page editor poms --- .../pageEditor/brickActionsPanel.ts | 4 +- .../pageEditor/brickConfigurationPanel.ts | 31 ++++++++++++ .../pageObjects/pageEditor/dataPanel.ts | 20 ++++++++ .../pageObjects/pageEditor/modEditorPane.ts | 25 ++++++++++ .../pageObjects/pageEditor/pageEditorPage.ts | 50 ++++++++----------- .../pageObjects/pageEditor/utils.ts | 35 +++++++++++++ end-to-end-tests/tests/modLifecycle.spec.ts | 25 ++++++---- .../tests/pageEditor/saveMod.spec.ts | 7 ++- .../doNotCloseSidebarOnPageEditorSave.spec.ts | 10 +++- 9 files changed, 164 insertions(+), 43 deletions(-) create mode 100644 end-to-end-tests/pageObjects/pageEditor/brickConfigurationPanel.ts create mode 100644 end-to-end-tests/pageObjects/pageEditor/dataPanel.ts create mode 100644 end-to-end-tests/pageObjects/pageEditor/modEditorPane.ts create mode 100644 end-to-end-tests/pageObjects/pageEditor/utils.ts diff --git a/end-to-end-tests/pageObjects/pageEditor/brickActionsPanel.ts b/end-to-end-tests/pageObjects/pageEditor/brickActionsPanel.ts index 5d0034ca47..b0a248963a 100644 --- a/end-to-end-tests/pageObjects/pageEditor/brickActionsPanel.ts +++ b/end-to-end-tests/pageObjects/pageEditor/brickActionsPanel.ts @@ -16,18 +16,20 @@ */ import { BasePageObject } from "../basePageObject"; +import { ModifiesModState } from "./utils"; export class BrickActionsPanel extends BasePageObject { getAddBrickButton(n: number) { return this.getByTestId(/icon-button-.*-add-brick/).nth(n); } + @ModifiesModState async addBrick(brickName: string, { index = 0 }: { index?: number } = {}) { await this.getAddBrickButton(index).click(); // Add brick modal await this.page.getByTestId("tag-search-input").fill(brickName); - await this.page.getByRole("button", { name: brickName }).click(); + await this.page.getByRole("button", { name: brickName }).first().click(); await this.page.getByRole("button", { name: "Add brick" }).click(); } diff --git a/end-to-end-tests/pageObjects/pageEditor/brickConfigurationPanel.ts b/end-to-end-tests/pageObjects/pageEditor/brickConfigurationPanel.ts new file mode 100644 index 0000000000..2ea126e697 --- /dev/null +++ b/end-to-end-tests/pageObjects/pageEditor/brickConfigurationPanel.ts @@ -0,0 +1,31 @@ +/* + * 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 { ModifiesModState } from "./utils"; + +export class BrickConfigurationPanel extends BasePageObject { + @ModifiesModState + async fillField(fieldLabel: string, value: string) { + await this.getByLabel(fieldLabel).fill(value); + } + + @ModifiesModState + async fillFieldByPlaceholder(fieldPlaceholder: string, value: string) { + await this.getByPlaceholder(fieldPlaceholder).fill(value); + } +} diff --git a/end-to-end-tests/pageObjects/pageEditor/dataPanel.ts b/end-to-end-tests/pageObjects/pageEditor/dataPanel.ts new file mode 100644 index 0000000000..7eace81df9 --- /dev/null +++ b/end-to-end-tests/pageObjects/pageEditor/dataPanel.ts @@ -0,0 +1,20 @@ +/* + * 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"; + +export class DataPanel extends BasePageObject {} diff --git a/end-to-end-tests/pageObjects/pageEditor/modEditorPane.ts b/end-to-end-tests/pageObjects/pageEditor/modEditorPane.ts new file mode 100644 index 0000000000..b8d5bb47e2 --- /dev/null +++ b/end-to-end-tests/pageObjects/pageEditor/modEditorPane.ts @@ -0,0 +1,25 @@ +/* + * 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"; + +export class ModEditorPane extends BasePageObject { + modId = this.getByLabel("Mod ID"); + name = this.getByLabel("Name"); + version = this.getByLabel("Version"); + description = this.getByLabel("Description"); +} diff --git a/end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts b/end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts index 90f078c8e3..5b3e9ff860 100644 --- a/end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts +++ b/end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts @@ -16,20 +16,21 @@ */ import { getBasePageEditorUrl } from "../constants"; -import { type Page, expect } from "@playwright/test"; +import { type Page, expect, Locator } from "@playwright/test"; import { ModsPage } from "../extensionConsole/modsPage"; import { WorkshopPage } from "../extensionConsole/workshop/workshopPage"; import { type UUID } from "@/types/stringTypes"; import { BasePageObject } from "../basePageObject"; import { ModListingPanel } from "./modListingPanel"; import { BrickActionsPanel } from "./brickActionsPanel"; +import { BrickConfigurationPanel } from "./brickConfigurationPanel"; +import { DataPanel } from "./dataPanel"; +import { ModEditorPane } from "./modEditorPane"; +import { ModifiesModState } from "./utils"; /** * Page object for the Page Editor. Prefer the newPageEditorPage fixture in testBase.ts to directly creating an * instance of this class to take advantage of automatic cleanup of saved mods. - * - * @knip usage of PageEditorPage indirectly via the newPageEditorPage fixture in testBase.ts causes a - * false-positive */ export class PageEditorPage extends BasePageObject { private readonly pageEditorUrl: string; @@ -41,6 +42,13 @@ export class PageEditorPage extends BasePageObject { this.getByTestId("brickActionsPanel"), ); + modEditorPane = new ModEditorPane(this.getByTestId("modEditorPane")); + brickConfigurationPanel = new BrickConfigurationPanel( + this.getByTestId("brickConfigurationPanel"), + ); + + dataPanel = new DataPanel(this.getByTestId("dataPanel")); + templateGalleryButton = this.getByRole("button", { name: "Launch Template Gallery", }); @@ -69,23 +77,13 @@ export class PageEditorPage extends BasePageObject { await this.page.bringToFront(); } - async waitForReduxUpdate() { - // See EditorPane.tsx:REDUX_SYNC_WAIT_MILLIS - // eslint-disable-next-line playwright/no-wait-for-timeout -- Wait for Redux to update - await this.page.waitForTimeout(500); - } - - async setStarterBrickName(modComponentName: string) { - await this.fillInBrickField("Name", modComponentName); - await this.waitForReduxUpdate(); - } - - async fillInBrickField(fieldLabel: string, value: string) { - await this.getByLabel(fieldLabel).fill(value); - await this.waitForReduxUpdate(); - } - - async selectConnectedPageElement(connectedPage: Page) { + /** Used for interactions that require selecting an element on the connected page, such as the button starter brick */ + @ModifiesModState + async selectConnectedPageElement( + connectedPage: Page, + selectLocator: Locator, + expectedElementSelector: string, + ) { // Without focusing first, the click doesn't enable selection tool ¯\_(ツ)_/¯ await this.getByLabel("Select element").focus(); await this.getByLabel("Select element").click(); @@ -94,16 +92,12 @@ export class PageEditorPage extends BasePageObject { await expect( connectedPage.getByText("Selection Tool: 0 matching"), ).toBeVisible(); - await connectedPage - .getByRole("heading", { name: "Transaction Table" }) - .click(); + await selectLocator.click(); await this.page.bringToFront(); await expect(this.getByPlaceholder("Select an element")).toHaveValue( - "#root h1", + expectedElementSelector, ); - - await this.waitForReduxUpdate(); } /** @@ -128,8 +122,6 @@ export class PageEditorPage extends BasePageObject { } async saveStandaloneMod(modName: string) { - // 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(); diff --git a/end-to-end-tests/pageObjects/pageEditor/utils.ts b/end-to-end-tests/pageObjects/pageEditor/utils.ts new file mode 100644 index 0000000000..7bbabd1c74 --- /dev/null +++ b/end-to-end-tests/pageObjects/pageEditor/utils.ts @@ -0,0 +1,35 @@ +/* + * 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 { type BasePageObject } from "../basePageObject"; + +type AsyncFunction = (...args: any[]) => Promise; + +// Decorator used for functions that modify the state of the mod. +// This is used to wait for Redux to update before continuing. +export function ModifiesModState( + value: AsyncFunction, + context: ClassMethodDecoratorContext>, +) { + return async function (this: BasePageObject, ...args: any[]): Promise { + const result = await value.apply(this, args); + // See EditorPane.tsx:REDUX_SYNC_WAIT_MILLIS + // eslint-disable-next-line playwright/no-wait-for-timeout -- Wait for Redux to update + await this.page.waitForTimeout(500); + return result; + }; +} diff --git a/end-to-end-tests/tests/modLifecycle.spec.ts b/end-to-end-tests/tests/modLifecycle.spec.ts index a7eb096320..3da8b72d9e 100644 --- a/end-to-end-tests/tests/modLifecycle.spec.ts +++ b/end-to-end-tests/tests/modLifecycle.spec.ts @@ -39,19 +39,29 @@ test("create, run, package, and update mod", async ({ await page.getByRole("button", { name: "Action #3" }).click(); await pageEditorPage.bringToFront(); - await pageEditorPage.getByLabel("Button text").fill("Search Youtube"); - await pageEditorPage.setStarterBrickName(modComponentName); + await pageEditorPage.brickConfigurationPanel.fillField( + "Button text", + "Search Youtube", + ); + await pageEditorPage.brickConfigurationPanel.fillField( + "name", + modComponentName, + ); }); await test.step("Add the Extract from Page brick and configure it", async () => { await pageEditorPage.brickActionsPanel.addBrick("extract from page"); - await pageEditorPage.getByPlaceholder("Property name").fill("searchText"); - await expect(pageEditorPage.getByPlaceholder("Property name")).toHaveValue( + await pageEditorPage.brickConfigurationPanel.fillFieldByPlaceholder( + "Property name", "searchText", ); - await pageEditorPage.selectConnectedPageElement(page); + await pageEditorPage.selectConnectedPageElement( + page, + page.getByRole("heading", { name: "Transaction Table" }), + "#root h1", + ); }); await test.step("Add the YouTube search brick and configure it", async () => { @@ -62,13 +72,10 @@ test("create, run, package, and update mod", async ({ }, ); - await pageEditorPage.getByLabel("Query").click(); - await pageEditorPage.fillInBrickField( + await pageEditorPage.brickConfigurationPanel.fillField( "Query", "{{ @data.searchText }} + Foo", ); - - await pageEditorPage.waitForReduxUpdate(); }); const { modId } = await pageEditorPage.createModFromModComponent({ diff --git a/end-to-end-tests/tests/pageEditor/saveMod.spec.ts b/end-to-end-tests/tests/pageEditor/saveMod.spec.ts index 5ef19faa3e..89e8e0ca01 100644 --- a/end-to-end-tests/tests/pageEditor/saveMod.spec.ts +++ b/end-to-end-tests/tests/pageEditor/saveMod.spec.ts @@ -32,7 +32,10 @@ test("can save a standalone trigger mod", async ({ const pageEditorPage = await newPageEditorPage(page.url()); const { modComponentName } = await pageEditorPage.modListingPanel.addStarterBrick("Trigger"); - await pageEditorPage.setStarterBrickName(modComponentName); + await pageEditorPage.brickConfigurationPanel.fillField( + "name", + modComponentName, + ); await pageEditorPage.saveStandaloneMod(modComponentName); const modsPage = new ModsPage(page, extensionId); await modsPage.goto(); @@ -56,7 +59,7 @@ test("shows error notification when updating a public mod without incrementing t const modListItem = pageEditorPage.modListingPanel.getModListItemByName(modName); await modListItem.activate(); - await pageEditorPage.fillInBrickField("Name", "8203 Repro Updated"); + await pageEditorPage.modEditorPane.name.fill("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 818c2015d7..c5e76f345d 100644 --- a/end-to-end-tests/tests/regressions/doNotCloseSidebarOnPageEditorSave.spec.ts +++ b/end-to-end-tests/tests/regressions/doNotCloseSidebarOnPageEditorSave.spec.ts @@ -30,7 +30,10 @@ test("#8104: Do not automatically close the sidebar when saving in the Page Edit const { modComponentName } = await pageEditorPage.modListingPanel.addStarterBrick("Sidebar Panel"); - await pageEditorPage.setStarterBrickName(modComponentName); + await pageEditorPage.brickConfigurationPanel.fillField( + "name", + modComponentName, + ); const sidebar = await getSidebarPage(page, extensionId); await expect( @@ -38,7 +41,10 @@ test("#8104: Do not automatically close the sidebar when saving in the Page Edit ).toBeVisible(); const updatedTabTitle = "Updated Tab Title"; - await pageEditorPage.fillInBrickField("Tab Title", updatedTabTitle); + await pageEditorPage.brickConfigurationPanel.fillField( + "Tab Title", + updatedTabTitle, + ); await pageEditorPage.getRenderPanelButton().click(); await expect( sidebar.getByRole("tab", { name: updatedTabTitle }), From 0070694bb3dc36b98c724758ac28c0ae2ea2b008 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Wed, 3 Jul 2024 14:31:47 -0400 Subject: [PATCH 3/3] fix type import --- end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts b/end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts index 5b3e9ff860..3429b796d0 100644 --- a/end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts +++ b/end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts @@ -16,7 +16,7 @@ */ import { getBasePageEditorUrl } from "../constants"; -import { type Page, expect, Locator } from "@playwright/test"; +import { type Page, expect, type Locator } from "@playwright/test"; import { ModsPage } from "../extensionConsole/modsPage"; import { WorkshopPage } from "../extensionConsole/workshop/workshopPage"; import { type UUID } from "@/types/stringTypes";