ui/e2e/explorer/workflow/run.spec.ts
import { createNamespace, deleteNamespace } from "../../utils/namespace";
import { expect, test } from "@playwright/test";
import {
jsonSchemaFormWorkflow,
jsonSchemaWithRequiredEnum,
testDiacriticsWorkflow,
} from "./utils";
import { noop as basicWorkflow } from "~/pages/namespace/Explorer/Tree/components/modals/CreateNew/Workflow/templates";
import { createFile } from "e2e/utils/files";
import { decode } from "js-base64";
import { faker } from "@faker-js/faker";
import { getInstanceInput } from "~/api/instances/query/input";
import { headers } from "e2e/utils/testutils";
import { prettifyJsonString } from "~/util/helpers";
let namespace = "";
test.beforeEach(async () => {
namespace = await createNamespace();
});
test.afterEach(async () => {
await deleteNamespace(namespace);
namespace = "";
});
test("it is possible to open and use the run workflow modal from the editor and the header of the workflow page", async ({
page,
}) => {
const workflowName = faker.system.commonFileName("yaml");
await createFile({
name: workflowName,
namespace,
type: "workflow",
yaml: basicWorkflow.data,
});
await page.goto(`/n/${namespace}/explorer/workflow/edit/${workflowName}`);
// open modal via editor button
await page.getByTestId("workflow-editor-btn-run").click();
expect(
await page.getByTestId("run-workflow-dialog"),
"it opens the dialog from the editor button"
).toBeVisible();
await page.getByTestId("run-workflow-cancel-btn").click();
expect(await page.getByTestId("run-workflow-dialog")).not.toBeVisible();
// open modal via header button
await page.getByTestId("workflow-header-btn-run").click();
expect(
await page.getByTestId("run-workflow-dialog"),
"it opens the dialog from the header button"
).toBeVisible();
// use the tabs
expect(
await page
.getByTestId("run-workflow-json-tab-btn")
.getAttribute("aria-selected"),
"the json tab is selected by default (since this workflow has no JSON schema)"
).toBe("true");
expect(
await page
.getByTestId("run-workflow-form-tab-btn")
.getAttribute("aria-selected")
).toBe("false");
await page.getByTestId("run-workflow-form-tab-btn").click();
expect(
await page
.getByTestId("run-workflow-form-tab-btn")
.getAttribute("aria-selected"),
"the form tab is now selected"
).toBe("true");
expect(
await page
.getByTestId("run-workflow-json-tab-btn")
.getAttribute("aria-selected")
).toBe("false");
expect(
await page.getByTestId("run-workflow-form-input-hint"),
"it shows a hint that no form could be generated"
).toBeVisible();
await page.getByTestId("run-workflow-cancel-btn").click();
expect(await page.getByTestId("run-workflow-dialog")).not.toBeVisible();
});
test("it is possible to run the workflow by setting an input JSON via the editor", async ({
page,
browserName,
}) => {
const workflowName = faker.system.commonFileName("yaml");
await createFile({
name: workflowName,
namespace,
type: "workflow",
yaml: basicWorkflow.data,
});
await page.goto(`/n/${namespace}/explorer/workflow/edit/${workflowName}`);
await page.getByTestId("workflow-editor-btn-run").click();
expect(
await page.getByTestId("run-workflow-dialog"),
"it opens the dialog"
).toBeVisible();
expect(
await page.getByTestId("run-workflow-submit-btn").isEnabled(),
"the submit button is enabled by default"
).toEqual(true);
await page.type("textarea", "some invalid json");
expect(
await page.getByTestId("run-workflow-submit-btn").isEnabled(),
"submit button is disabled when the json is invalid"
).toEqual(false);
await page.getByTestId("run-workflow-editor").click();
await page.keyboard.press(browserName === "webkit" ? "Meta+A" : "Control+A");
await page.keyboard.press("Backspace");
const userInputString = `{"string": "1", "integer": 1, "boolean": true, "array": [1,2,3], "object": {"key": "value"}}`;
await page.keyboard.type(userInputString);
expect(
await page.getByTestId("run-workflow-submit-btn").isEnabled(),
"submit is enabled when the json is valid"
).toEqual(true);
// submit to run the workflow
await page.getByTestId("run-workflow-submit-btn").click();
const reg = new RegExp(`/n/${namespace}/instances/(.*)`);
await expect(
page,
"workflow was triggered with our input and user was redirected to the instances page"
).toHaveURL(reg);
const instanceId = page.url().match(reg)?.[1];
if (!instanceId) {
throw new Error("instanceId not found");
}
// check the server state of the input
const res = await getInstanceInput({
urlParams: {
baseUrl: process.env.PLAYWRIGHT_UI_BASE_URL,
instanceId,
namespace,
},
headers,
});
const inputResponseString = decode(res.data.input);
expect(
inputResponseString,
"the server result is the same as the input that was sent"
).toBe(userInputString);
});
test("it is possible to run a workflow with input data containing special characters", async ({
page,
}) => {
const name = "test-diacritics.yaml";
await createFile({
name,
namespace,
type: "workflow",
yaml: testDiacriticsWorkflow,
});
await page.goto(`/n/${namespace}/explorer/workflow/edit/${name}`);
await expect(
page.locator(".view-lines"),
"The editor renders special characters correctly"
).toContainText("A workflow for testing characters like îèüñÆ");
await page.getByTestId("workflow-editor-btn-run").click();
await page.getByLabel("Name").fill("Kateřina Horáčková");
await page.getByTestId("run-workflow-submit-btn").click();
await expect(
page.locator(".lines-content"),
"The text from the input is rendered correctly in the workflow output"
).toContainText(
`{
"result": "Hello Kateřina Horáčková"
}`,
{ useInnerText: true }
);
});
test("it is not possible to run the workflow when the editor has unsaved changes", async ({
page,
}) => {
const workflowName = faker.system.commonFileName("yaml");
await createFile({
name: workflowName,
namespace,
type: "workflow",
yaml: basicWorkflow.data,
});
await page.goto(`/n/${namespace}/explorer/workflow/edit/${workflowName}`);
await expect(page.getByTestId("workflow-header-btn-run")).not.toBeDisabled();
await expect(page.getByTestId("workflow-editor-btn-run")).not.toBeDisabled();
await page.type("textarea", faker.random.alphaNumeric(1));
await expect(page.getByTestId("workflow-header-btn-run")).toBeDisabled();
await expect(page.getByTestId("workflow-editor-btn-run")).toBeDisabled();
await page.locator("textarea").press("Backspace");
await expect(
page.getByTestId("workflow-header-btn-run"),
"when the text input is equal to the saved data the run button is active"
).not.toBeDisabled();
await expect(
page.getByTestId("workflow-editor-btn-run"),
"when the text input is equal to the saved data the run button is active"
).not.toBeDisabled();
});
test("it is possible to provide the input via generated form", async ({
page,
}) => {
const workflowName = faker.system.commonFileName("yaml");
await createFile({
name: workflowName,
namespace,
type: "workflow",
yaml: jsonSchemaFormWorkflow,
});
await page.goto(`/n/${namespace}/explorer/workflow/edit/${workflowName}`);
await page.getByTestId("workflow-editor-btn-run").click();
expect(
await page.getByTestId("run-workflow-dialog"),
"it opens the dialog"
).toBeVisible();
expect(
await page
.getByTestId("run-workflow-form-tab-btn")
.getAttribute("aria-selected"),
"it detects the validate step and makes the form tab active by default"
).toBe("true");
// it generated a form (first and last name are required)
await expect(page.getByLabel("First Name")).toBeVisible();
await expect(page.getByLabel("Last Name")).toBeVisible();
await expect(page.getByLabel("Age")).toBeVisible();
await expect(page.getByRole("combobox", { name: "role" })).toBeVisible();
await expect(
await page.getByRole("combobox", { name: "role" }).innerText(),
"the select input shows a fallback text when it has no value"
).toBe("Select role");
await expect(page.getByTestId("json-schema-form-add-button")).toBeVisible();
await expect(page.getByLabel("Age")).toBeVisible();
await expect(page.getByLabel("File")).toBeVisible();
// interact with the select input
await page.getByRole("combobox", { name: "role" }).click();
await page.getByRole("option", { name: "guest" }).click();
await expect(
await page.getByRole("combobox", { name: "role" }).innerText(),
"the select input now shows the selected value"
).toBe("guest");
// interact with the file input
await page
.getByLabel("File")
.setInputFiles("./e2e/utils/fixtures/upload-testfile.txt");
// interact with the array input
await page.getByTestId("json-schema-form-add-button").click();
await page.getByTestId("json-schema-form-add-button").click();
await page.getByTestId("json-schema-form-add-button").click();
await page.getByLabel("array-0*").fill("array item 2");
await page.getByLabel("array-1*").fill("array item 1");
await page.getByTestId("json-schema-form-down-button-0").click(); // switch 1 and 2
await page
.getByLabel("array-2*")
.fill("this will be deleted in the next step");
await page.getByTestId("json-schema-form-remove-button-2").click();
// interact with the number input
await page.getByLabel("Age").fill("2");
// submit this form via enter:
// we have an array form on this page, which also has some buttons
// using enter here makes sure that we will submit the form and
// not trigger the buttons from the array form
await page.keyboard.press("Enter");
// last name is required and we just tried to send the form without filling it
await expect(page.getByLabel("First Name")).toBeFocused();
await page.getByLabel("First Name").fill("Marty");
await page.getByTestId("run-workflow-submit-btn").click();
// first name is also required and will now be focused
await expect(page.getByLabel("Last Name")).toBeFocused();
await page.getByLabel("Last Name").fill("McFly");
await page.getByTestId("run-workflow-submit-btn").click();
const reg = new RegExp(`/n/${namespace}/instances/(.*)`);
await expect(
page,
"workflow was triggered with our input and user was redirected to the instances page"
).toHaveURL(reg);
const instanceId = page.url().match(reg)?.[1];
if (!instanceId) {
throw new Error("instanceId not found");
}
// check the server state of the input
const res = await getInstanceInput({
urlParams: {
baseUrl: process.env.PLAYWRIGHT_UI_BASE_URL,
instanceId,
namespace,
},
headers,
});
const expectedJson = {
age: 2,
array: ["array item 1", "array item 2"],
firstName: "Marty",
lastName: "McFly",
select: "guest",
file: `data:text/plain;base64,SSBhbSBqdXN0IGEgdGVzdGZpbGUgdGhhdCBjYW4gYmUgdXNlZCB0byB0ZXN0IGFuIHVwbG9hZCBmb3JtIHdpdGhpbiBhIHBsYXl3cmlnaHQgdGVzdA==`,
};
const inputResponseAsJson = JSON.parse(decode(res.data.input));
expect(inputResponseAsJson).toEqual(expectedJson);
});
test("it is possible to provide the input via generated form and resolve form errors", async ({
page,
}) => {
const workflowName = faker.system.commonFileName("yaml");
await createFile({
name: workflowName,
namespace,
type: "workflow",
yaml: jsonSchemaWithRequiredEnum,
});
await page.goto(`/n/${namespace}/explorer/workflow/edit/${workflowName}`);
await page.getByTestId("workflow-editor-btn-run").click();
expect(
await page.getByTestId("run-workflow-dialog"),
"it opens the dialog"
).toBeVisible();
expect(
await page
.getByTestId("run-workflow-form-tab-btn")
.getAttribute("aria-selected"),
"it detects the validate step and makes the form tab active by default"
).toBe("true");
// it generated a form (first and last name are required)
await expect(page.getByLabel("First Name")).toBeVisible();
await expect(page.getByLabel("Last Name")).toBeVisible();
await expect(page.getByRole("combobox", { name: "role" })).toBeVisible();
//click on submit
await page.getByTestId("run-workflow-submit-btn").click();
// last name is required and we just tried to send the form without filling it
await expect(page.getByLabel("First Name")).toBeFocused();
await page.getByLabel("First Name").fill("Marty");
await page.getByTestId("run-workflow-submit-btn").click();
// first name is also required and will now be focused
await expect(page.getByLabel("Last Name")).toBeFocused();
await page.getByLabel("Last Name").fill("McFly");
await page.getByTestId("run-workflow-submit-btn").click();
// shows the error to select option
await expect(
page.getByTestId("jsonschema-form-error"),
"an error should be visible"
).toBeVisible();
await expect(
page.getByTestId("jsonschema-form-error"),
"error message should be \"must have required property 'role'\""
).toContainText("must have required property 'role'");
// interact with the select input
await page.getByRole("combobox", { name: "role" }).click();
await page.getByRole("option", { name: "guest" }).click();
await page.getByTestId("run-workflow-submit-btn").click();
const reg = new RegExp(`/n/${namespace}/instances/(.*)`);
await expect(
page,
"workflow was triggered with our input and user was redirected to the instances page"
).toHaveURL(reg);
const instanceId = page.url().match(reg)?.[1];
if (!instanceId) {
throw new Error("instanceId not found");
}
// check the server state of the input
const res = await getInstanceInput({
urlParams: {
baseUrl: process.env.PLAYWRIGHT_UI_BASE_URL,
instanceId,
namespace,
},
headers,
});
const expectedJson = {
firstName: "Marty",
lastName: "McFly",
select: "guest",
};
const inputResponseAsJson = JSON.parse(decode(res.data.input));
expect(inputResponseAsJson).toEqual(expectedJson);
});
test("it is possible to provide the input via Form Input and see the same data in the tab JSON Input", async ({
page,
}) => {
const workflowName = faker.system.commonFileName("yaml");
await createFile({
name: workflowName,
namespace,
type: "workflow",
yaml: jsonSchemaWithRequiredEnum,
});
await page.goto(`/n/${namespace}/explorer/workflow/edit/${workflowName}`);
await page.getByTestId("workflow-editor-btn-run").click();
expect(
await page.getByTestId("run-workflow-dialog"),
"it opens the dialog"
).toBeVisible();
expect(
await page
.getByTestId("run-workflow-form-tab-btn")
.getAttribute("aria-selected"),
"it detects the validate step and makes the form tab active by default"
).toBe("true");
// it generated a form (first and last name are required)
await expect(page.getByLabel("First Name")).toBeVisible();
await expect(page.getByLabel("Last Name")).toBeVisible();
await expect(page.getByRole("combobox", { name: "role" })).toBeVisible();
await page.getByLabel("First Name").fill("Marty");
await page.getByLabel("Last Name").fill("McFly");
// interact with the select input
await page.getByRole("combobox", { name: "role" }).click();
await page.getByRole("option", { name: "guest" }).click();
// switch to tab json input
await page.getByTestId("run-workflow-json-tab-btn").click();
expect(
await page
.getByTestId("run-workflow-json-tab-btn")
.getAttribute("aria-selected"),
"the json tab is selected"
).toBe("true");
const expectedEditorInput = prettifyJsonString(
JSON.stringify({
firstName: "Marty",
lastName: "McFly",
select: "guest",
})
);
await expect(
page.getByTestId("run-workflow-editor").locator(".lines-content"),
"all entered data is represented in the editor preview"
).toContainText(expectedEditorInput, {
useInnerText: true,
});
// run the workflow from the json tab
await page.getByTestId("run-workflow-submit-btn").click();
const reg = new RegExp(`/n/${namespace}/instances/(.*)`);
await expect(
page,
"workflow was triggered and the user was redirected to the instances page"
).toHaveURL(reg);
await page.getByRole("tab", { name: "Input" }).click();
// turn the input/output panel to full screen
await page.getByTestId("inputOutputPanel").locator("button").nth(1).click();
await expect(
page.locator(".lines-content"),
"all entered data is represented in the editor preview"
).toContainText(expectedEditorInput, {
useInnerText: true,
});
});
test("it is possible to provide the input via JSON Input and see the same data in the tab Form Input", async ({
page,
}) => {
const workflowName = faker.system.commonFileName("yaml");
await createFile({
name: workflowName,
namespace,
type: "workflow",
yaml: jsonSchemaWithRequiredEnum,
});
await page.goto(`/n/${namespace}/explorer/workflow/edit/${workflowName}`);
await page.getByTestId("workflow-editor-btn-run").click();
expect(
await page.getByTestId("run-workflow-dialog"),
"it opens the dialog"
).toBeVisible();
// switch to tab JSON input
await page.getByTestId("run-workflow-json-tab-btn").click();
expect(
await page
.getByTestId("run-workflow-json-tab-btn")
.getAttribute("aria-selected"),
"the json tab is selected"
).toBe("true");
// clear editor, to prevent invalid JSON due to auto completion
await page.getByRole("textbox").fill("");
// give valid JSON data
await page
.getByRole("textbox")
.fill('{"firstName":"Marty","lastName":"McFly","select":"guest"}');
// switch to tab Form input
await page.getByTestId("run-workflow-form-tab-btn").click();
// the generated form is visible
await expect(page.getByLabel("First Name")).toBeVisible();
await expect(page.getByLabel("Last Name")).toBeVisible();
await expect(page.getByRole("combobox", { name: "role" })).toBeVisible();
expect(
await page.getByLabel("First Name"),
"the value for first name was set automatically"
).toHaveValue("Marty");
expect(
await page.getByLabel("Last Name"),
"the value for last name was set automatically"
).toHaveValue("McFly");
expect(
await page.getByRole("combobox").locator("span"),
"the value for role was set automatically"
).toContainText("guest");
await page.getByTestId("run-workflow-submit-btn").click();
const reg = new RegExp(`/n/${namespace}/instances/(.*)`);
await expect(
page,
"workflow was triggered and user was redirected to the instances page"
).toHaveURL(reg);
const expectedEditorInput = prettifyJsonString(
JSON.stringify({
firstName: "Marty",
lastName: "McFly",
select: "guest",
})
);
await page.getByRole("tab", { name: "Input" }).click();
// turn the input/output panel to full screen
await page.getByTestId("inputOutputPanel").locator("button").nth(1).click();
await expect(
page.locator(".lines-content"),
"all entered data is represented in the editor preview"
).toContainText(expectedEditorInput, {
useInnerText: true,
});
});
test("the input is synchronized between tabs, but the data that is currently in the view will be sent", async ({
page,
}) => {
const workflowName = faker.system.commonFileName("yaml");
await createFile({
name: workflowName,
namespace,
type: "workflow",
yaml: jsonSchemaWithRequiredEnum,
});
await page.goto(`/n/${namespace}/explorer/workflow/edit/${workflowName}`);
await page.getByTestId("workflow-editor-btn-run").click();
expect(
await page.getByTestId("run-workflow-dialog"),
"it opens the dialog"
).toBeVisible();
// switch to tab JSON input
await page.getByTestId("run-workflow-json-tab-btn").click();
expect(
await page
.getByTestId("run-workflow-json-tab-btn")
.getAttribute("aria-selected"),
"the json tab is selected"
).toBe("true");
// clear editor, to prevent invalid JSON due to auto completion
await page.getByRole("textbox").fill("");
// give valid JSON data
await page
.getByRole("textbox")
.fill(
'{"firstName":"Marty","lastName":"McFly","select":"guest", "obsoleteData": "random"}'
);
// switch to tab Form input
await page.getByTestId("run-workflow-form-tab-btn").click();
// the generated form is visible
await expect(page.getByLabel("First Name")).toBeVisible();
await expect(page.getByLabel("Last Name")).toBeVisible();
await expect(page.getByRole("combobox", { name: "role" })).toBeVisible();
expect(
await page.getByLabel("First Name"),
"the value for first name was set automatically"
).toHaveValue("Marty");
expect(
await page.getByLabel("Last Name"),
"the value for last name was set automatically"
).toHaveValue("McFly");
expect(
await page.getByRole("combobox").locator("span"),
"the value for role was set automatically"
).toContainText("guest");
// change data again
await page.getByLabel("Last Name").fill("McDonald");
await page.getByTestId("run-workflow-submit-btn").click();
const reg = new RegExp(`/n/${namespace}/instances/(.*)`);
await expect(
page,
"workflow was triggered with our input and user was redirected to the instances page"
).toHaveURL(reg);
await expect(
page.getByRole("tab", { name: "Input" }),
"tab for input is visible"
).toBeVisible();
await page.getByRole("tab", { name: "Input" }).click();
// turn the input/output panel to full screen
await page.getByTestId("inputOutputPanel").locator("button").nth(1).click();
const expectedEditorInput = prettifyJsonString(
JSON.stringify({
firstName: "Marty",
lastName: "McDonald",
select: "guest",
})
);
await expect(
page.locator(".lines-content"),
"all entered data is represented in the editor preview"
).toContainText(expectedEditorInput, {
useInnerText: true,
});
// check if the data from the JSON Input was overwritten when we switched to Form Input and sent the new data
await expect(
page.locator(".lines-content"),
"overwritten data does not exist anymore in the editor preview"
).not.toContainText("obsoleteData", {
useInnerText: true,
});
});
test("switching the window focus will preserve the state of the form", async ({
page,
}) => {
const workflowName = faker.system.commonFileName("yaml");
await createFile({
name: workflowName,
namespace,
type: "workflow",
yaml: jsonSchemaWithRequiredEnum,
});
await page.goto(`/n/${namespace}/explorer/workflow/edit/${workflowName}`);
await page.getByTestId("workflow-editor-btn-run").click();
expect(
await page.getByTestId("run-workflow-dialog"),
"it opens the dialog"
).toBeVisible();
// the generated form is visible
await expect(page.getByLabel("First Name")).toBeVisible();
await expect(page.getByLabel("Last Name")).toBeVisible();
await expect(page.getByRole("combobox", { name: "role" })).toBeVisible();
// fill data
await page.getByLabel("First Name").fill("Marty");
await page.getByLabel("Last Name").fill("Mortensen");
await page.getByRole("combobox", { name: "role" }).click();
await page.getByRole("option", { name: "guest" }).click();
// dispatch visibilitychange event to emulate switching the window focus
page.evaluate(() => {
window.dispatchEvent(new Event("visibilitychange"));
});
// run the workflow
await page.getByTestId("run-workflow-submit-btn").click();
const reg = new RegExp(`/n/${namespace}/instances/(.*)`);
await expect(
page,
"workflow was triggered with our input and user was redirected to the instances page"
).toHaveURL(reg);
await expect(
page.getByRole("tab", { name: "Input" }),
"tab for input is visible"
).toBeVisible();
await page.getByRole("tab", { name: "Input" }).click();
// turn the input/output panel to full screen
await page.getByTestId("inputOutputPanel").locator("button").nth(1).click();
const expectedEditorInput = prettifyJsonString(
JSON.stringify({
firstName: "Marty",
lastName: "Mortensen",
select: "guest",
})
);
// the data from the input fields was sent correctly
await expect(
page.locator(".lines-content"),
"all entered data is represented in the editor preview"
).toContainText(expectedEditorInput, {
useInnerText: true,
});
});