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: More page editor poms #8748

Merged
merged 4 commits into from
Jul 3, 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
36 changes: 36 additions & 0 deletions end-to-end-tests/pageObjects/pageEditor/brickActionsPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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 { 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 }).first().click();

await this.page.getByRole("button", { name: "Add brick" }).click();
}
}
31 changes: 31 additions & 0 deletions end-to-end-tests/pageObjects/pageEditor/brickConfigurationPanel.ts
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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);
}
}
20 changes: 20 additions & 0 deletions end-to-end-tests/pageObjects/pageEditor/dataPanel.ts
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

import { BasePageObject } from "../basePageObject";

export class DataPanel extends BasePageObject {}
25 changes: 25 additions & 0 deletions end-to-end-tests/pageObjects/pageEditor/modEditorPane.ts
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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");
}
66 changes: 24 additions & 42 deletions end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,38 @@
*/

import { getBasePageEditorUrl } from "../constants";
import { type Page, expect } 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";
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;
private readonly savedStandaloneModNames: string[] = [];
private readonly savedPackageModIds: string[] = [];

modListingPanel = new ModListingPanel(this.getByTestId("modListingPanel"));
brickActionsPanel = new BrickActionsPanel(
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",
Expand Down Expand Up @@ -65,37 +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 addBrickToModComponent(
brickName: string,
{ index = 0 }: { index?: number } = {},
/** 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,
) {
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();
await this.getByLabel("Select element").click();
Expand All @@ -104,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();
}

/**
Expand All @@ -138,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();
Expand Down
35 changes: 35 additions & 0 deletions end-to-end-tests/pageObjects/pageEditor/utils.ts
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

import { type BasePageObject } from "../basePageObject";

type AsyncFunction<T> = (...args: any[]) => Promise<T>;

// 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<T>(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The idea for using this as a decorator rather than as a helper function is that it's easier to remember to annotate which methods are modifying the mod state at the top of the function, and also serves to communicate their intent a bit more clearly to devs (plus I was curious and wanted to try writing a decorator myself)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I like it

value: AsyncFunction<T>,
context: ClassMethodDecoratorContext<BasePageObject, AsyncFunction<T>>,
) {
return async function (this: BasePageObject, ...args: any[]): Promise<T> {
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;
};
}
36 changes: 23 additions & 13 deletions end-to-end-tests/tests/modLifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,33 +39,43 @@ 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.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(
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 () => {
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(
await pageEditorPage.brickConfigurationPanel.fillField(
"Query",
"{{ @data.searchText }} + Foo",
);

await pageEditorPage.waitForReduxUpdate();
});

const { modId } = await pageEditorPage.createModFromModComponent({
Expand Down
7 changes: 5 additions & 2 deletions end-to-end-tests/tests/pageEditor/saveMod.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,21 @@ 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(
sidebar.getByRole("tab", { name: "Sidebar Panel" }),
).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 }),
Expand Down
Loading