Skip to content

Commit

Permalink
Create mod definitions fixture for e2e tests (#8604)
Browse files Browse the repository at this point in the history
* Renaming fixture files for clarity

* fix renaming

* Create mod definitions fixture for e2e tests

* fix merging fixtures

* add extra note

* lint error fixes

* PR feedback
  • Loading branch information
fungairino authored Jun 13, 2024
1 parent 5256aa0 commit d095691
Show file tree
Hide file tree
Showing 17 changed files with 326 additions and 35 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ module.exports = {
"no-restricted-imports": "off",
"unicorn/prefer-dom-node-dataset": "off",
"unicorn/prefer-module": "off", // `import.meta.dirname` throws "cannot use 'import meta' outside a module"
"no-await-in-loop": "off",
"playwright/no-skipped-test": [
"error",
{
Expand Down
52 changes: 52 additions & 0 deletions end-to-end-tests/fixtures/modDefinitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 { test as pageContextFixture } from "./pageContext";
import { WorkshopPage } from "../pageObjects/extensionConsole/workshop/workshopPage";

export const test = pageContextFixture.extend<{
// These should correspond 1-1 with the mod definition file names in the fixtures/modDefinitions directory
modDefinitionNames: string[];
createdModIds: string[];
}>({
modDefinitionNames: [],
createdModIds: [
async ({ modDefinitionNames, page, extensionId }, use) => {
const createdIds: string[] = [];
if (modDefinitionNames.length > 0) {
const workshopPage = new WorkshopPage(page, extensionId);
for (const definition of modDefinitionNames) {
await workshopPage.goto();
const createdModId =
await workshopPage.createNewModFromDefinition(definition);
createdIds.push(createdModId);
}
}

await use(createdIds);

if (createdIds.length > 0) {
const workshopPage = new WorkshopPage(page, extensionId);
for (const id of createdIds) {
await workshopPage.goto();
await workshopPage.deletePackagedModByModId(id);
}
}
},
{ auto: true },
],
});
27 changes: 27 additions & 0 deletions end-to-end-tests/fixtures/modDefinitions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
This directory contains yaml definition files for mods used in end to end tests. They are
used alongside the `modDefinitions` fixture to automatically create the specified mods
and handle their cleanup for each test.

Definition file names must be lowercase in dash-case. The definition yaml must contain a placeholder value
for the mod id, which will be replaced with the actual mod id when the mod is created. The placeholder
value is `"{{ modId }}"`.

Test Usage:

```
// Specify what mods to create and use in the test. In this case, a file named `mod-example-1.yaml` should be
// in the `modDefinitions` directory.
test.use({
modDefinitions: ['mod-example-1'],
});
test('test name', async ({
page,
extensionId,
createdModIds,
}) => {
// Get the created mod Id from the `createdModIds` fixture
const modExample1Id = createdModIds[0];
// Activate the mod, etc.
});
```
73 changes: 73 additions & 0 deletions end-to-end-tests/fixtures/modDefinitions/simple-sidebar-panel.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
kind: recipe
options:
schema:
type: object
properties: {}
uiSchema:
ui:order:
- "*"
metadata:
id: "{{ modId }}"
name: Simple Sidebar Panel
version: 1.0.0
description: Created with the PixieBrix Page Editor
apiVersion: v3
definitions:
extensionPoint:
kind: extensionPoint
definition:
type: actionPanel
reader:
- "@pixiebrix/document-metadata"
- "@pixiebrix/document-context"
isAvailable:
matchPatterns:
- https://pbx.vercel.app/*
urlPatterns: []
selectors: []
trigger: load
debounce:
waitMillis: 250
leading: false
trailing: true
customEvent: null
extensionPoints:
- label: Simple Sidebar Panel
config:
heading: Simple Sidebar Panel
body:
- id: "@pixiebrix/document"
rootMode: document
config:
body:
- type: container
config: {}
children:
- type: row
config: {}
children:
- type: column
config: {}
children:
- type: header
config:
title: !nunjucks Simple Sidebar Panel
heading: h1
- type: row
config: {}
children:
- type: column
config: {}
children:
- type: text
config:
text: !nunjucks >-
Simple sidebar panel for testing sidepanel
open/close behavior
enableMarkdown: true
root: null
permissions:
origins: []
permissions: []
id: extensionPoint
services: {}
2 changes: 1 addition & 1 deletion end-to-end-tests/fixtures/pageContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export const test = base.extend<
});

for (const page of pageEditorPages) {
// eslint-disable-next-line no-await-in-loop -- optimization via parallelization not relevant here
await page.bringToFront();
await page.cleanup();
}
},
Expand Down
8 changes: 7 additions & 1 deletion end-to-end-tests/fixtures/testBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
import { mergeTests } from "@playwright/test";
import { test as pageContextFixture } from "./pageContext";
import { test as envFixture } from "./environmentCheck";
import { test as modDefinitionsFixture } from "./modDefinitions";

export const test = mergeTests(
pageContextFixture,
envFixture,
modDefinitionsFixture,
);

export const test = mergeTests(pageContextFixture, envFixture);
export const { expect } = test;
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 { type Locator, type Page } from "@playwright/test";
import { WorkshopModEditor } from "./modEditor";

export class CreateWorkshopModPage {
readonly editor: WorkshopModEditor;
readonly createBrickButton: Locator;

constructor(private readonly page: Page) {
this.editor = new WorkshopModEditor(this.page);
this.createBrickButton = this.page.getByRole("button", {
name: "Create Brick",
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,12 @@
*/

import { type Page } from "@playwright/test";
import { WorkshopModEditor } from "./modEditor";

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();
readonly editor: WorkshopModEditor;
constructor(private readonly page: Page) {
this.editor = new WorkshopModEditor(this.page);
}

async updateBrick() {
Expand All @@ -46,5 +31,7 @@ export class EditWorkshopModPage {
async deleteBrick() {
await this.page.getByRole("button", { name: "Delete Brick" }).click();
await this.page.getByRole("button", { name: "Permanently Delete" }).click();
// eslint-disable-next-line playwright/no-networkidle -- for some reason, can't assert on the "Brick deleted" notice
await this.page.waitForLoadState("networkidle");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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 Locator, type Page } from "@playwright/test";
import fs from "node:fs/promises";
import path from "node:path";
import { uuidv4 } from "@/types/helpers";

export class WorkshopModEditor {
readonly textArea: Locator;
readonly baseLocator: Locator;

constructor(private readonly page: Page) {
this.baseLocator = this.page.getByLabel("Editor");
this.textArea = this.baseLocator.getByRole("textbox");
}

async findText(text: string) {
await this.baseLocator.locator(".ace_content").click(); // Focus on the visible editor
await this.page.keyboard.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();
}

// Loads the corresponding yaml from the fixtures/modDefinitions directory
async replaceWithModDefinition(modDefinitionName: string) {
const modDefinition = await fs.readFile(
path.join(
__dirname,
`../../../fixtures/modDefinitions/${modDefinitionName}.yaml`,
),
"utf8",
);
const uuid = uuidv4();
const modId = `@extension-e2e-test-unaffiliated/${modDefinitionName}-${uuid}`;
const replacedDefinition = modDefinition.replace("{{ modId }}", modId);

await this.textArea.fill(replacedDefinition);
return modId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,23 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { type Page } from "@playwright/test";
import { getBaseExtensionConsoleUrl } from "../constants";
import { EditWorkshopModPage } from "end-to-end-tests/pageObjects/extensionConsole/editWorkshopModPage";
import { type Locator, type Page, expect } from "@playwright/test";
import { getBaseExtensionConsoleUrl } from "../../constants";
import { EditWorkshopModPage } from "end-to-end-tests/pageObjects/extensionConsole/workshop/editWorkshopModPage";
import { CreateWorkshopModPage } from "./createWorkshopModPage";

export class WorkshopPage {
private readonly extensionConsoleUrl: string;
private readonly createNewBrickButton: Locator;

constructor(
private readonly page: Page,
extensionId: string,
) {
this.extensionConsoleUrl = getBaseExtensionConsoleUrl(extensionId);
this.createNewBrickButton = this.page.getByRole("button", {
name: "Create New Brick",
});
}

async goto() {
Expand All @@ -47,8 +52,19 @@ export class WorkshopPage {
return new EditWorkshopModPage(this.page);
}

async createNewModFromDefinition(modDefinitionName: string) {
await this.createNewBrickButton.click();
const createPage = new CreateWorkshopModPage(this.page);
const modId =
await createPage.editor.replaceWithModDefinition(modDefinitionName);
await createPage.createBrickButton.click();
await expect(
this.page.getByRole("status").getByText("Created "),
).toBeVisible({ timeout: 8000 });
return modId;
}

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

// Starter brick names as shown in the Page Editor UI
Expand Down Expand Up @@ -237,14 +237,12 @@ export class PageEditorPage {
const modsPage = new ModsPage(this.page, this.extensionId);
await modsPage.goto();
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 workshopPage.deletePackagedModByModId(packagedModId);
}
}
Expand Down
Loading

0 comments on commit d095691

Please sign in to comment.