Skip to content

Commit

Permalink
#8463: playwright create run package and update mod (#8492)
Browse files Browse the repository at this point in the history
* initial

* wip

* wip

* can save a mod

* update via workshop

* cleanup mod

* adds additional assertions

* separate out editWorkshopModPage

* cleanup modName, modComponentName, modUuid

* separate adding and configuring starterbrick concerns

* more cleanup

* fixed typo in method name

* update snapshots
  • Loading branch information
grahamlangford authored May 29, 2024
1 parent 3d3c1cb commit 63db07b
Show file tree
Hide file tree
Showing 15 changed files with 337 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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 Page } from "@playwright/test";

export class EditWorkshopModPage {
constructor(private readonly page: Page) {}

async findText(text: string) {
await this.page
.getByLabel("Editor")
.locator("div")
.filter({ hasText: text })
.nth(2)
.click();

await this.page.getByRole("textbox").nth(0).press("ControlOrMeta+f");
await this.page.getByPlaceholder("Search for").fill(text);
}

async findAndReplaceText(findText: string, replaceText: string) {
await this.findText(findText);
await this.page.getByText("+", { exact: true }).click();
await this.page.getByPlaceholder("Replace with").fill(replaceText);
await this.page.getByText("Replace").click();
}

async updateBrick() {
await this.page.getByRole("button", { name: "Update Brick" }).click();
}

async deleteBrick() {
await this.page.getByRole("button", { name: "Delete Brick" }).click();
await this.page.getByRole("button", { name: "Permanently Delete" }).click();
}
}
6 changes: 5 additions & 1 deletion end-to-end-tests/pageObjects/extensionConsole/modsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export class ModsPage {
return this.page.getByRole("table").locator(".list-group-item");
}

modTableItemById(modId: string) {
return this.modTableItems().filter({ hasText: modId });
}

