Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
770 changes: 385 additions & 385 deletions e2e/fixtures/connectionsTestCases.json

Large diffs are not rendered by default.

32 changes: 23 additions & 9 deletions e2e/pages/dashboard.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, type Locator, type Page } from "@playwright/test";
import randomatic from "randomatic";

import { randomName } from "../utils/randomName";
import { waitForLoadingOverlayGone } from "../utils/waitForLoadingOverlayToDisappear";
import { waitForMonacoEditorToLoad } from "../utils/waitForMonacoEditor";

Expand Down Expand Up @@ -29,30 +29,44 @@ export class DashboardPage {
return this.page.getByText(text);
}

async createProjectFromMenu(): Promise<string> {
async createProjectFromMenu(fixedName?: string): Promise<string> {
await waitForLoadingOverlayGone(this.page);
await this.page.goto("/");
await this.page.goto("/?e2e=true");

const projectName = fixedName ?? randomName();

if (fixedName) {
const existingProject = this.page.locator(`[data-testid="project-row-${fixedName}"]`);
if (await existingProject.isVisible({ timeout: 1000 }).catch(() => false)) {
await existingProject.locator('button[aria-label="Delete project"]').click();
await this.page.getByRole("button", { name: "Ok", exact: true }).click();
await this.page.waitForTimeout(500);
}
}

await this.createButton.hover();
await this.createButton.click();
await this.page.getByRole("button", { name: "New Project From Scratch" }).hover();
await this.page.getByRole("button", { name: "New Project From Scratch" }).click();
const projectName = randomatic("Aa", 8);
await this.page.getByPlaceholder("Enter project name").fill(projectName);
await this.page.getByRole("button", { name: "Create", exact: true }).click();
await expect(this.page.locator('button[aria-label="Open program.py"]')).toBeVisible();
await this.page.getByRole("button", { name: "Open program.py" }).click();

const programPyButton = this.page.locator('button[aria-label="Open program.py"]');
await programPyButton.waitFor({ state: "visible", timeout: 3000 });
await programPyButton.waitFor({ state: "attached", timeout: 1000 });
await programPyButton.click({ timeout: 3000 });

await expect(this.page.getByRole("tab", { name: "program.py Close file tab" })).toBeVisible();

await waitForMonacoEditorToLoad(this.page, 6000);

await expect(this.page.getByRole("heading", { name: "Configuration" })).toBeVisible({ timeout: 1200 });
await expect(this.page.getByRole("heading", { name: "Configuration" })).toBeVisible({ timeout: 1500 });

return projectName;
}

async createProjectFromTemplate(projectName: string) {
await this.page.goto("/welcome");
await this.page.goto("/welcome?e2e=true");
await this.page.getByRole("button", { name: "Start from Template" }).hover();
await this.page.getByRole("button", { name: "Start from Template" }).click();

Expand All @@ -64,6 +78,6 @@ export class DashboardPage {
await this.page.getByPlaceholder("Enter project name").fill(projectName);
await this.page.waitForTimeout(500);
await this.page.getByRole("button", { name: "Create", exact: true }).click();
await expect(this.page.getByRole("heading", { name: "Configuration" })).toBeVisible({ timeout: 1200 });
await expect(this.page.getByRole("heading", { name: "Configuration" })).toBeVisible({ timeout: 5000 });
}
}
59 changes: 30 additions & 29 deletions e2e/pages/project.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect } from "@playwright/test";
import type { Page } from "@playwright/test";

import { waitForToast } from "../utils";
import { waitForToast, waitForToastToBeRemoved } from "../utils";

