Skip to content

Commit

Permalink
Fix CreateModModalBody logic
Browse files Browse the repository at this point in the history
  • Loading branch information
twschiller committed Oct 6, 2024
1 parent f402421 commit 99f9601
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 25 deletions.
9 changes: 6 additions & 3 deletions end-to-end-tests/pageObjects/pageEditor/createModModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { BasePageObject } from "../basePageObject";
import { type UUID } from "@/types/stringTypes";
import { ModifiesModFormState } from "./utils";
import { uuidv4 } from "@/types/helpers";

export class CreateModModal extends BasePageObject {
modIdInput = this.getByTestId("registryId-id-id");
Expand All @@ -27,11 +28,13 @@ export class CreateModModal extends BasePageObject {
/**
* Creates a mod using the Create Mod modal, with the given modId and modName.
* @param modName the modName to use
* @param modUuid the UUID of the mod component from adding the starter brick
* @param modUuid an optional UUID to force the modId to be unique, if not provided a random UUID will be generated
*/
@ModifiesModFormState
async createMod(modName: string, modUuid: UUID): Promise<string> {
const modId = `${modName.split(" ").join("-").toLowerCase()}-${modUuid}`;
async createMod(modName: string, modUuid?: UUID): Promise<string> {
const modId = `${modName.split(" ").join("-").toLowerCase()}-${
modUuid ?? uuidv4()
}`;

await this.modIdInput.fill(modId);
await this.modNameInput.fill(modName);
Expand Down
65 changes: 58 additions & 7 deletions end-to-end-tests/pageObjects/pageEditor/pageEditorPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import { uuidv4 } from "@/types/helpers";

class EditorPane extends BasePageObject {
editTab = this.getByRole("tab", { name: "Edit" });
logsTab = this.getByRole("tab", { name: "Logs" });

runTriggerButton = this.getByRole("button", { name: "Run Trigger" });
autoRunTrigger = this.getSwitchByLabel("Auto-Run");
Expand Down Expand Up @@ -199,16 +198,68 @@ export class PageEditorPage extends BasePageObject {
return { modId };
}

/**
* Create a new mod by moving a mod component to a new mod.
* @param sourceModComponentName the name of the mod component to move
* @param destinationModName the root name of the new mod
*/
@ModifiesModFormState
async saveStandaloneMod(modName: string, modUuid: UUID) {
const modListItem = this.modListingPanel.getModListItemByName(modName);
await modListItem.select();
await modListItem.saveButton.click();
async moveModComponentToNewMod({
sourceModComponentName,
destinationModName,
}: {
sourceModComponentName: string;
destinationModName: string;
}) {
const modListItem = this.modListingPanel.getModListItemByName(
sourceModComponentName,
);
await modListItem.menuButton.click();
await this.getByRole("menuitem", { name: "Move to Mod" }).click();

const moveDialog = this.getByRole("dialog");

await moveDialog.getByRole("combobox").click();
await moveDialog.getByRole("option", { name: /Create new mod.../ }).click();
await moveDialog.getByRole("button", { name: "Move" }).click();

// Create mod modal is shown
const createModModal = new CreateModModal(this.getByRole("dialog"));
const modId = await createModModal.createMod(modName, modUuid);

this.savedPackageModIds.push(modId);
const modId = await createModModal.createMod(destinationModName);
return { modId };
}

/**
* Create a new mod by copying a mod component to a new mod.
* @param sourceModComponentName the name of the mod component to move
* @param destinationModName the root name of the new mod
*/
@ModifiesModFormState
async copyModComponentToNewMod({
sourceModComponentName,
destinationModName,
}: {
sourceModComponentName: string;
destinationModName: string;
}) {
const modListItem = this.modListingPanel.getModListItemByName(
sourceModComponentName,
);
await modListItem.menuButton.click();
await this.getByRole("menuitem", { name: "Copy to Mod" }).click();

const moveDialog = this.getByRole("dialog");

await moveDialog.getByRole("combobox").click();
await moveDialog.getByRole("option", { name: /Create new mod.../ }).click();
await moveDialog.getByRole("button", { name: "Copy" }).click();

// Create mod modal is shown
const createModModal = new CreateModModal(this.getByRole("dialog"));

const modId = await createModModal.createMod(destinationModName);
return { modId };
}

@ModifiesModFormState
Expand Down
116 changes: 116 additions & 0 deletions end-to-end-tests/tests/pageEditor/moveCopyModComponent.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* 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, expect } from "../../fixtures/testBase";
// @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 { uuidv4 } from "@/types/helpers";

test("Create new mod by moving mod component", async ({
page,
newPageEditorPage,
}) => {
await page.goto("/");
const pageEditorPage = await newPageEditorPage(page.url());

await test.step("Add new Trigger starter brick", async () => {
const { modComponentNameMatcher } =
await pageEditorPage.modListingPanel.addNewMod({
starterBrickName: "Trigger",
});

await expect(
pageEditorPage.brickConfigurationPanel.getByRole("textbox", {
name: "Name",
}),
).toHaveValue(modComponentNameMatcher);
});

const modComponentName = await pageEditorPage.brickConfigurationPanel
.getByRole("textbox", {
name: "Name",
})
.inputValue();

// Since 2.1.4, new mods are created with the name "New Mod" instead of being a standalone mod component
// Use span locator to distinguish from the New Mod button
await expect(
pageEditorPage.locator("span").filter({ hasText: "New Mod" }),
).toBeVisible();

const modName = `Destination Mod ${uuidv4()}`;

await pageEditorPage.moveModComponentToNewMod({
sourceModComponentName: modComponentName,
destinationModName: modName,
});

await expect(pageEditorPage.getByText(modName)).toBeVisible();
await expect(pageEditorPage.getByText(modComponentName)).toBeVisible();

// Should not be visible. Because it's only mod component was moved
await expect(
pageEditorPage.locator("span").filter({ hasText: "New Mod" }),
).toBeHidden();
});

test("Create new mod by copying a mod component", async ({
page,
newPageEditorPage,
}) => {
await page.goto("/");
const pageEditorPage = await newPageEditorPage(page.url());

await test.step("Add new Trigger starter brick", async () => {
const { modComponentNameMatcher } =
await pageEditorPage.modListingPanel.addNewMod({
starterBrickName: "Trigger",
});

await expect(
pageEditorPage.brickConfigurationPanel.getByRole("textbox", {
name: "Name",
}),
).toHaveValue(modComponentNameMatcher);
});

const modComponentName = await pageEditorPage.brickConfigurationPanel
.getByRole("textbox", {
name: "Name",
})
.inputValue();

// Since 2.1.4, new mods are created with the name "New Mod" instead of being a standalone mod component
// Use span locator to distinguish from the New Mod button
await expect(
pageEditorPage.locator("span").filter({ hasText: "New Mod" }),
).toBeVisible();

const modName = `Destination Mod ${uuidv4()}`;

await pageEditorPage.copyModComponentToNewMod({
sourceModComponentName: modComponentName,
destinationModName: modName,
});

// Use span locator to distinguish from the New Mod button
await expect(
pageEditorPage.locator("span").filter({ hasText: "New Mod" }),
).toBeVisible();
await expect(pageEditorPage.getByText(modName)).toBeVisible();
await expect(pageEditorPage.getByText(modComponentName)).toHaveCount(2);
});
28 changes: 13 additions & 15 deletions src/pageEditor/modListingPanel/modals/CreateModModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,21 +163,19 @@ const CreateModModalBody: React.FC = () => {
);

const { createModFromMod } = useCreateModFromMod();
const { createModFromUnsavedMod } = useCreateModFromUnsavedMod();
const { createModFromComponent } = useCreateModFromModComponent(
activeModComponentFormState,
);

// `selectActiveModId` returns the mod id if a mod is selected. Assumption: if the CreateModal
// is open, and a mod is active, then we're performing a "Save as New" on that mod.
// `selectActiveModId` returns the mod id if a mod entry is selected (not a mod component within the mod)
const directlyActiveModId = useSelector(selectActiveModId);
const activeModId =
directlyActiveModId ?? activeModComponentFormState?.modMetadata?.id;

const { data: activeModDefinition, isFetching: isModFetching } =
const { data: activeModDefinition, isFetching: isModDefinitionFetching } =
useOptionalModDefinition(activeModId);

const { createModFromUnsavedMod } = useCreateModFromUnsavedMod();

const formSchema = useFormSchema();

const hideModal = useCallback(() => {
Expand All @@ -191,21 +189,20 @@ const CreateModModalBody: React.FC = () => {
});

const onSubmit: OnSubmit<ModMetadataFormState> = async (values, helpers) => {
if (isModFetching) {
if (isModDefinitionFetching) {
helpers.setSubmitting(false);
return;
}

try {
// If the active mod's saved definition could be loaded from the server, we need to use createModFromMod
if (activeModDefinition) {
await createModFromMod(activeModDefinition, values);
} else if (activeModId && isInnerDefinitionRegistryId(activeModId)) {
// New local mod, definition couldn't be fetched from the server, so we use createModFromUnsavedMod
await createModFromUnsavedMod(activeModId, values);
} else if (activeModComponentFormState) {
// Stand-alone mod component
if (activeModComponentFormState) {
// Move/Copy a mod component to create a new mod
await createModFromComponent(activeModComponentFormState, values);
} else if (directlyActiveModId && activeModDefinition) {
await createModFromMod(activeModDefinition, values);
} else if (directlyActiveModId) {
// If the mod is unsaved or there was an error fetching the mod definition from the server
await createModFromUnsavedMod(directlyActiveModId, values);
} else {
// Should not happen in practice
// noinspection ExceptionCaughtLocallyJS
Expand All @@ -215,6 +212,7 @@ const CreateModModalBody: React.FC = () => {
notify.success({
message: "Mod created successfully",
});

hideModal();
} catch (error) {
if (isSingleObjectBadRequestError(error) && error.response.data.config) {
Expand Down Expand Up @@ -280,7 +278,7 @@ const CreateModModalBody: React.FC = () => {

return (
<>
{isModFetching ? (
{isModDefinitionFetching ? (
<Loader />
) : (
<Form
Expand Down

0 comments on commit 99f9601

Please sign in to comment.