searchModsInput() {
return this.page.getByTestId("blueprints-search-input");
}
Expand All @@ -76,7 +80,7 @@ export class ModsPage {
* Will fail if the mod is not found, or multiple mods are found for the same mod name.
* @param modName the name of the standalone mod to delete (must be a standalone mod, not a packaged mod)
*/
async deleteModByName(modName: string) {
async deleteStandaloneModByName(modName: string) {
await this.page.bringToFront();
await this.searchModsInput().fill(modName);
await expect(this.page.getByText(`results for "${modName}`)).toBeVisible();
Expand Down
16 changes: 16 additions & 0 deletions end-to-end-tests/pageObjects/extensionConsole/workshopPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import { type Page } from "@playwright/test";
import { getBaseExtensionConsoleUrl } from "../constants";
import { EditWorkshopModPage } from "end-to-end-tests/pageObjects/extensionConsole/editWorkshopModPage";

export class WorkshopPage {
private readonly extensionConsoleUrl: string;
Expand All @@ -36,4 +37,19 @@ export class WorkshopPage {
})
.click();
}

async findAndSelectMod(modId: string) {
await this.page
.getByPlaceholder("Start typing to find results")
.fill(modId);
await this.page.getByRole("cell", { name: modId }).click();

return new EditWorkshopModPage(this.page);
}

async deletePackagedModByModId(modId: string) {
await this.page.bringToFront();
const editWorkshopModPage = await this.findAndSelectMod(modId);
await editWorkshopModPage.deleteBrick();
}
}
110 changes: 105 additions & 5 deletions end-to-end-tests/pageObjects/pageEditorPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ 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/workshopPage";
import { type UUID } from "@/types/stringTypes";

// Starter brick names as shown in the Page Editor UI
export type StarterBrickName =
Expand All @@ -40,6 +42,7 @@ export type StarterBrickName =
export class PageEditorPage {
private readonly pageEditorUrl: string;
private readonly savedStandaloneModNames: string[] = [];
private readonly savedPackageModIds: string[] = [];

constructor(
private readonly page: Page,
Expand All @@ -60,6 +63,16 @@ export class PageEditorPage {
await expect(heading).toBeVisible();
}

async bringToFront() {
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);
}

getTemplateGalleryButton() {
return this.page.getByRole("button", { name: "Launch Template Gallery" });
}
Expand All @@ -73,19 +86,62 @@ export class PageEditorPage {
* @returns modName the generated mod name
*/
async addStarterBrick(starterBrickName: StarterBrickName) {
const modName = `Test ${starterBrickName} ${uuidv4()}`;
const modUuid = uuidv4();
const modComponentName = `Test ${starterBrickName} ${modUuid}`;
await this.page.getByRole("button", { name: "Add", exact: true }).click();
await this.page
.locator("[role=button].dropdown-item", {
hasText: starterBrickName,
})
.click();
await this.fillInBrickField("Name", modName);
return modName;

return { modComponentName, modUuid };
}

async setStarterBrickName(modComponentName: string) {
await this.fillInBrickField("Name", modComponentName);
await this.waitForReduxUpdate();
}

async fillInBrickField(fieldLabel: string, value: string) {
await this.page.getByLabel(fieldLabel).fill(value);
await this.waitForReduxUpdate();
}

async addBrickToModComponent(
brickName: string,
{ index = 0 }: { index?: number } = {},
) {
await this.page
.getByTestId(/icon-button-.*-add-brick/)
.nth(index)
.click();

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();
}

async selectConnectedPageElement(connectedPage: Page) {
// Without focusing first, the click doesn't enable selection tool ¯\_(ツ)_/¯
await this.page.getByLabel("Select element").focus();
await this.page.getByLabel("Select element").click();

await connectedPage.bringToFront();
await expect(
connectedPage.getByText("Selection Tool: 0 matching"),
).toBeVisible();
await connectedPage
.getByRole("heading", { name: "Transaction Table" })
.click();

await this.page.bringToFront();
await expect(this.page.getByPlaceholder("Select an element")).toHaveValue(
"#root h1",
);

await this.waitForReduxUpdate();
}

getModListItemByName(modName: string) {
Expand All @@ -110,6 +166,14 @@ export class PageEditorPage {
return this.page.getByText(text);
}

getByLabel(text: string) {
return this.page.getByLabel(text);
}

getByPlaceholder(text: string) {
return this.page.getByPlaceholder(text);
}

getRenderPanelButton() {
return this.page.getByRole("button", { name: "Render Panel" });
}
Expand All @@ -134,6 +198,35 @@ export class PageEditorPage {
this.savedStandaloneModNames.push(modName);
}

async createModFromModComponent({
modNameRoot,
modComponentName,
modUuid,
}: {
modNameRoot: string;
modComponentName: string;
modUuid: UUID;
}) {
const modName = `${modNameRoot} ${modUuid}`;

await this.page.getByLabel(`${modComponentName} - Ellipsis`).click();
await this.page.getByRole("button", { name: "Add to mod" }).click();

await this.page.getByText("Select...Choose a mod").click();
await this.page.getByRole("option", { name: /Create new mod.../ }).click();
await this.page.getByRole("button", { name: "Move" }).click();

const modId = `${modName.split(" ").join("-").toLowerCase()}-${modUuid}`;
await this.page.getByTestId("registryId-id-id").fill(modId);

await this.page.getByLabel("Name", { exact: true }).fill(modName);
await this.page.getByRole("button", { name: "Create" }).click();

this.savedPackageModIds.push(modId);

return { modName, modId };
}

/**
* This method is meant to be called exactly once after the test is done to clean up any saved mods created during the
* test.
Expand All @@ -143,9 +236,16 @@ export class PageEditorPage {
async cleanup() {
const modsPage = new ModsPage(this.page, this.extensionId);
await modsPage.goto();
for (const modName of this.savedStandaloneModNames) {
for (const standaloneModName of this.savedStandaloneModNames) {
// eslint-disable-next-line no-await-in-loop -- optimization via parallelization not relevant here
await modsPage.deleteStandaloneModByName(standaloneModName);
}

const workshopPage = new WorkshopPage(this.page, this.extensionId);
await workshopPage.goto();
for (const packagedModId of this.savedPackageModIds) {
// eslint-disable-next-line no-await-in-loop -- optimization via parallelization not relevant here
await modsPage.deleteModByName(modName);
await workshopPage.deletePackagedModByModId(packagedModId);
}
}
}
126 changes: 126 additions & 0 deletions end-to-end-tests/tests/modLifecycle.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* 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 { expect, test } from "../fixtures/extensionBase";
// @ts-expect-error -- https://youtrack.jetbrains.com/issue/AQUA-711/Provide-a-run-configuration-for-Playwright-tests-in-specs-with-fixture-imports-only
import { type Page, test as base } from "@playwright/test";
import { ModsPage } from "../pageObjects/extensionConsole/modsPage";
import { clickAndWaitForNewPage } from "end-to-end-tests/utils";
import { WorkshopPage } from "end-to-end-tests/pageObjects/extensionConsole/workshopPage";

test("create, run, package, and update mod", async ({
page,
extensionId,
newPageEditorPage,
context,
}) => {
await page.goto("/create-react-app/table");
const pageEditorPage = await newPageEditorPage(page.url());

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

await test.step("Configure the Button brick", async () => {
await page.bringToFront();
await page.getByRole("button", { name: "Action #3" }).click();

await pageEditorPage.bringToFront();
await pageEditorPage.getByLabel("Button text").fill("Search Youtube");
await pageEditorPage.setStarterBrickName(modComponentName);
});

await test.step("Add the Extract from Page brick and configure it", async () => {
await pageEditorPage.addBrickToModComponent("extract from page");

await pageEditorPage.getByPlaceholder("Property name").fill("searchText");
await expect(pageEditorPage.getByPlaceholder("Property name")).toHaveValue(
"searchText",
);

await pageEditorPage.selectConnectedPageElement(page);
});

await test.step("Add the YouTube search brick and configure it", async () => {
await pageEditorPage.addBrickToModComponent("YouTube search in new tab", {
index: 1,
});

await pageEditorPage.getByLabel("Query").click();
await pageEditorPage.fillInBrickField(
"Query",
"{{ @data.searchText }} + Foo",
);

await pageEditorPage.waitForReduxUpdate();
});

const { modId } = await pageEditorPage.createModFromModComponent({
modNameRoot: "Lifecycle Test",
modComponentName,
modUuid,
});

let newPage: Page | undefined;
await test.step("Run the mod", async () => {
newPage = await clickAndWaitForNewPage(
page.getByRole("button", { name: "Search Youtube" }),
context,
);
await expect(newPage).toHaveURL(
"https://www.youtube.com/results?search_query=Transaction+Table+%2B+Foo",
);
});

await test.step("View and update mod in the Workshop", async () => {
const workshopPage = new WorkshopPage(newPage, extensionId);
await workshopPage.goto();
const editWorkshopModPage = await workshopPage.findAndSelectMod(modId);
await editWorkshopModPage.findAndReplaceText(
"version: 1.0.0",
"version: 1.0.1",
);
await editWorkshopModPage.findAndReplaceText(
"description: Created with the PixieBrix Page Editor",
"description: Created through Playwright Automation",
);
await editWorkshopModPage.updateBrick();
});

await test.step("View the updated mod on the mods page", async () => {
const modsPage = new ModsPage(newPage, extensionId);
await modsPage.goto();

await modsPage.viewActiveMods();
const modListing = modsPage.modTableItemById(modId);

await expect(
modListing.getByRole("button", { name: "Update" }),
).toBeVisible();
await modListing.getByRole("button", { name: "Update" }).click();

await expect(newPage.locator("form")).toContainText(
"Created through Playwright Automation",
);

await expect(
newPage.getByRole("button", { name: "Reactivate" }),
).toBeVisible();
await newPage.getByRole("button", { name: "Reactivate" }).click();

await expect(modListing).toContainText("version 1.0.1");
});
});
Loading

0 comments on commit 63db07b

Please sign in to comment.