Skip to content

Commit

Permalink
#9209: eliminate standalone mod components from Page Editor via "unsa…
Browse files Browse the repository at this point in the history
…ved" mods (#9219)
  • Loading branch information
BLoe authored Oct 6, 2024
1 parent 6d1fc07 commit c3648a0
Show file tree
Hide file tree
Showing 40 changed files with 1,099 additions and 680 deletions.
55 changes: 41 additions & 14 deletions end-to-end-tests/pageObjects/pageEditor/modListingPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
*/

import { BasePageObject } from "../basePageObject";
import { uuidv4 } from "@/types/helpers";
import { ModifiesModFormState } from "./utils";

export type StarterBrickUIName =
Expand All @@ -27,6 +26,23 @@ export type StarterBrickUIName =
| "Dynamic Quick Bar"
| "Sidebar Panel";

const domainRegExp =
/(?:[\da-z](?:[\da-z-]{0,61}[\da-z])?\.)+[\da-z][\da-z-]{0,61}[\da-z]/i;

const starterBrickUINameToComponentDefaultName: Record<
StarterBrickUIName,
RegExp
> = {
"Context Menu": /Context menu item/,
// eslint-disable-next-line security/detect-non-literal-regexp -- Inserting from another static RegExp
Trigger: new RegExp(`My ${domainRegExp.source} trigger`),
// eslint-disable-next-line security/detect-non-literal-regexp -- Inserting from another static RegExp
Button: new RegExp(`My ${domainRegExp.source} button`),
"Quick Bar Action": /Quick Bar item/,
"Dynamic Quick Bar": /Dynamic Quick Bar/,
"Sidebar Panel": /Sidebar Panel/,
};

