mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-19 18:05:41 -05:00
feat(tests): add E2E on Executions view (#11556)
This commit is contained in:
6
ui/.gitignore
vendored
6
ui/.gitignore
vendored
@@ -1,2 +1,6 @@
|
||||
playwright-report
|
||||
test-results
|
||||
test-results
|
||||
tests/.env
|
||||
tests/data/
|
||||
tests/e2e/.env
|
||||
tests/e2e/data/application-secrets.yml
|
||||
13
ui/tests/e2e/api/base.api.ts
Normal file
13
ui/tests/e2e/api/base.api.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {APIRequestContext} from "playwright/test";
|
||||
import {shared} from "../fixtures/shared";
|
||||
|
||||
export class BaseApi {
|
||||
protected static readonly BASED_PAIR = Buffer.from(`${shared.username}:${shared.password}`).toString("base64");
|
||||
protected static readonly AUTH = `Basic ${BaseApi.BASED_PAIR}`;
|
||||
|
||||
protected readonly apiUrl: string;
|
||||
|
||||
constructor(public readonly request: APIRequestContext, protected readonly baseURL: string | undefined) {
|
||||
this.apiUrl = `${baseURL}/api/v1/main`;
|
||||
}
|
||||
}
|
||||
61
ui/tests/e2e/api/executions.api.ts
Normal file
61
ui/tests/e2e/api/executions.api.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {APIRequestContext} from "playwright/test";
|
||||
import {shared} from "../fixtures/shared";
|
||||
import {BaseApi} from "./base.api";
|
||||
|
||||
export class ExecutionsApi extends BaseApi {
|
||||
private readonly executionIds: string[] = [];
|
||||
|
||||
constructor(public readonly requests: APIRequestContext, public readonly flowId: string, protected readonly baseURL: string | undefined) {
|
||||
super(requests, baseURL);
|
||||
}
|
||||
|
||||
async generateExecutionViaApi(labels: [string, string][] = []) {
|
||||
const formData = new FormData();
|
||||
formData.append("INPUT_A", "test");
|
||||
|
||||
const params = new URLSearchParams();
|
||||
labels.forEach((tuple) => {
|
||||
params.append("labels", `${tuple[0]}:${tuple[1]}`);
|
||||
});
|
||||
|
||||
const response = this.request.post(`${this.apiUrl}/executions/${shared.namespace}/${this.flowId}`, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Authorization": ExecutionsApi.AUTH
|
||||
},
|
||||
params,
|
||||
multipart: formData
|
||||
});
|
||||
|
||||
const status = (await response).status();
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error(`Execution creation failed with HTTP ${status}: ${await (await response).text()}`);
|
||||
}
|
||||
|
||||
const responseJson = await (await response).json();
|
||||
|
||||
this.executionIds.push(responseJson["id"]);
|
||||
}
|
||||
|
||||
async removeExecutionsViaApi() {
|
||||
for (const executionId of this.executionIds) {
|
||||
const params = new URLSearchParams();
|
||||
params.append("deleteLogs", "true");
|
||||
params.append("deleteMetric", "true");
|
||||
params.append("deleteStorage", "true");
|
||||
|
||||
const status = (await this.request.delete(`${this.apiUrl}/executions/${executionId}`, {
|
||||
headers: {
|
||||
"Authorization": ExecutionsApi.AUTH
|
||||
},
|
||||
params
|
||||
})).status();
|
||||
|
||||
if (status !== 204) {
|
||||
throw new Error(`Deletion of execution ${executionId} failed with HTTP ${status}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
65
ui/tests/e2e/api/flows.api.ts
Normal file
65
ui/tests/e2e/api/flows.api.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {APIRequestContext} from "playwright/test";
|
||||
import {BaseApi} from "./base.api";
|
||||
import {shared} from "../fixtures/shared";
|
||||
import {v4 as uuid} from "uuid";
|
||||
import {fileURLToPath} from "url";
|
||||
import {dirname} from "path";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export class FlowsApi extends BaseApi {
|
||||
private readonly flowIds: string[] = [];
|
||||
|
||||
constructor(public readonly requests: APIRequestContext, protected readonly baseURL: string | undefined) {
|
||||
super(requests, baseURL);
|
||||
}
|
||||
|
||||
async generateFlowViaApi(fileName: string, fileFlowId: string) {
|
||||
const flowId = `test-flow-${uuid()}`;
|
||||
|
||||
// Create flow via API
|
||||
const response = this.request.post(`${this.apiUrl}/flows`, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-yaml",
|
||||
"Accept": "application/json",
|
||||
"Authorization": FlowsApi.AUTH
|
||||
},
|
||||
data: this.getFlowYaml(fileName, fileFlowId, flowId)
|
||||
});
|
||||
|
||||
const status = (await response).status();
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error(`Flow creation failed with HTTP ${status}`);
|
||||
}
|
||||
|
||||
this.flowIds.push(flowId);
|
||||
|
||||
return flowId;
|
||||
}
|
||||
|
||||
async removeFlowsViaApi() {
|
||||
for(const flowId of this.flowIds) {
|
||||
const status = (await this.request.delete(`${this.apiUrl}/flows/${shared.namespace}/${flowId}`, {
|
||||
headers: {
|
||||
"Authorization": FlowsApi.AUTH
|
||||
}
|
||||
})).status();
|
||||
|
||||
if (status !== 204) {
|
||||
throw new Error(`Deletion of flow ${flowId} failed with HTTP ${status}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected getFlowYaml(fileName: string, fileFlowId: string, desiredFlowId: string): string {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const flowYaml = fs.readFileSync(
|
||||
path.resolve(__dirname, `../fixtures/flows/${fileName}`),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
return flowYaml.replace(fileFlowId, desiredFlowId);
|
||||
}
|
||||
}
|
||||
135
ui/tests/e2e/executions/execution-bulk-actions.spec.ts
Normal file
135
ui/tests/e2e/executions/execution-bulk-actions.spec.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import {test, expect} from "../fixtures/executions.fixture";
|
||||
import {ExecutionState, Pagination} from "../pages/base.page";
|
||||
|
||||
test.describe("Executions' view Bulk Actions", () => {
|
||||
// Use specific flow to create executions
|
||||
test.use({flow: {fileName: "hello.yaml", flowId: "my-hello-flow-1"}});
|
||||
test("Labels changed only on a filtered set of executions when using Select All", async ({executionsPage, executionsApi, page}) => {
|
||||
test.slow(); // creating many executions
|
||||
expect(page.getByRole("heading", {name: "Executions"})).toBeVisible();
|
||||
|
||||
await test.step("Generate 26 executions with the 'foo:bar' label and a single 'a:b' one", async () => {
|
||||
for (let i = 0; i < 26; i++) {
|
||||
await executionsApi.generateExecutionViaApi([["foo", "bar"]]);
|
||||
}
|
||||
await executionsApi.generateExecutionViaApi([["a", "b"]]);
|
||||
});
|
||||
|
||||
await test.step("Filter just the executions featuring the 'foo:bar' label", async () => {
|
||||
await executionsPage.setPaginationTo(Pagination.ITEMS_25);
|
||||
await executionsPage.setFilterByFlowId(executionsApi.flowId);
|
||||
await executionsPage.setFilterByLabel("foo", "bar");
|
||||
|
||||
expect(await executionsPage.getCountOfDisplayedExecutions()).toEqual(25);
|
||||
expect(await executionsPage.getTotalExecutionsCount()).toEqual(26);
|
||||
});
|
||||
|
||||
await test.step("Set label to 'foo:baz' using Select All on filtered 'foo:bar' executions", async () => {
|
||||
await page.waitForTimeout(1500); // somehow the execution selection de-selects itself due a data load
|
||||
await executionsPage.selectExecutionRowByNumber();
|
||||
await executionsPage.clickOnSelectAll();
|
||||
await executionsPage.clickOnSetLabels();
|
||||
await executionsPage.setLabelOnSelectedExecutions();
|
||||
|
||||
expect(await executionsPage.getCountOfDisplayedExecutions()).toEqual(0);
|
||||
});
|
||||
|
||||
await test.step("Switch filter to label 'a:b' which should not be affected by the label change", async () => {
|
||||
await executionsPage.removeFilterByLabelKey("foo");
|
||||
await executionsPage.setFilterByLabel("a", "b");
|
||||
|
||||
expect(await executionsPage.getCountOfDisplayedExecutions()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.use({flow: {fileName: "failure-then-success.yaml", flowId: "failure-then-success"}});
|
||||
test("Restart only on a filtered set of executions when using Select All", async ({executionsPage, executionsApi, page}) => {
|
||||
test.slow(); // creating and resuming many executions
|
||||
expect(page.getByRole("heading", {name: "Executions"})).toBeVisible();
|
||||
|
||||
await test.step("Generate 26 executions with the 'foo:bar' label and a single 'a:b' one", async () => {
|
||||
for (let i = 0; i < 26; i++) {
|
||||
await executionsApi.generateExecutionViaApi([["foo", "bar"]]);
|
||||
}
|
||||
await executionsApi.generateExecutionViaApi([["a", "b"]]);
|
||||
});
|
||||
|
||||
await test.step("Filter just 'FAILED' executions featuring the 'foo:bar' label", async () => {
|
||||
await executionsPage.setPaginationTo(Pagination.ITEMS_25);
|
||||
await executionsPage.setFilterByFlowId(executionsApi.flowId);
|
||||
await executionsPage.setFilterByLabel("foo", "bar");
|
||||
await executionsPage.setFilterByState(ExecutionState.FAILED);
|
||||
|
||||
expect(await executionsPage.getCountOfDisplayedExecutions()).toEqual(25);
|
||||
expect(await executionsPage.getTotalExecutionsCount()).toEqual(26);
|
||||
});
|
||||
|
||||
await test.step("Call Restart using Select All on filtered 'FAILED' & 'foo:bar' executions", async () => {
|
||||
await page.waitForTimeout(1500); // somehow the execution selection de-selects itself due a data load
|
||||
await executionsPage.selectExecutionRowByNumber();
|
||||
await executionsPage.clickOnSelectAll();
|
||||
await executionsPage.clickOnRestart();
|
||||
});
|
||||
|
||||
await test.step("Show all 26 now successfully finished 'foo:bar' executions on a single page", async () => {
|
||||
await page.waitForTimeout(2000); // ensure restarted executions finished
|
||||
await executionsPage.setFilterByState(ExecutionState.SUCCESS);
|
||||
await executionsPage.setPaginationTo(Pagination.ITEMS_50);
|
||||
|
||||
expect(await executionsPage.getCountOfDisplayedExecutions()).toEqual(26);
|
||||
});
|
||||
|
||||
await test.step("Switch filter to label 'a:b' which should not be affected by the Restart action", async () => {
|
||||
await executionsPage.removeFilterByLabelKey("foo");
|
||||
await executionsPage.setFilterByLabel("a", "b");
|
||||
await executionsPage.setFilterByState(ExecutionState.FAILED);
|
||||
|
||||
expect(await executionsPage.getCountOfDisplayedExecutions()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.use({flow: {fileName: "failure-then-success.yaml", flowId: "failure-then-success"}});
|
||||
test("Replay only on a filtered set of executions when using Select All", async ({executionsPage, executionsApi, page}) => {
|
||||
test.slow(); // creating and resuming many executions
|
||||
expect(page.getByRole("heading", {name: "Executions"})).toBeVisible();
|
||||
|
||||
await test.step("Generate 26 executions with the 'foo:bar' label and a single 'a:b' one", async () => {
|
||||
for (let i = 0; i < 26; i++) {
|
||||
await executionsApi.generateExecutionViaApi([["foo", "bar"]]);
|
||||
}
|
||||
await executionsApi.generateExecutionViaApi([["a", "b"]]);
|
||||
});
|
||||
|
||||
await test.step("Filter just 'FAILED' executions featuring the 'foo:bar' label", async () => {
|
||||
await executionsPage.setPaginationTo(Pagination.ITEMS_25);
|
||||
await executionsPage.setFilterByFlowId(executionsApi.flowId);
|
||||
await executionsPage.setFilterByLabel("foo", "bar");
|
||||
await executionsPage.setFilterByState(ExecutionState.FAILED);
|
||||
|
||||
expect(await executionsPage.getCountOfDisplayedExecutions()).toEqual(25);
|
||||
expect(await executionsPage.getTotalExecutionsCount()).toEqual(26);
|
||||
});
|
||||
|
||||
await test.step("Call Replay using Select All on filtered 'FAILED' & 'foo:bar' executions", async () => {
|
||||
await page.waitForTimeout(1500); // somehow the execution selection de-selects itself due a data load
|
||||
await executionsPage.selectExecutionRowByNumber();
|
||||
await executionsPage.clickOnSelectAll();
|
||||
await executionsPage.clickOnReplay();
|
||||
});
|
||||
|
||||
await test.step("Show 26 original and 26 replayed 'foo:bar' executions on a single page", async () => {
|
||||
await page.waitForTimeout(2000); // ensure replayed executions finished
|
||||
await executionsPage.setPaginationTo(Pagination.ITEMS_100);
|
||||
|
||||
expect(await executionsPage.getCountOfDisplayedExecutions()).toEqual(26 * 2);
|
||||
});
|
||||
|
||||
await test.step("Switch filter to label 'a:b' which should not be affected by the Restart action", async () => {
|
||||
await executionsPage.removeFilterByLabelKey("foo");
|
||||
await executionsPage.setFilterByLabel("a", "b");
|
||||
await executionsPage.setFilterByState(ExecutionState.FAILED);
|
||||
|
||||
expect(await executionsPage.getCountOfDisplayedExecutions()).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
38
ui/tests/e2e/fixtures/executions.fixture.ts
Normal file
38
ui/tests/e2e/fixtures/executions.fixture.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import {test as base} from "@playwright/test";
|
||||
import {ExecutionsPage} from "../pages/executions.page";
|
||||
import {ExecutionsApi} from "../api/executions.api";
|
||||
import {FlowsApi} from "../api/flows.api";
|
||||
|
||||
type ExecutionsFixtures = {
|
||||
executionsApi: ExecutionsApi,
|
||||
executionsPage: ExecutionsPage,
|
||||
flow: {fileName: string, flowId: string}
|
||||
};
|
||||
|
||||
export const test = base.extend<ExecutionsFixtures>({
|
||||
// define the default `flow` option
|
||||
flow: [{fileName: "hello.yaml", flowId: "my-hello-flow-1"}, {option: true}],
|
||||
executionsApi: async ({page, request, baseURL, flow}, use) => {
|
||||
// Prepare data
|
||||
const flowsApi = new FlowsApi(request, baseURL);
|
||||
const executionsPage = new ExecutionsPage(page);
|
||||
const executionsApi = new ExecutionsApi(request, await flowsApi.generateFlowViaApi(flow.fileName, flow.flowId), baseURL);
|
||||
await executionsApi.generateExecutionViaApi();
|
||||
|
||||
// Navigate
|
||||
await executionsPage.goto();
|
||||
|
||||
// Do the work
|
||||
await use(executionsApi);
|
||||
|
||||
// Clean up
|
||||
await executionsApi.removeExecutionsViaApi();
|
||||
},
|
||||
executionsPage: async ({page}, use) => {
|
||||
const executionsPage = new ExecutionsPage(page);
|
||||
|
||||
await use(executionsPage);
|
||||
}
|
||||
});
|
||||
|
||||
export {expect} from "@playwright/test";
|
||||
7
ui/tests/e2e/fixtures/flows/failure-then-success.yaml
Normal file
7
ui/tests/e2e/fixtures/flows/failure-then-success.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
id: failure-then-success
|
||||
namespace: company.team
|
||||
|
||||
tasks:
|
||||
- id: fail-and-recover
|
||||
type: io.kestra.plugin.core.execution.Fail
|
||||
condition: "{{ taskrun.attemptsCount < 1}}"
|
||||
8
ui/tests/e2e/fixtures/shared.ts
Normal file
8
ui/tests/e2e/fixtures/shared.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* TODO: use ENV instead
|
||||
*/
|
||||
export const shared = {
|
||||
namespace: "company.team",
|
||||
username: "user@kestra.io",
|
||||
password: "DemoDemo1"
|
||||
};
|
||||
50
ui/tests/e2e/pages/base.page.ts
Normal file
50
ui/tests/e2e/pages/base.page.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type {Page} from "@playwright/test";
|
||||
import {expect} from "@playwright/test";
|
||||
import {shared} from "../fixtures/shared";
|
||||
|
||||
export class BasePage {
|
||||
constructor(public readonly page: Page) { }
|
||||
|
||||
async login() {
|
||||
await this.page.goto("/ui");
|
||||
await this.page.getByRole("textbox", {name: "Email"}).fill(shared.username);
|
||||
await this.page.getByRole("textbox", {name: "Password"}).fill(shared.password);
|
||||
await this.page.getByRole("button", {name: "Login"}).click();
|
||||
|
||||
await expect(this.page.getByRole("heading", {name: "Overview"})).toBeVisible();
|
||||
}
|
||||
|
||||
async addQueryParam(page: Page, key: string, value: string) {
|
||||
// Get the current URL
|
||||
const url = new URL(page.url());
|
||||
|
||||
// Change query params
|
||||
url.searchParams.set(key, value);
|
||||
|
||||
// Navigate to the new URL
|
||||
await page.goto(url.toString());
|
||||
}
|
||||
|
||||
async removeQueryParam(page: Page, key: string) {
|
||||
// Get the current URL
|
||||
const url = new URL(page.url());
|
||||
|
||||
// Change query params
|
||||
url.searchParams.delete(key);
|
||||
|
||||
// Navigate to the new URL
|
||||
await page.goto(url.toString());
|
||||
}
|
||||
}
|
||||
|
||||
export enum ExecutionState {
|
||||
FAILED = "FAILED",
|
||||
SUCCESS = "SUCCESS"
|
||||
}
|
||||
|
||||
export enum Pagination {
|
||||
ITEMS_10 = 10,
|
||||
ITEMS_25 = 25,
|
||||
ITEMS_50 = 50,
|
||||
ITEMS_100 = 100
|
||||
}
|
||||
118
ui/tests/e2e/pages/executions.page.ts
Normal file
118
ui/tests/e2e/pages/executions.page.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import type {Page} from "@playwright/test";
|
||||
import {expect} from "@playwright/test";
|
||||
|
||||
import {BasePage, ExecutionState, Pagination} from "./base.page";
|
||||
|
||||
export class ExecutionsPage extends BasePage {
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.login();
|
||||
await this.page.goto("/ui/main/executions");
|
||||
|
||||
await expect(this.page.getByRole("heading", {name: "Executions"})).toBeVisible();
|
||||
}
|
||||
|
||||
async setFilterByFlowId(flowId: string) {
|
||||
const param = "filters[flowId][EQUALS]";
|
||||
await this.removeQueryParam(this.page, param);
|
||||
await this.addQueryParam(this.page, param, flowId);
|
||||
}
|
||||
|
||||
async setFilterByLabel(key: string, value: string) {
|
||||
const param = `filters[labels][EQUALS][${key}]`;
|
||||
await this.removeQueryParam(this.page, param);
|
||||
await this.addQueryParam(this.page, param, value);
|
||||
}
|
||||
|
||||
async setFilterByState(state: ExecutionState) {
|
||||
const param = "filters[state][EQUALS]";
|
||||
await this.removeQueryParam(this.page, param);
|
||||
await this.addQueryParam(this.page, param, state);
|
||||
}
|
||||
|
||||
async removeFilterByLabelKey(key: string) {
|
||||
await this.removeQueryParam(this.page, `filters[labels][EQUALS][${key}]`);
|
||||
}
|
||||
|
||||
async getCountOfDisplayedExecutions() {
|
||||
const rows = this.page.getByRole("row");
|
||||
return await rows.count() - 1;
|
||||
}
|
||||
|
||||
async getTotalExecutionsCount() {
|
||||
const content = await this.page.getByText(/Total:/).first().textContent();
|
||||
if (!content) {
|
||||
throw new Error("Totals not found");
|
||||
}
|
||||
return Number.parseInt(content.split(":")[1].trim());
|
||||
}
|
||||
|
||||
async selectExecutionRowByNumber(rowNumber: number = 1) {
|
||||
if (rowNumber < 0) {
|
||||
throw new Error("Negative row number is not allowed");
|
||||
}
|
||||
const checkbox = this.page.getByRole("row").nth(rowNumber).locator("label.el-checkbox");
|
||||
|
||||
await checkbox.waitFor({state: "visible"});
|
||||
await checkbox.click();
|
||||
|
||||
await expect(checkbox).toContainClass("is-checked");
|
||||
}
|
||||
|
||||
async clickOnSelectAll() {
|
||||
await this.page.getByRole("button", {name: "Select All"}).click();
|
||||
}
|
||||
|
||||
async clickOnSetLabels() {
|
||||
await this.page.locator(".bulk-select").locator(".el-button-group").locator(".el-dropdown").click();
|
||||
await this.page.getByRole("menuitem", {name: "Set labels"}).click();
|
||||
}
|
||||
|
||||
async clickOnResume() {
|
||||
await this.page.locator(".bulk-select").locator(".el-button-group").locator(".el-dropdown").click();
|
||||
await this.page.getByRole("menuitem", {name: "Resume"}).click();
|
||||
// Confirm
|
||||
await this.page.getByRole("button", {name: "OK"}).click();
|
||||
}
|
||||
|
||||
async clickOnRestart() {
|
||||
await this.page.getByRole("button", {name: "Restart"}).click();
|
||||
// Confirm
|
||||
await this.page.getByRole("button", {name: "OK"}).click();
|
||||
}
|
||||
|
||||
async clickOnReplay() {
|
||||
await this.page.getByRole("button", {name: "Replay"}).click();
|
||||
// Confirm
|
||||
await this.page.getByRole("button", {name: "OK"}).click();
|
||||
}
|
||||
|
||||
async setLabelOnSelectedExecutions() {
|
||||
await this.page.getByRole("textbox", {name: "Key"}).fill("foo");
|
||||
await this.page.getByRole("textbox", {name: "Value"}).fill("baz");
|
||||
await this.page.getByRole("button", {name: "OK"}).click();
|
||||
// Confirm
|
||||
await this.page.getByRole("button", {name: "OK"}).click();
|
||||
}
|
||||
|
||||
async setPaginationTo(size: Pagination) {
|
||||
// The Element-Plus dropdown is not a `select` - click on text
|
||||
await this.page.locator(".pagination .el-select").click();
|
||||
|
||||
// Wait for the select dropdown to show
|
||||
const dropdowns = this.page.locator(".el-select-dropdown");
|
||||
const visibleDropdown = dropdowns.filter({has: this.page.locator(":visible")}).last();
|
||||
|
||||
// Wait for the visible dropdown to actually appear
|
||||
await visibleDropdown.waitFor({state: "visible", timeout: 500});
|
||||
|
||||
// Find and click the matching option
|
||||
const option = visibleDropdown.locator(".el-select-dropdown__item", {hasText: `${size} per page`});
|
||||
await option.waitFor({state: "visible", timeout: 500});
|
||||
await option.click();
|
||||
}
|
||||
}
|
||||
17
ui/tests/e2e/pages/flows.page.ts
Normal file
17
ui/tests/e2e/pages/flows.page.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type {Page} from "@playwright/test";
|
||||
import {expect} from "@playwright/test";
|
||||
|
||||
import {BasePage} from "./base.page";
|
||||
|
||||
export class FlowsPage extends BasePage {
|
||||
constructor(public readonly page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.login();
|
||||
await this.page.goto("/ui/main/flows");
|
||||
|
||||
await expect(this.page.getByRole("heading", {name: "Flows"})).toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ const config: PlaywrightTestConfig = {
|
||||
/* Shared settings for all the projects below. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto("/")`. */
|
||||
baseURL: "http://localhost:9011/ui",
|
||||
baseURL: "http://localhost:9011",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "retain-on-failure",
|
||||
|
||||
Reference in New Issue
Block a user