ui/e2e/instances/list/index.spec.ts
import { createNamespace, deleteNamespace } from "../../utils/namespace";
import { expect, test } from "@playwright/test";
import {
parentWorkflow as parentWorkflowContent,
simpleWorkflow as simpleWorkflowContent,
workflowThatFails as workflowThatFailsContent,
workflowWithDelay as workflowWithDelayContent,
} from "../utils/workflows";
import { createFile } from "e2e/utils/files";
import { createInstance } from "../utils";
import { faker } from "@faker-js/faker";
import { getInstances } from "~/api/instances/query/get";
import { headers } from "e2e/utils/testutils";
import moment from "moment";
type Instance = Awaited<ReturnType<typeof createInstance>>;
let namespace = "";
const simpleWorkflowName = faker.system.commonFileName("yaml");
const longRunningWorkflowName = faker.system.commonFileName("yaml");
const failingWorkflowName = faker.system.commonFileName("yaml");
test.beforeEach(async () => {
namespace = await createNamespace();
// place some workflows in the namespace that we can use to create instances
await createFile({
name: simpleWorkflowName,
namespace,
type: "workflow",
yaml: simpleWorkflowContent,
});
await createFile({
name: failingWorkflowName,
namespace,
type: "workflow",
yaml: workflowThatFailsContent,
});
await createFile({
name: longRunningWorkflowName,
namespace,
type: "workflow",
yaml: workflowWithDelayContent,
});
});
test.afterEach(async () => {
await deleteNamespace(namespace);
namespace = "";
});
test("it displays a note, when there are no instances yet.", async ({
page,
}) => {
await page.goto(`/n/${namespace}/instances/`);
await expect(
page.getByTestId("no-result"),
"no result message should be visible"
).toBeVisible();
await expect(
page.getByTestId("instance-list-pagination"),
"there is no pagination when there is no result"
).not.toBeVisible();
});
test("it renders the instance item correctly for failed and success status", async ({
page,
}) => {
const instances = [
await createInstance({ namespace, path: simpleWorkflowName }),
await createInstance({ namespace, path: failingWorkflowName }),
];
const checkInstanceRender = async (instance: Instance) => {
const instancesList = await getInstances({
urlParams: {
baseUrl: process.env.PLAYWRIGHT_UI_BASE_URL,
namespace,
limit: 10,
offset: 0,
},
headers,
});
const instanceDetail = instancesList.data.find(
(x) => x.id === instance.data.id
);
if (!instanceDetail) {
throw new Error("instance not found");
}
const workflowName = instanceDetail?.path.split(":")[0];
if (!workflowName) throw new Error("workflowName is not defined");
const instanceItemRow = page.getByTestId(
`instance-row-${instance.data.id}`
);
await expect(
instanceItemRow.getByTestId(`instance-column-name`),
"the workflow name should be visible"
).toContainText(workflowName);
const instanceItemIdColumn =
instanceItemRow.getByTestId("instance-column-id");
await expect(
instanceItemIdColumn.getByTestId(`tooltip-copy-trigger`),
"id badge shows the first 8 digits of the id"
).toContainText(instance.data.id.slice(0, 8));
await instanceItemIdColumn.getByTestId(`tooltip-copy-trigger`).hover();
await expect(
instanceItemIdColumn.getByTestId("tooltip-copy-content"),
"on hover, a tooltip reveals full id"
).toContainText(instance.data.id);
await page
.getByRole("heading", { name: "Recently executed instances" })
.click(); // click on header to close all tooltips opened
await expect(
instanceItemRow.getByTestId("instance-column-invoker"),
'invoker column shows "api"'
).toContainText("api");
await expect(
instanceItemRow.getByTestId("instance-column-state"),
"the status column should should be same status as the api response"
).toContainText(instanceDetail.status.toString());
if (instanceDetail?.status === "failed") {
await instanceItemRow
.getByTestId("instance-column-state")
.getByTestId("tooltip-copy-trigger")
.hover();
await expect(
instanceItemRow
.getByTestId("instance-column-state")
.getByTestId("tooltip-copy-content"),
"on hover, a tooltip reveals the error message"
).toContainText("this is my error message");
}
await page
.getByRole("heading", { name: "Recently executed instances" })
.click(); // click on header to close all tooltips opened
await expect(
instanceItemRow.getByTestId("instance-column-created-time"),
`the "started at" column should display a relative time of the createdAt api response`
).toContainText(moment(instanceDetail.createdAt).fromNow(true));
await instanceItemRow
.getByTestId("instance-column-created-time")
.getByTestId("tooltip-trigger")
.hover(); // is force: true needed?
await expect(
instanceItemRow
.getByTestId("instance-column-created-time")
.getByTestId("tooltip-content"),
"on hover, the absolute time should appear"
).toContainText(instanceDetail.createdAt);
await page
.getByRole("heading", { name: "Recently executed instances" })
.click(); // click on header to close all tooltips opened
await expect(
instanceItemRow.getByTestId("instance-column-ended-time"),
`the "endedAt" column should display a relative time of the endedAt api response`
).toContainText(moment(instanceDetail.endedAt).fromNow(true));
await instanceItemRow
.getByTestId("instance-column-ended-time")
.getByTestId("tooltip-trigger")
.hover();
await expect(
instanceItemRow
.getByTestId("instance-column-ended-time")
.getByTestId("tooltip-content"),
"on hover, the absolute time should appear"
).toContainText(instanceDetail.endedAt ?? "no endedAt");
await instanceItemRow.click();
await expect(
page,
"on click row, page should navigate to the instance detail page"
).toHaveURL(`/n/${namespace}/instances/${instance.data.id}`);
await page.goBack();
};
await page.goto(`/n/${namespace}/instances/`);
for (let i = 0; i < instances.length; i++) {
const instance = instances[i];
if (!instance) throw new Error("instance is not created properly");
await checkInstanceRender(instance);
}
await expect(
page.getByTestId("instance-list-pagination"),
"no pagination is visible when there is only one page"
).not.toBeVisible();
});
test("it will treat the status and finish date of pending instances accordingly", async ({
page,
}) => {
await createInstance({ namespace, path: longRunningWorkflowName });
await page.goto(`/n/${namespace}/instances/`);
await expect(
page.getByTestId("instance-column-state"),
"the status column should show the pending status"
).toContainText("pending");
await expect(
page.getByTestId("instance-column-ended-time"),
`the "endedAt" column should display "still running"`
).toContainText("still running");
await expect(
page.getByTestId("instance-column-state"),
"the status column should update to complete when the instance is finished"
).toContainText("complete");
});
test("it provides a proper pagination", async ({ page }) => {
const totalCount = 35;
const pageSize = 15;
const parentWorkflow = faker.system.commonFileName("yaml");
const yaml = parentWorkflowContent({
childPath: `/${simpleWorkflowName}`,
children: totalCount - 1,
});
await createFile({
name: parentWorkflow,
namespace,
type: "workflow",
yaml,
});
await createInstance({ namespace, path: parentWorkflow }),
/**
* child workflows are spawned asynchronously in the backend and the page
* does not refresh, so we need to wait until they are initialized before
* visiting the page.
*/
await page.waitForTimeout(500);
await page.goto(`/n/${namespace}/instances/`, { waitUntil: "networkidle" });
await expect(
page.getByTestId("pagination-wrapper"),
"there should be pagination component"
).toBeVisible();
const btnPrev = page.getByTestId("pagination-btn-left");
const btnNext = page.getByTestId("pagination-btn-right");
const page1Btn = page.getByTestId(`pagination-btn-page-1`);
// page number starts from 1
await expect(
page1Btn,
"active button with the page number should have an aria-current attribute with a value of page"
).toHaveAttribute("aria-current", "page");
await expect(
btnPrev,
"prev button should be disabled at page 1"
).toBeDisabled();
await expect(
btnNext,
"next button should be enabled at page 1"
).toBeEnabled();
// go to page 2 by clicking nextButton
await btnNext.click();
await expect(
btnPrev,
"prev button should be enabled at page 2"
).toBeEnabled();
await expect(
btnNext,
"next button should be enabled at page 2"
).toBeEnabled();
// go to page 3 by clicking number 3
const btnNumber3 = page.getByTestId(`pagination-btn-page-3`);
await btnNumber3.click();
// check with api response
const instancesListPage3 = await getInstances({
urlParams: {
baseUrl: process.env.PLAYWRIGHT_UI_BASE_URL,
namespace,
limit: pageSize,
offset: 2 * pageSize,
},
headers,
});
const firstInstance = instancesListPage3.data[0];
if (!firstInstance) throw new Error("there should be at least one instance");
const instanceItemRow = page.getByTestId(`instance-row-${firstInstance.id}`);
await expect(
instanceItemRow.getByTestId("instance-column-id"),
"the first row on page 3 should should be same as the api response"
).toContainText(firstInstance.id.slice(0, 8));
});
test("It will display child instances as well", async ({ page }) => {
const parentWorkflow = faker.system.commonFileName("yaml");
await createFile({
name: parentWorkflow,
namespace,
type: "workflow",
yaml: parentWorkflowContent({
childPath: `/${simpleWorkflowName}`,
children: 1,
}),
});
const parentInstance = await createInstance({
namespace,
path: parentWorkflow,
});
await page.goto(`/n/${namespace}/instances/`, { waitUntil: "networkidle" });
const instancesList = await getInstances({
urlParams: {
baseUrl: process.env.PLAYWRIGHT_UI_BASE_URL,
namespace,
limit: 15,
offset: 0,
},
headers,
});
const childInstanceDetail = instancesList.data.find(
(x) => x.id !== parentInstance.data.id
);
if (!childInstanceDetail)
throw new Error("there should be at least one child instance");
const instanceItemRow = page.getByTestId(
`instance-row-${childInstanceDetail.id}`
);
await expect(
instanceItemRow.getByTestId("instance-column-invoker"),
`invoker is "instance"`
).toContainText("instance");
await instanceItemRow
.getByTestId("instance-column-invoker")
.getByTestId("tooltip-copy-trigger")
.hover();
const expectedInvokerId = childInstanceDetail.invoker.split(":")[1] as string;
await expect(
instanceItemRow
.getByTestId("instance-column-invoker")
.getByTestId("tooltip-copy-content")
).toContainText(expectedInvokerId);
});