export class ModActionMenu extends BasePageObject {
get copyButton() {
return this.getByRole("menuitem", { name: "Make a copy" });
Expand All @@ -44,7 +60,10 @@ export class ModActionMenu extends BasePageObject {
}

export class ModListItem extends BasePageObject {
saveButton = this.locator("[data-icon=save]");
get saveButton() {
return this.locator("[data-icon=save]");
}

get menuButton() {
return this.getByLabel(" - Ellipsis");
}
Expand All @@ -65,30 +84,38 @@ export class ModListItem extends BasePageObject {
}

export class ModListingPanel extends BasePageObject {
addButton = this.getByRole("button", { name: "Add", exact: true });
quickFilterInput = this.getByPlaceholder("Quick filter");
newModButton = this.getByRole("button", { name: "New Mod", exact: true });

get activeModListItem() {
return new ModListItem(this.locator(".list-group-item.active"));
}

/**
* Adds a starter brick in the Page Editor. Generates a unique mod name to prevent
* test collision.
* Adds a new mod in the Page Editor, with one component with the given starter brick type.
*
* When adding a Button starter brick, the caller is responsible for placing the button on the page. See
* `selectConnectedPageElement`
*
* @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
* @param starterBrickName the UI label of the starter brick to add
* @returns matcher to match the auto-generated mod component name
*/
@ModifiesModFormState
async addStarterBrick(starterBrickName: StarterBrickUIName) {
const modUuid = uuidv4();
const modComponentName = `Test ${starterBrickName} ${modUuid}`;
await this.addButton.click();
async addNewMod({
starterBrickName,
}: {
starterBrickName: StarterBrickUIName;
}): Promise<{
modComponentNameMatcher: RegExp;
}> {
await this.newModButton.click();
await this.locator("[role=button].dropdown-item", {
hasText: starterBrickName,
}).click();

return { modComponentName, modUuid };
return {
modComponentNameMatcher:
starterBrickUINameToComponentDefaultName[starterBrickName],
};
}

getModListItemByName(modName: string) {
Expand Down
65 changes: 65 additions & 0 deletions end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { ModEditorPane } from "./modEditorPane";
import { ModifiesModFormState } from "./utils";
import { CreateModModal } from "./createModModal";
import { DeactivateModModal } from "end-to-end-tests/pageObjects/pageEditor/deactivateModModal";
import { uuidv4 } from "@/types/helpers";

class EditorPane extends BasePageObject {
editTab = this.getByRole("tab", { name: "Edit" });
Expand Down Expand Up @@ -134,6 +135,70 @@ export class PageEditorPage extends BasePageObject {
);
}

/**
* Save a new mod with the given name and optional description.
*
* @param currentModName the current name (not registry id) of the mod to save
* @param descriptionOverride the optional description override
* @returns the RegistryId of the saved mod
*/
@ModifiesModFormState
async saveNewMod({
currentModName,
descriptionOverride,
}: {
currentModName: string;
descriptionOverride?: string;
}): Promise<{
modId: string;
}> {
const modListItem =
this.modListingPanel.getModListItemByName(currentModName);
await modListItem.select();
// Expect the mod metadata editor to be showing form for a mod that's never been saved before
await expect(
this.modEditorPane.editMetadataTabPanel.getByPlaceholder(
"Save the mod to assign a Mod ID",
),
).toBeVisible();
// eslint-disable-next-line playwright/no-wait-for-timeout -- The save button re-mounts several times so we need a slight delay here before playwright clicks
await this.page.waitForTimeout(2000);
await modListItem.saveButton.click();

// Handle the "Save new mod" modal
const saveNewModModal = this.page.locator(".modal-content");
await expect(saveNewModModal).toBeVisible();
await expect(saveNewModModal.getByText("Save new mod")).toBeVisible();

// // Can't use getByLabel to target because the field is composed of multiple widgets
const registryIdInput = saveNewModModal.getByTestId("registryId-id-id");
const currentId = await registryIdInput.inputValue();
// Add a random uuid to the mod id to prevent test collisions
await registryIdInput.fill(`${currentId}-${uuidv4()}`);

if (descriptionOverride) {
// Update the mod description
// TODO: https://github.com/pixiebrix/pixiebrix-extension/issues/9238, prefer getByLabel
const descriptionInput = saveNewModModal.locator("#description");
await descriptionInput.fill(descriptionOverride);
}

// Click the Save button in the modal
await saveNewModModal.getByRole("button", { name: "Save" }).click();

// Wait for the save confirmation
await expect(
this.page
.getByRole("status")
.filter({ hasText: "Mod created successfully" }),
).toBeVisible();

const modId =
await this.modEditorPane.editMetadataTabPanel.modId.inputValue();

return { modId };
}

@ModifiesModFormState
async saveStandaloneMod(modName: string, modUuid: UUID) {
const modListItem = this.modListingPanel.getModListItemByName(modName);
Expand Down
4 changes: 3 additions & 1 deletion end-to-end-tests/tests/extensionConsole/activation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,9 @@ test("can activate a mod via url", async ({ page, extensionId }) => {
`chrome-extension://${extensionId}/options.html#/marketplace/activate/${modIdUrlEncoded}`,
);
}).toPass({ timeout: 5000 });
await expect(page.getByRole("code")).toContainText(modId);
await expect(page.getByRole("code")).toContainText(modId, {
timeout: 10_000,
});

const modActivationPage = new ActivateModPage(page, extensionId, modId);
await modActivationPage.clickActivateAndWaitForModsPageRedirect();
Expand Down
36 changes: 20 additions & 16 deletions end-to-end-tests/tests/modLifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,19 @@ test("create, run, package, and update mod", async ({
await page.goto("/create-react-app/table");
const pageEditorPage = await newPageEditorPage(page.url());

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

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

await test.step("Configure the Button brick", async () => {
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 () => {
Expand Down Expand Up @@ -93,10 +90,17 @@ test("create, run, package, and update mod", async ({
);
});

const { modId } = await pageEditorPage.createModFromModComponent({
modNameRoot: "Lifecycle Test",
modComponentName,
modUuid,
const newModName = "Lifecycle Test";
const modListItem =
pageEditorPage.modListingPanel.getModListItemByName("New Mod");
await modListItem.select();
await pageEditorPage.modEditorPane.editMetadataTabPanel.fillField(
"name",
newModName,
);
const { modId } = await pageEditorPage.saveNewMod({
currentModName: newModName,
descriptionOverride: "Created through Playwright Automation",
});

let newPage: Page | undefined;
Expand All @@ -119,8 +123,8 @@ test("create, run, package, and update mod", async ({
"version: 1.0.1",
);
await editWorkshopModPage.editor.findAndReplaceText(
"description: Created with the PixieBrix Page Editor",
"description: Created through Playwright Automation",
"description: Created and updated with Playwright Automation",
);
await editWorkshopModPage.updateBrick();
});
Expand All @@ -137,7 +141,7 @@ test("create, run, package, and update mod", async ({
const modActivatePage = new ActivateModPage(newPage!, extensionId, modId);

await expect(modActivatePage.locator("form")).toContainText(
"Created through Playwright Automation",
"Created and updated with Playwright Automation",
);
});

Expand Down
Loading

0 comments on commit c3648a0

Please sign in to comment.