ui/e2e/explorer/service/index.spec.ts
import {
PatchOperationType,
PatchOperations,
PatchSchemaType,
} from "~/pages/namespace/Explorer/Service/ServiceEditor/schema";
import { createNamespace, deleteNamespace } from "e2e/utils/namespace";
import { createService, createServiceYaml } from "./utils";
import { expect, test } from "@playwright/test";
import { EnvVarSchemaType } from "~/api/services/schema/services";
import { faker } from "@faker-js/faker";
let namespace = "";
test.beforeEach(async () => {
namespace = await createNamespace();
});
test.afterEach(async () => {
await deleteNamespace(namespace);
namespace = "";
});
test("it is possible to create a service", async ({ page }) => {
/* prepare data */
/**
* note: keep number of variables and patches low because we only
* compare the yaml that is visible in the editor at one time
**/
const envs = Array.from({ length: 3 }, () => ({
name: faker.lorem.word(),
value: faker.git.shortSha(),
}));
const patches = Array.from({ length: 2 }, () => ({
op: PatchOperations[Math.floor(Math.random() * 3)] as PatchOperationType,
path: faker.internet.url(),
value: faker.lorem.words(3),
}));
const service = {
name: "mynewservice.yaml",
image: "bash",
scale: 2,
size: "medium",
cmd: "hello",
envs,
patches,
};
const expectedYaml = createServiceYaml(service);
/* visit page */
await page.goto(`/n/${namespace}/explorer/tree`, {
waitUntil: "networkidle",
});
await expect(
page.getByTestId("breadcrumb-namespace"),
"it navigates to the test namespace in the explorer"
).toHaveText(namespace);
/* create service */
await page.getByRole("button", { name: "New" }).first().click();
await page.getByRole("button", { name: "New Service" }).click();
await expect(page.getByRole("button", { name: "Create" })).toBeDisabled();
await page.getByPlaceholder("service-name.yaml").fill(service.name);
await page.getByRole("button", { name: "Create" }).click();
await expect(
page,
"it creates the service and opens the file in the explorer"
).toHaveURL(`/n/${namespace}/explorer/service/${service.name}`);
/* fill in form */
await page.getByLabel("Image").fill("bash");
await page.locator("button").filter({ hasText: "Select a scale" }).click();
await page.getByLabel(service.scale.toString()).click();
await page.locator("button").filter({ hasText: "Select a size" }).click();
await page.getByLabel(service.size).click();
await page.getByLabel("Cmd").fill(service.cmd);
/* add patches */
for (let i = 0; i < patches.length; i++) {
const item = patches[i] as PatchSchemaType;
await page.getByRole("button", { name: "add patch" }).click();
await page.getByLabel("Operation").click();
await page.getByLabel(item.op).click();
await page.getByLabel("path").fill(item.path);
await page.keyboard.press("Tab");
await page.type("textarea", item.value);
await page.getByRole("button", { name: "Save" }).click();
}
/* add env variables */
const envsElement = page
.locator("fieldset")
.filter({ hasText: "Environment variables" });
for (let i = 0; i < envs.length; i++) {
const item = envs[i] as EnvVarSchemaType;
await expect(
envsElement.getByPlaceholder("NAME"),
"it renders one set of inputs for every existing env +1 empty set"
).toHaveCount(i + 1);
await envsElement.getByPlaceholder("NAME").last().fill(item.name);
await envsElement.getByPlaceholder("VALUE").last().fill(item.value);
await envsElement.getByRole("button").last().click();
}
/**
* assert preview editor content
* note that only the visible part of the yaml is compared, so
* this will fail if the document gets too long.
*/
const editor = page.locator(".lines-content");
await expect(
editor,
"all entered data is represented in the editor preview"
).toContainText(expectedYaml, { useInnerText: true });
await expect(
page.getByTestId("unsaved-note"),
"it renders a hint that there are unsaved changes"
).toBeVisible();
await page.getByRole("button", { name: "Save" }).click();
await expect(
page.getByTestId("unsaved-note"),
"it does not render a hint that there are unsaved changes"
).not.toBeVisible();
/* reload and assert data has been persisted */
await page.reload({ waitUntil: "domcontentloaded" });
await expect(
editor,
"after reloading, the entered data is still in the editor preview"
).toContainText(expectedYaml, { useInnerText: true });
await expect(page.getByLabel("Image")).toHaveValue("bash");
await expect(page.locator("button").filter({ hasText: "2" })).toBeVisible();
await expect(
page.locator("button").filter({ hasText: "medium" })
).toBeVisible();
await expect(page.getByLabel("Cmd")).toHaveValue("hello");
await Promise.all(
envs.map(async (item, index) => {
await expect(
page.getByTestId("env-item-form").getByTestId("env-name").nth(index)
).toHaveValue(item.name);
await expect(
page.getByTestId("env-item-form").getByTestId("env-value").nth(index)
).toHaveValue(item.value);
})
);
await expect(
page.getByRole("cell", { name: `${patches.length} Patches` }),
"It renders a table of patches, displaying the number of patches in the header"
).toBeVisible();
await Promise.all(
patches.map(async (item, index) => {
const currentElement = page.getByTestId("patch-row").nth(index);
await expect(currentElement).toContainText(item.op);
await expect(currentElement).toContainText(item.path);
})
);
});
test("it is possible to edit patches", async ({ page }) => {
/* prepare data */
const patches = Array.from({ length: 4 }, () => ({
op: PatchOperations[Math.floor(Math.random() * 3)] as PatchOperationType,
path: faker.internet.url(),
value: faker.lorem.words(3),
}));
const service = {
name: "mynewservice.yaml",
image: "bash",
scale: 2,
size: "medium",
cmd: "hello",
patches,
};
await createService(namespace, service);
/* visit page, assert content rendered */
await page.goto(`/n/${namespace}/explorer/service/${service.name}`);
await Promise.all(
patches.map(async (item, index) => {
const currentElement = page.getByTestId("patch-row").nth(index);
await expect(currentElement).toContainText(item.op);
await expect(currentElement).toContainText(item.path);
})
);
/* update list and assert content after each manipulation*/
await page.getByTestId("patch-row").nth(1).getByRole("button").click();
await page.getByRole("button", { name: "Move down" }).click();
let expectNewPatches;
expectNewPatches = [
patches[0],
patches[2],
patches[1],
patches[3],
] as PatchSchemaType[];
await Promise.all(
expectNewPatches.map(async (item, index) => {
const currentElement = page.getByTestId("patch-row").nth(index);
await expect(currentElement).toContainText(item.op);
await expect(currentElement).toContainText(item.path);
})
);
await page.getByTestId("patch-row").nth(3).getByRole("button").click();
await page.getByRole("button", { name: "Move up" }).click();
expectNewPatches = [
patches[0],
patches[2],
patches[3],
patches[1],
] as PatchSchemaType[];
await Promise.all(
expectNewPatches.map(async (item, index) => {
const currentElement = page.getByTestId("patch-row").nth(index);
await expect(currentElement).toContainText(item.op);
await expect(currentElement).toContainText(item.path);
})
);
await page.getByTestId("patch-row").nth(1).getByRole("button").click();
await page.getByRole("button", { name: "Delete" }).click();
expectNewPatches = [patches[0], patches[3], patches[1]] as PatchSchemaType[];
await Promise.all(
expectNewPatches.map(async (item, index) => {
const currentElement = page.getByTestId("patch-row").nth(index);
await expect(currentElement).toContainText(item.op);
await expect(currentElement).toContainText(item.path);
})
);
/* edit one patch */
const updatedPatch: PatchSchemaType = {
op: PatchOperations[Math.floor(Math.random() * 3)] as PatchOperationType,
path: faker.internet.url(),
value: faker.lorem.words(3),
};
const patchToEdit = expectNewPatches[1];
if (!patchToEdit) throw Error("patch to edit is undefined");
await page.getByTestId("patch-row").nth(1).click();
await page.getByLabel("Operation").click();
await page.getByLabel(updatedPatch.op).click();
await page.getByLabel("Path").fill(updatedPatch.path);
const editorTarget = await page
.getByRole("dialog")
.getByText(patchToEdit.value, {
exact: true,
});
await editorTarget.click();
await page.locator("textarea").last().fill(updatedPatch.value);
await page.getByRole("button", { name: "Save" }).click();
expectNewPatches = [
patches[0],
updatedPatch,
patches[1],
] as PatchSchemaType[];
await Promise.all(
expectNewPatches.map(async (item, index) => {
const currentElement = page.getByTestId("patch-row").nth(index);
await expect(currentElement).toContainText(item.op);
await expect(currentElement).toContainText(item.path);
})
);
/* assert preview has been updated */
const updatedService = {
name: "mynewservice.yaml",
image: "bash",
scale: 2,
size: "medium",
cmd: "hello",
patches: expectNewPatches,
};
const expectedYaml = createServiceYaml(updatedService);
const editor = page.locator(".lines-content");
await expect(
editor,
"all entered data is represented in the editor preview"
).toContainText(expectedYaml, { useInnerText: true });
/* note: saving the plugin should have saved the whole file. */
await expect(
page.getByTestId("unsaved-note"),
"it does not render a hint that there are unsaved changes"
).not.toBeVisible();
});
test("it is possible to edit environment variables", async ({ page }) => {
const envs = Array.from({ length: 5 }, () => ({
name: faker.lorem.word(),
value: faker.git.shortSha(),
}));
const service = {
name: "mynewservice.yaml",
image: "bash",
scale: 2,
size: "medium",
cmd: "hello",
envs,
};
await createService(namespace, service);
/* visit page, assert content rendered */
await page.goto(`/n/${namespace}/explorer/service/${service.name}`);
await Promise.all(
envs.map(async (item, index) => {
await expect(
page.getByTestId("env-item-form").getByTestId("env-name").nth(index)
).toHaveValue(item.name);
await expect(
page.getByTestId("env-item-form").getByTestId("env-value").nth(index)
).toHaveValue(item.value);
})
);
/* edit one item */
const updatedEnv: EnvVarSchemaType = {
name: faker.lorem.word(),
value: faker.git.shortSha(),
};
const envToEdit = envs[3];
if (!envToEdit) throw Error("env to edit is undefined");
await page.getByTestId("env-name").nth(3).fill(updatedEnv.name);
await page.getByTestId("env-value").nth(3).fill(updatedEnv.value);
let expectNewEnvs;
expectNewEnvs = [envs[1], envs[2], updatedEnv, envs[4]] as EnvVarSchemaType[];
/* delete items and assert rendered list is updated*/
await page.getByTestId("env-item-form").getByRole("button").nth(0).click();
await page.getByTestId("env-item-form").getByRole("button").nth(2).click();
expectNewEnvs = [envs[1], envs[2], envs[4]] as EnvVarSchemaType[];
await Promise.all(
expectNewEnvs.map(async (item, index) => {
await expect(
page.getByTestId("env-item-form").getByTestId("env-name").nth(index)
).toHaveValue(item.name);
await expect(
page.getByTestId("env-item-form").getByTestId("env-value").nth(index)
).toHaveValue(item.value);
})
);
/* assert preview has been updated */
const updatedService = {
name: "mynewservice.yaml",
image: "bash",
scale: 2,
size: "medium",
cmd: "hello",
envs: expectNewEnvs,
};
const expectedYaml = createServiceYaml(updatedService);
const editor = page.locator(".lines-content");
await expect(
editor,
"all entered data is represented in the editor preview"
).toContainText(expectedYaml, { useInnerText: true });
await expect(
page.getByTestId("unsaved-note"),
"it renders a hint that there are unsaved changes"
).toBeVisible();
await page.getByRole("button", { name: "Save" }).click();
await expect(
page.getByTestId("unsaved-note"),
"it does not render a hint that there are unsaved changes"
).not.toBeVisible();
});