export class ProjectPage {
private readonly page: Page;
Expand All @@ -10,42 +10,43 @@ export class ProjectPage {
this.page = page;
}

async deleteProject(projectName: string) {
await this.page.locator('button[aria-label="Project additional actions"]').hover();
await this.page.locator('button[aria-label="Delete project"]').click();
await this.page.locator('button[aria-label="Ok"]').click();
async deleteProject(projectName: string, withActiveDeployment: boolean = false) {
if (!projectName?.trim()?.length) {
throw new Error("Project name is required to delete a project");
}

let successToast;
try {
successToast = await waitForToast(
this.page,
`Close "Success Project deletion completed successfully" toast`
);
if (await successToast.isVisible()) {
await successToast
.locator("button[aria-label=\"Close 'Project deletion completed successfully' toast\"]")
.click();
} else {
// eslint-disable-next-line no-console
console.warn("Success toast was found but is not visible. Continuing test without closing toast.");
}
} catch (error) {
// eslint-disable-next-line no-console
console.warn(
`Success toast not found: ${error instanceof Error ? error.message : String(error)}. Continuing test without closing toast.`
);
if ((await this.page.getByRole("button", { name: "Edit project title" }).textContent()) !== projectName) {
throw new Error("Project name is not the same as the one in the page");
}

const additionalActionsButton = this.page.locator('button[aria-label="Project additional actions"]');
await additionalActionsButton.waitFor({ state: "visible", timeout: 3000 });
await additionalActionsButton.waitFor({ state: "attached", timeout: 1000 });
await additionalActionsButton.hover();

const deleteProjectButton = this.page.locator('button[aria-label="Delete project"]');
await deleteProjectButton.waitFor({ state: "visible", timeout: 3000 });
await deleteProjectButton.click();
if (withActiveDeployment) {
await this.page.locator('button[aria-label="Delete"]').click();
} else {
await this.page.locator('button[aria-label="Ok"]').click();
}
await waitForToastToBeRemoved(this.page, "Project deletion completed successfully");

await this.page.mouse.move(0, 0);
try {
await Promise.race([
this.page.waitForURL("/welcome", { waitUntil: "domcontentloaded" }),
this.page.waitForURL("/", { waitUntil: "domcontentloaded" }),
]);
} catch {
throw new Error('Neither "/welcome" nor "/" URL was reached after project deletion');
}

const loaders = this.page.locator(".loader-cycle-disks").all();
const loadersArray = await loaders;
await Promise.all(loadersArray.map((loader) => loader.waitFor({ state: "detached" })));

if (successToast) {
await expect(successToast).not.toBeVisible({ timeout: 2000 });
}

const deletedProjectNameCell = this.page.getByRole("cell", { name: projectName });

await expect(deletedProjectNameCell).toHaveCount(0);
Expand Down
4 changes: 2 additions & 2 deletions e2e/pages/webhookSession.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { expect, type APIRequestContext, type Page } from "@playwright/test";
import randomatic from "randomatic";

import { DashboardPage } from "./dashboard";
import { createNetworkListeners, logNetworkDiagnostics, waitForToast, type NetworkCapture } from "../utils";
import { randomName } from "../utils/randomName";
import { waitForLoadingOverlayGone } from "../utils/waitForLoadingOverlayToDisappear";

export class WebhookSessionPage {
Expand All @@ -15,7 +15,7 @@ export class WebhookSessionPage {
this.page = page;
this.request = request;
this.dashboardPage = new DashboardPage(page);
this.projectName = `test_${randomatic("Aa", 4)}`;
this.projectName = `test_${randomName()}`;
}

async waitForFirstCompletedSession(timeoutMs = 180000) {
Expand Down
122 changes: 77 additions & 45 deletions e2e/project/connections/buttonPresence.visual.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-console */
import { expect, test } from "../../fixtures";
import connectionTestCasesData from "../../fixtures/connectionsTestCases.json" assert { type: "json" };
import { ProjectPage } from "../../pages/project";

type ConnectionTestCategory = "single-type" | "multi-type";
interface ConnectionTestCase {
Expand All @@ -14,73 +14,106 @@ interface ConnectionTestCase {

const testCases = connectionTestCasesData as ConnectionTestCase[];

test.describe.skip("Connection Form Button Presence - Generated", () => {
const fixedProjectName = "visual_test_connections";

test.describe("Connection Form Button Presence - Generated", () => {
let projectId: string;

test.beforeAll(async ({ browser }) => {
const stats = {
total: testCases.length,
singleType: testCases.filter((tc) => tc.category === "single-type").length,
multiType: testCases.filter((tc) => tc.category === "multi-type").length,
};

console.log("\n📊 Test Coverage Statistics:");
console.log(` Total test cases: ${stats.total}`);
console.log(` Single-type: ${stats.singleType}`);
console.log(` Multi-type: ${stats.multiType}\n`);
if (!testCases || testCases.length === 0) {
throw new Error(
"Connection test cases data is empty. Please run 'npm run generate:connection-test-data' to generate test data."
);
}

const context = await browser.newContext();
const page = await context.newPage();

try {
await page.goto("/welcome");
await page.waitForLoadState("networkidle");

const newProjectButton = page.getByRole("button", { name: "New Project From Scratch", exact: true });
await expect(newProjectButton).toBeVisible();
await newProjectButton.click();
const { DashboardPage } = await import("../../pages/dashboard");
const dashboardPage = new DashboardPage(page);

const projectName = `connectionsButtonsTest`;
await page.goto("/?e2e=true");

const projectNameInput = page.getByPlaceholder("Enter project name");
await expect(projectNameInput).toBeVisible();
await projectNameInput.fill(projectName);
const existingProjectCell = page.getByRole("cell", { name: fixedProjectName });
const existingProjectCount = await existingProjectCell.count();

const createButton = page.getByRole("button", { name: "Create" });
await expect(createButton).toBeVisible();
await createButton.click();
if (existingProjectCount > 0) {
try {
await existingProjectCell.scrollIntoViewIfNeeded();
await existingProjectCell.click();
await page.waitForURL(/\/projects\/[^/]+/);

await page.waitForURL(/\/projects\/.+/);
await page.waitForLoadState("networkidle");
projectId = page.url().match(/\/projects\/([^/]+)/)?.[1] || "";
const projectTitleButton = page.getByRole("button", { name: "Edit project title" });
const currentProjectName = await projectTitleButton.textContent();

if (!projectId) {
throw new Error("Failed to extract project ID from URL");
if (currentProjectName === fixedProjectName) {
const projectPage = new ProjectPage(page);
await projectPage.deleteProject(fixedProjectName, false);
}
} catch {
await page.goto("/?e2e=true");
}
}

console.log(`✅ Created test project: ${projectName} (ID: ${projectId})\n`);
} finally {
await context.close();
await dashboardPage.createProjectFromMenu(fixedProjectName);
projectId = page.url().match(/\/projects\/([^/]+)/)?.[1] || "";

if (!projectId) {
throw new Error("Failed to extract project ID from URL");
}

await context.close();
});

test.beforeEach(async ({ page }) => {
if (!projectId) {
throw new Error("Project ID not set - beforeAll may have failed");
}
await page.goto(`/projects/${projectId}/explorer/settings`);
await page.waitForLoadState("networkidle");
const addButton = page.getByRole("button", { name: "Add Connections" });

const addConnectionsButton = page.getByRole("button", { name: "Add Connections" });
await expect(addConnectionsButton).toBeVisible();
await addConnectionsButton.click();
await addButton.waitFor({ state: "visible", timeout: 1000 });
await expect(addButton).toBeEnabled({ timeout: 1000 });
await addButton.click();

await page.waitForLoadState("networkidle");
await page.waitForTimeout(500);
await expect(page.getByText("Add new connection")).toBeVisible();
await expect(page.getByTestId("select-integration-empty")).toBeVisible();
});

test.afterAll(async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();

try {
await page.goto(`/projects/${projectId}/explorer`);
await page.waitForLoadState("networkidle");

const projectTitleButton = page.getByRole("button", { name: "Edit project title" });
const isOnProjectPage = await projectTitleButton.isVisible({ timeout: 3000 }).catch(() => false);

if (!isOnProjectPage) {
await context.close();

return;
}

const currentProjectName = await projectTitleButton.textContent();
if (currentProjectName !== fixedProjectName) {
await context.close();

return;
}

const projectPage = new ProjectPage(page);
await projectPage.deleteProject(fixedProjectName, false);
} catch {
// Cleanup failed - project may not exist or already deleted
}

await context.close();
});

for (const testCase of testCases) {
test(`${testCase.testName} should show action button`, async ({ connectionsConfig, page }) => {
test.setTimeout(250000);

await connectionsConfig.fillConnectionName(`Test ${testCase.testName}`);

await connectionsConfig.selectIntegration(testCase.label);
Expand All @@ -92,12 +125,11 @@ test.describe.skip("Connection Form Button Presence - Generated", () => {
await connectionsConfig.expectAnySubmitButton();

await page.waitForLoadState("networkidle");
await page.waitForTimeout(300);
await page.waitForTimeout(5800);

await expect(page).toHaveScreenshot(`connection-forms/${testCase.testName}-save-button.png`, {
fullPage: false,
animations: "disabled",
maxDiffPixelRatio: 0.05,
});

const backButton = page.getByRole("button", { name: "Close Add new connection" });
Expand Down
Loading
Loading