From 66ac6b747a8f2bfdfb639cb95a1b7d0b1b1e0c27 Mon Sep 17 00:00:00 2001 From: Ronen Mars Date: Tue, 16 Dec 2025 22:29:42 +0200 Subject: [PATCH] test(e2e): improve test infrastructure and utilities - Add randomName utility for unique project names - Update dashboard page with fixed name support - Improve project page selectors - Update variable tests with better selectors - Update topbar tests - Update trigger tests - Refactor connection visual tests - Update waitForToast utility - Add getItemId utility export --- e2e/fixtures/connectionsTestCases.json | 770 +++++++++--------- e2e/pages/dashboard.ts | 32 +- e2e/pages/project.ts | 59 +- e2e/pages/webhookSession.ts | 4 +- .../connections/buttonPresence.visual.spec.ts | 122 ++- e2e/project/project.spec.ts | 60 +- e2e/project/topbar.spec.ts | 43 +- e2e/project/triggers/triggerBase.spec.ts | 8 +- e2e/project/variable.spec.ts | 246 +++--- e2e/utils/index.ts | 2 +- e2e/utils/randomName.ts | 5 + e2e/utils/waitForToast.ts | 16 +- src/utilities/generateItemIds.utils.ts | 10 +- 13 files changed, 753 insertions(+), 624 deletions(-) create mode 100644 e2e/utils/randomName.ts diff --git a/e2e/fixtures/connectionsTestCases.json b/e2e/fixtures/connectionsTestCases.json index 8cb72d5892..1767c1d19c 100644 --- a/e2e/fixtures/connectionsTestCases.json +++ b/e2e/fixtures/connectionsTestCases.json @@ -1,386 +1,386 @@ [ - { - "testName": "Linear - OAuth v2 - Default app", - "integration": "linear", - "label": "Linear", - "authType": "oauthDefault", - "authLabel": "OAuth v2 - Default app", - "category": "multi-type" - }, - { - "testName": "Linear - OAuth v2 - Private app", - "integration": "linear", - "label": "Linear", - "authType": "oauthPrivate", - "authLabel": "OAuth v2 - Private app", - "category": "multi-type" - }, - { - "testName": "Linear - API Key", - "integration": "linear", - "label": "Linear", - "authType": "apiKey", - "authLabel": "API Key", - "category": "multi-type" - }, - { - "testName": "Auth0 (no auth types)", - "integration": "auth0", - "label": "Auth0", - "authType": null, - "authLabel": null, - "category": "single-type" - }, - { - "testName": "Asana (no auth types)", - "integration": "asana", - "label": "Asana", - "authType": null, - "authLabel": null, - "category": "single-type" - }, - { - "testName": "Anthropic (no auth types)", - "integration": "anthropic", - "label": "Anthropic", - "authType": null, - "authLabel": null, - "category": "single-type" - }, - { - "testName": "AWS (no auth types)", - "integration": "aws", - "label": "AWS", - "authType": null, - "authLabel": null, - "category": "single-type" - }, - { - "testName": "Google Calendar - User (OAuth 2.0)", - "integration": "calendar", - "label": "Google Calendar", - "authType": "oauth", - "authLabel": "User (OAuth 2.0)", - "category": "multi-type" - }, - { - "testName": "Google Calendar - Service Account (JSON Key)", - "integration": "calendar", - "label": "Google Calendar", - "authType": "json", - "authLabel": "Service Account (JSON Key)", - "category": "multi-type" - }, - { - "testName": "OpenAI ChatGPT (no auth types)", - "integration": "chatgpt", - "label": "OpenAI ChatGPT", - "authType": null, - "authLabel": null, - "category": "single-type" - }, - { - "testName": "Atlassian Confluence - OAuth 2.0 App", - "integration": "confluence", - "label": "Atlassian Confluence", - "authType": "oauth", - "authLabel": "OAuth 2.0 App", - "category": "multi-type" - }, - { - "testName": "Atlassian Confluence - User API Token / PAT", - "integration": "confluence", - "label": "Atlassian Confluence", - "authType": "apiToken", - "authLabel": "User API Token / PAT", - "category": "multi-type" - }, - { - "testName": "Discord (no auth types)", - "integration": "discord", - "label": "Discord", - "authType": null, - "authLabel": null, - "category": "single-type" - }, - { - "testName": "Google Drive - User (OAuth 2.0)", - "integration": "drive", - "label": "Google Drive", - "authType": "oauth", - "authLabel": "User (OAuth 2.0)", - "category": "multi-type" - }, - { - "testName": "Google Drive - Service Account (JSON Key)", - "integration": "drive", - "label": "Google Drive", - "authType": "json", - "authLabel": "Service Account (JSON Key)", - "category": "multi-type" - }, - { - "testName": "Google Forms - User (OAuth 2.0)", - "integration": "forms", - "label": "Google Forms", - "authType": "oauth", - "authLabel": "User (OAuth 2.0)", - "category": "multi-type" - }, - { - "testName": "Google Forms - Service Account (JSON Key)", - "integration": "forms", - "label": "Google Forms", - "authType": "json", - "authLabel": "Service Account (JSON Key)", - "category": "multi-type" - }, - { - "testName": "GitHub - OAuth v2 - Default app", - "integration": "github", - "label": "GitHub", - "authType": "oauth", - "authLabel": "OAuth v2 - Default app", - "category": "multi-type" - }, - { - "testName": "GitHub - OAuth v2 - Private app", - "integration": "github", - "label": "GitHub", - "authType": "oauthPrivate", - "authLabel": "OAuth v2 - Private app", - "category": "multi-type" - }, - { - "testName": "GitHub - PAT + Webhook", - "integration": "github", - "label": "GitHub", - "authType": "pat", - "authLabel": "PAT + Webhook", - "category": "multi-type" - }, - { - "testName": "Gmail - User (OAuth 2.0)", - "integration": "gmail", - "label": "Gmail", - "authType": "oauth", - "authLabel": "User (OAuth 2.0)", - "category": "multi-type" - }, - { - "testName": "Gmail - Service Account (JSON Key)", - "integration": "gmail", - "label": "Gmail", - "authType": "json", - "authLabel": "Service Account (JSON Key)", - "category": "multi-type" - }, - { - "testName": "YouTube - User (OAuth 2.0)", - "integration": "youtube", - "label": "YouTube", - "authType": "oauth", - "authLabel": "User (OAuth 2.0)", - "category": "multi-type" - }, - { - "testName": "YouTube - Service Account (JSON Key)", - "integration": "youtube", - "label": "YouTube", - "authType": "json", - "authLabel": "Service Account (JSON Key)", - "category": "multi-type" - }, - { - "testName": "Google Gemini (no auth types)", - "integration": "googlegemini", - "label": "Google Gemini", - "authType": null, - "authLabel": null, - "category": "single-type" - }, - { - "testName": "Atlassian Jira - OAuth 2.0 App", - "integration": "jira", - "label": "Atlassian Jira", - "authType": "oauth", - "authLabel": "OAuth 2.0 App", - "category": "multi-type" - }, - { - "testName": "Atlassian Jira - User API Token / PAT", - "integration": "jira", - "label": "Atlassian Jira", - "authType": "apiToken", - "authLabel": "User API Token / PAT", - "category": "multi-type" - }, - { - "testName": "Google Sheets - User (OAuth 2.0)", - "integration": "sheets", - "label": "Google Sheets", - "authType": "oauth", - "authLabel": "User (OAuth 2.0)", - "category": "multi-type" - }, - { - "testName": "Google Sheets - Service Account (JSON Key)", - "integration": "sheets", - "label": "Google Sheets", - "authType": "json", - "authLabel": "Service Account (JSON Key)", - "category": "multi-type" - }, - { - "testName": "Slack - OAuth v2 - Default app", - "integration": "slack", - "label": "Slack", - "authType": "oauthDefault", - "authLabel": "OAuth v2 - Default app", - "category": "multi-type" - }, - { - "testName": "Slack - OAuth v2 - Private app", - "integration": "slack", - "label": "Slack", - "authType": "oauthPrivate", - "authLabel": "OAuth v2 - Private app", - "category": "multi-type" - }, - { - "testName": "Twilio - Auth Token", - "integration": "twilio", - "label": "Twilio", - "authType": "authToken", - "authLabel": "Auth Token", - "category": "multi-type" - }, - { - "testName": "Twilio - API Key", - "integration": "twilio", - "label": "Twilio", - "authType": "apiKey", - "authLabel": "API Key", - "category": "multi-type" - }, - { - "testName": "Telegram (no auth types)", - "integration": "telegram", - "label": "Telegram", - "authType": null, - "authLabel": null, - "category": "single-type" - }, - { - "testName": "HubSpot (no auth types)", - "integration": "hubspot", - "label": "HubSpot", - "authType": null, - "authLabel": null, - "category": "single-type" - }, - { - "testName": "Zoom - OAuth v2 - Default app", - "integration": "zoom", - "label": "Zoom", - "authType": "oauthDefault", - "authLabel": "OAuth v2 - Default app", - "category": "multi-type" - }, - { - "testName": "Zoom - OAuth v2 - Private app", - "integration": "zoom", - "label": "Zoom", - "authType": "oauthPrivate", - "authLabel": "OAuth v2 - Private app", - "category": "multi-type" - }, - { - "testName": "Zoom - Private Server-to-Server", - "integration": "zoom", - "label": "Zoom", - "authType": "serverToServer", - "authLabel": "Private Server-to-Server", - "category": "multi-type" - }, - { - "testName": "Salesforce - OAuth v2 - Default app", - "integration": "salesforce", - "label": "Salesforce", - "authType": "oauthDefault", - "authLabel": "OAuth v2 - Default app", - "category": "multi-type" - }, - { - "testName": "Salesforce - OAuth v2 - Private app", - "integration": "salesforce", - "label": "Salesforce", - "authType": "oauthPrivate", - "authLabel": "OAuth v2 - Private app", - "category": "multi-type" - }, - { - "testName": "Microsoft Teams - Default user-delegated app", - "integration": "microsoft_teams", - "label": "Microsoft Teams", - "authType": "oauthDefault", - "authLabel": "Default user-delegated app", - "category": "multi-type" - }, - { - "testName": "Microsoft Teams - Private user-delegated app", - "integration": "microsoft_teams", - "label": "Microsoft Teams", - "authType": "oauthPrivate", - "authLabel": "Private user-delegated app", - "category": "multi-type" - }, - { - "testName": "Microsoft Teams - Private daemon application", - "integration": "microsoft_teams", - "label": "Microsoft Teams", - "authType": "daemonApp", - "authLabel": "Private daemon application", - "category": "multi-type" - }, - { - "testName": "Kubernetes (no auth types)", - "integration": "kubernetes", - "label": "Kubernetes", - "authType": null, - "authLabel": null, - "category": "single-type" - }, - { - "testName": "Reddit (no auth types)", - "integration": "reddit", - "label": "Reddit", - "authType": null, - "authLabel": null, - "category": "single-type" - }, - { - "testName": "Pipedrive (no auth types)", - "integration": "pipedrive", - "label": "Pipedrive", - "authType": null, - "authLabel": null, - "category": "single-type" - }, - { - "testName": "Notion - OAuth v2 - Default app", - "integration": "notion", - "label": "Notion", - "authType": "oauthDefault", - "authLabel": "OAuth v2 - Default app", - "category": "multi-type" - }, - { - "testName": "Notion - API Key", - "integration": "notion", - "label": "Notion", - "authType": "apiKey", - "authLabel": "API Key", - "category": "multi-type" - } -] \ No newline at end of file + { + "testName": "Linear - OAuth v2 - Default app", + "integration": "linear", + "label": "Linear", + "authType": "oauthDefault", + "authLabel": "OAuth v2 - Default app", + "category": "multi-type" + }, + { + "testName": "Linear - OAuth v2 - Private app", + "integration": "linear", + "label": "Linear", + "authType": "oauthPrivate", + "authLabel": "OAuth v2 - Private app", + "category": "multi-type" + }, + { + "testName": "Linear - API Key", + "integration": "linear", + "label": "Linear", + "authType": "apiKey", + "authLabel": "API Key", + "category": "multi-type" + }, + { + "testName": "Auth0 (no auth types)", + "integration": "auth0", + "label": "Auth0", + "authType": null, + "authLabel": null, + "category": "single-type" + }, + { + "testName": "Asana (no auth types)", + "integration": "asana", + "label": "Asana", + "authType": null, + "authLabel": null, + "category": "single-type" + }, + { + "testName": "Anthropic (no auth types)", + "integration": "anthropic", + "label": "Anthropic", + "authType": null, + "authLabel": null, + "category": "single-type" + }, + { + "testName": "AWS (no auth types)", + "integration": "aws", + "label": "AWS", + "authType": null, + "authLabel": null, + "category": "single-type" + }, + { + "testName": "Google Calendar - User (OAuth 2.0)", + "integration": "calendar", + "label": "Google Calendar", + "authType": "oauth", + "authLabel": "User (OAuth 2.0)", + "category": "multi-type" + }, + { + "testName": "Google Calendar - Service Account (JSON Key)", + "integration": "calendar", + "label": "Google Calendar", + "authType": "json", + "authLabel": "Service Account (JSON Key)", + "category": "multi-type" + }, + { + "testName": "OpenAI ChatGPT (no auth types)", + "integration": "chatgpt", + "label": "OpenAI ChatGPT", + "authType": null, + "authLabel": null, + "category": "single-type" + }, + { + "testName": "Atlassian Confluence - OAuth 2.0 App", + "integration": "confluence", + "label": "Atlassian Confluence", + "authType": "oauth", + "authLabel": "OAuth 2.0 App", + "category": "multi-type" + }, + { + "testName": "Atlassian Confluence - User API Token / PAT", + "integration": "confluence", + "label": "Atlassian Confluence", + "authType": "apiToken", + "authLabel": "User API Token / PAT", + "category": "multi-type" + }, + { + "testName": "Discord (no auth types)", + "integration": "discord", + "label": "Discord", + "authType": null, + "authLabel": null, + "category": "single-type" + }, + { + "testName": "Google Drive - User (OAuth 2.0)", + "integration": "drive", + "label": "Google Drive", + "authType": "oauth", + "authLabel": "User (OAuth 2.0)", + "category": "multi-type" + }, + { + "testName": "Google Drive - Service Account (JSON Key)", + "integration": "drive", + "label": "Google Drive", + "authType": "json", + "authLabel": "Service Account (JSON Key)", + "category": "multi-type" + }, + { + "testName": "Google Forms - User (OAuth 2.0)", + "integration": "forms", + "label": "Google Forms", + "authType": "oauth", + "authLabel": "User (OAuth 2.0)", + "category": "multi-type" + }, + { + "testName": "Google Forms - Service Account (JSON Key)", + "integration": "forms", + "label": "Google Forms", + "authType": "json", + "authLabel": "Service Account (JSON Key)", + "category": "multi-type" + }, + { + "testName": "GitHub - OAuth v2 - Default app", + "integration": "github", + "label": "GitHub", + "authType": "oauth", + "authLabel": "OAuth v2 - Default app", + "category": "multi-type" + }, + { + "testName": "GitHub - OAuth v2 - Private app", + "integration": "github", + "label": "GitHub", + "authType": "oauthPrivate", + "authLabel": "OAuth v2 - Private app", + "category": "multi-type" + }, + { + "testName": "GitHub - PAT + Webhook", + "integration": "github", + "label": "GitHub", + "authType": "pat", + "authLabel": "PAT + Webhook", + "category": "multi-type" + }, + { + "testName": "Gmail - User (OAuth 2.0)", + "integration": "gmail", + "label": "Gmail", + "authType": "oauth", + "authLabel": "User (OAuth 2.0)", + "category": "multi-type" + }, + { + "testName": "Gmail - Service Account (JSON Key)", + "integration": "gmail", + "label": "Gmail", + "authType": "json", + "authLabel": "Service Account (JSON Key)", + "category": "multi-type" + }, + { + "testName": "YouTube - User (OAuth 2.0)", + "integration": "youtube", + "label": "YouTube", + "authType": "oauth", + "authLabel": "User (OAuth 2.0)", + "category": "multi-type" + }, + { + "testName": "YouTube - Service Account (JSON Key)", + "integration": "youtube", + "label": "YouTube", + "authType": "json", + "authLabel": "Service Account (JSON Key)", + "category": "multi-type" + }, + { + "testName": "Google Gemini (no auth types)", + "integration": "googlegemini", + "label": "Google Gemini", + "authType": null, + "authLabel": null, + "category": "single-type" + }, + { + "testName": "Atlassian Jira - OAuth 2.0 App", + "integration": "jira", + "label": "Atlassian Jira", + "authType": "oauth", + "authLabel": "OAuth 2.0 App", + "category": "multi-type" + }, + { + "testName": "Atlassian Jira - User API Token / PAT", + "integration": "jira", + "label": "Atlassian Jira", + "authType": "apiToken", + "authLabel": "User API Token / PAT", + "category": "multi-type" + }, + { + "testName": "Google Sheets - User (OAuth 2.0)", + "integration": "sheets", + "label": "Google Sheets", + "authType": "oauth", + "authLabel": "User (OAuth 2.0)", + "category": "multi-type" + }, + { + "testName": "Google Sheets - Service Account (JSON Key)", + "integration": "sheets", + "label": "Google Sheets", + "authType": "json", + "authLabel": "Service Account (JSON Key)", + "category": "multi-type" + }, + { + "testName": "Slack - OAuth v2 - Default app", + "integration": "slack", + "label": "Slack", + "authType": "oauthDefault", + "authLabel": "OAuth v2 - Default app", + "category": "multi-type" + }, + { + "testName": "Slack - OAuth v2 - Private app", + "integration": "slack", + "label": "Slack", + "authType": "oauthPrivate", + "authLabel": "OAuth v2 - Private app", + "category": "multi-type" + }, + { + "testName": "Twilio - Auth Token", + "integration": "twilio", + "label": "Twilio", + "authType": "authToken", + "authLabel": "Auth Token", + "category": "multi-type" + }, + { + "testName": "Twilio - API Key", + "integration": "twilio", + "label": "Twilio", + "authType": "apiKey", + "authLabel": "API Key", + "category": "multi-type" + }, + { + "testName": "Telegram (no auth types)", + "integration": "telegram", + "label": "Telegram", + "authType": null, + "authLabel": null, + "category": "single-type" + }, + { + "testName": "HubSpot (no auth types)", + "integration": "hubspot", + "label": "HubSpot", + "authType": null, + "authLabel": null, + "category": "single-type" + }, + { + "testName": "Zoom - OAuth v2 - Default app", + "integration": "zoom", + "label": "Zoom", + "authType": "oauthDefault", + "authLabel": "OAuth v2 - Default app", + "category": "multi-type" + }, + { + "testName": "Zoom - OAuth v2 - Private app", + "integration": "zoom", + "label": "Zoom", + "authType": "oauthPrivate", + "authLabel": "OAuth v2 - Private app", + "category": "multi-type" + }, + { + "testName": "Zoom - Private Server-to-Server", + "integration": "zoom", + "label": "Zoom", + "authType": "serverToServer", + "authLabel": "Private Server-to-Server", + "category": "multi-type" + }, + { + "testName": "Salesforce - OAuth v2 - Default app", + "integration": "salesforce", + "label": "Salesforce", + "authType": "oauthDefault", + "authLabel": "OAuth v2 - Default app", + "category": "multi-type" + }, + { + "testName": "Salesforce - OAuth v2 - Private app", + "integration": "salesforce", + "label": "Salesforce", + "authType": "oauthPrivate", + "authLabel": "OAuth v2 - Private app", + "category": "multi-type" + }, + { + "testName": "Microsoft Teams - Default user-delegated app", + "integration": "microsoft_teams", + "label": "Microsoft Teams", + "authType": "oauthDefault", + "authLabel": "Default user-delegated app", + "category": "multi-type" + }, + { + "testName": "Microsoft Teams - Private user-delegated app", + "integration": "microsoft_teams", + "label": "Microsoft Teams", + "authType": "oauthPrivate", + "authLabel": "Private user-delegated app", + "category": "multi-type" + }, + { + "testName": "Microsoft Teams - Private daemon application", + "integration": "microsoft_teams", + "label": "Microsoft Teams", + "authType": "daemonApp", + "authLabel": "Private daemon application", + "category": "multi-type" + }, + { + "testName": "Kubernetes (no auth types)", + "integration": "kubernetes", + "label": "Kubernetes", + "authType": null, + "authLabel": null, + "category": "single-type" + }, + { + "testName": "Reddit (no auth types)", + "integration": "reddit", + "label": "Reddit", + "authType": null, + "authLabel": null, + "category": "single-type" + }, + { + "testName": "Pipedrive (no auth types)", + "integration": "pipedrive", + "label": "Pipedrive", + "authType": null, + "authLabel": null, + "category": "single-type" + }, + { + "testName": "Notion - OAuth v2 - Default app", + "integration": "notion", + "label": "Notion", + "authType": "oauthDefault", + "authLabel": "OAuth v2 - Default app", + "category": "multi-type" + }, + { + "testName": "Notion - API Key", + "integration": "notion", + "label": "Notion", + "authType": "apiKey", + "authLabel": "API Key", + "category": "multi-type" + } +] diff --git a/e2e/pages/dashboard.ts b/e2e/pages/dashboard.ts index c4d2f08162..897b7aad74 100644 --- a/e2e/pages/dashboard.ts +++ b/e2e/pages/dashboard.ts @@ -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"; @@ -29,30 +29,44 @@ export class DashboardPage { return this.page.getByText(text); } - async createProjectFromMenu(): Promise { + async createProjectFromMenu(fixedName?: string): Promise { 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(); @@ -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 }); } } diff --git a/e2e/pages/project.ts b/e2e/pages/project.ts index 178e5ffe4d..4f1b49d25e 100644 --- a/e2e/pages/project.ts +++ b/e2e/pages/project.ts @@ -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; @@ -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); diff --git a/e2e/pages/webhookSession.ts b/e2e/pages/webhookSession.ts index 5ee852c699..8695ccf2cb 100644 --- a/e2e/pages/webhookSession.ts +++ b/e2e/pages/webhookSession.ts @@ -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 { @@ -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) { diff --git a/e2e/project/connections/buttonPresence.visual.spec.ts b/e2e/project/connections/buttonPresence.visual.spec.ts index 61c7dacda0..07d7a5d439 100644 --- a/e2e/project/connections/buttonPresence.visual.spec.ts +++ b/e2e/project/connections/buttonPresence.visual.spec.ts @@ -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 { @@ -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); @@ -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" }); diff --git a/e2e/project/project.spec.ts b/e2e/project/project.spec.ts index ab2f319f0d..43f6c7c0b1 100644 --- a/e2e/project/project.spec.ts +++ b/e2e/project/project.spec.ts @@ -1,51 +1,45 @@ -import randomatic from "randomatic"; - import { expect, test } from "../fixtures"; -import { waitForLoadingOverlayGone } from "../utils/waitForLoadingOverlayToDisappear"; -import { waitForMonacoEditorToLoad } from "../utils/waitForMonacoEditor"; +import { ProjectPage } from "../pages/project"; +import { waitForToastToBeRemoved } from "../utils"; +import { randomName } from "../utils/randomName"; test.describe("Project Suite", () => { let projectName: string; let projectId: string; + let randomSuffix: string; - test.beforeAll(async ({ browser }) => { - const context = await browser.newContext(); - const page = await context.newPage(); - - await waitForLoadingOverlayGone(page); - await page.goto("/"); - await page.locator('nav[aria-label="Main navigation"] button[aria-label="New Project"]').hover(); - await page.locator('nav[aria-label="Main navigation"] button[aria-label="New Project"]').click(); - await page.getByRole("button", { name: "New Project From Scratch" }).hover(); - await page.getByRole("button", { name: "New Project From Scratch" }).click(); - projectName = randomatic("Aa", 8); - await page.getByPlaceholder("Enter project name").fill(projectName); - await page.getByRole("button", { name: "Create", exact: true }).click(); - await expect(page.locator('button[aria-label="Open program.py"]')).toBeVisible(); - await page.getByRole("button", { name: "Open program.py" }).click(); - - await expect(page.getByRole("tab", { name: "program.py Close file tab" })).toBeVisible(); - - await waitForMonacoEditorToLoad(page, 6000); - - await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible({ timeout: 1200 }); - + test.beforeEach(async ({ dashboardPage, page }) => { + projectName = await dashboardPage.createProjectFromMenu(); projectId = page.url().match(/\/projects\/([^/]+)/)?.[1] || ""; - - await context.close(); + await page.goto(`/projects/${projectId}`); }); - test.beforeEach(async ({ page }) => { - await page.goto(`/projects/${projectId}`); + test.afterEach(async ({ page }) => { + const projectPage = new ProjectPage(page); + const deploymentExists = await page.locator('button[aria-label="Sessions"]').isEnabled(); + await projectPage.deleteProject(projectName, !!deploymentExists); }); test("Change project name", async ({ page }) => { await page.getByText(projectName).hover(); await page.getByText(projectName).click(); const input = page.getByRole("textbox", { name: "Rename" }); - await input.fill("NewProjectName"); + randomSuffix = randomName(); + const modifiedProjectName = `proj_${randomSuffix}`; + await input.fill(modifiedProjectName); await input.press("Enter"); - - await expect(page.getByText("NewProjectName")).toBeVisible(); + await waitForToastToBeRemoved(page, "Project renamed successfully"); + const projectNameElement = await page.getByRole("button", { name: "Edit project title" }); + await expect(projectNameElement).toBeVisible(); + const projectNameText = await projectNameElement.getByText(modifiedProjectName, { exact: true }); + await expect(projectNameText).toBeVisible(); + projectName = modifiedProjectName; + await page.locator('button[aria-label="System Log"]').click(); + + const renamedProjectLogText = `INFO: Project renamed successfully, project name: ${modifiedProjectName}, project ID: ${projectId}`; + const renamedProjectLog = page.getByText(renamedProjectLogText, { exact: true }); + await expect(renamedProjectLog).toBeVisible(); + + await page.locator('button[aria-label="System Log"]').click(); }); }); diff --git a/e2e/project/topbar.spec.ts b/e2e/project/topbar.spec.ts index f9e0500516..f134055f68 100644 --- a/e2e/project/topbar.spec.ts +++ b/e2e/project/topbar.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from "../fixtures"; +import { DashboardPage } from "../pages/dashboard"; test.describe("Project Topbar Suite", () => { test("Changed deployments topbar", async ({ dashboardPage, page }) => { @@ -28,10 +29,24 @@ test.describe("Project Topbar Suite", () => { }); test.describe("Responsive button behavior", () => { - test("Navigation buttons - icons always visible on all screen sizes", async ({ dashboardPage, page }) => { - await page.setViewportSize({ width: 1920, height: 1080 }); + let projectId: string; + + test.beforeAll(async ({ browser }) => { + const context = await browser.newContext(); + const page = await context.newPage(); + + const dashboardPage = new DashboardPage(page); await dashboardPage.createProjectFromMenu(); + projectId = page.url().match(/\/projects\/([^/]+)/)?.[1] || ""; + + await context.close(); + }); + + test("Navigation buttons - icons always visible on all screen sizes", async ({ page }) => { + await page.setViewportSize({ width: 1920, height: 1080 }); + await page.goto(`/projects/${projectId}`); + await page.waitForTimeout(500); const explorerIcon = page.locator('button[aria-label="Explorer"] svg').first(); @@ -52,9 +67,9 @@ test.describe("Project Topbar Suite", () => { await expect(eventsIcon).toBeVisible(); }); - test("Navigation buttons - labels visible on large screens (>= 1280px)", async ({ dashboardPage, page }) => { + test("Navigation buttons - labels visible on large screens (>= 1280px)", async ({ page }) => { await page.setViewportSize({ width: 1920, height: 1080 }); - await dashboardPage.createProjectFromMenu(); + await page.goto(`/projects/${projectId}`); await page.waitForTimeout(500); @@ -69,9 +84,9 @@ test.describe("Project Topbar Suite", () => { await expect(eventsLabel).toBeVisible(); }); - test("Navigation buttons - labels hidden on small screens (< 1280px)", async ({ dashboardPage, page }) => { + test("Navigation buttons - labels hidden on small screens (< 1280px)", async ({ page }) => { await page.setViewportSize({ width: 1200, height: 800 }); - await dashboardPage.createProjectFromMenu(); + await page.goto(`/projects/${projectId}`); await page.waitForTimeout(500); @@ -86,9 +101,9 @@ test.describe("Project Topbar Suite", () => { await expect(eventsLabel).toBeHidden(); }); - test("Action buttons - labels visible on large screens (> 1600px)", async ({ dashboardPage, page }) => { + test("Action buttons - labels visible on large screens (> 1600px)", async ({ page }) => { await page.setViewportSize({ width: 1920, height: 1080 }); - await dashboardPage.createProjectFromMenu(); + await page.goto(`/projects/${projectId}`); const buildButton = page.locator('button[aria-label="Validate project"]'); const deployButton = page.locator('button[aria-label="Deploy project"]'); @@ -99,9 +114,9 @@ test.describe("Project Topbar Suite", () => { await expect(moreButton.getByText("More")).toBeVisible(); }); - test("Action buttons - labels hidden on medium screens (<= 1600px)", async ({ dashboardPage, page }) => { + test("Action buttons - labels hidden on medium screens (<= 1600px)", async ({ page }) => { await page.setViewportSize({ width: 1600, height: 900 }); - await dashboardPage.createProjectFromMenu(); + await page.goto(`/projects/${projectId}`); const buildButton = page.locator('button[aria-label="Validate project"]'); const deployButton = page.locator('button[aria-label="Deploy project"]'); @@ -137,9 +152,9 @@ test.describe("Project Topbar Suite", () => { await expect(settingsButton).toBeVisible(); }); - test("Buttons remain functional after viewport resize", async ({ dashboardPage, page }) => { + test("Buttons remain functional after viewport resize", async ({ page }) => { await page.setViewportSize({ width: 1920, height: 1080 }); - await dashboardPage.createProjectFromMenu(); + await page.goto(`/projects/${projectId}`); await page.setViewportSize({ width: 1200, height: 800 }); @@ -154,9 +169,9 @@ test.describe("Project Topbar Suite", () => { await expect(page.locator('button[aria-label="Explorer"]')).toHaveClass(/active/); }); - test("Action buttons icons remain visible when labels hidden", async ({ dashboardPage, page }) => { + test("Action buttons icons remain visible when labels hidden", async ({ page }) => { await page.setViewportSize({ width: 1400, height: 900 }); - await dashboardPage.createProjectFromMenu(); + await page.goto(`/projects/${projectId}`); const buildButton = page.locator('button[aria-label="Validate project"]'); const deployButton = page.locator('button[aria-label="Deploy project"]'); diff --git a/e2e/project/triggers/triggerBase.spec.ts b/e2e/project/triggers/triggerBase.spec.ts index 21a87e7edb..83d87ce690 100644 --- a/e2e/project/triggers/triggerBase.spec.ts +++ b/e2e/project/triggers/triggerBase.spec.ts @@ -88,8 +88,8 @@ async function modifyTrigger( await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible(); } - const configureButtons = page.locator(`button[aria-label="Edit ${name}"]`); - await configureButtons.click(); + const configureButton = page.locator(`button[aria-label="Configure ${name}"]`); + await configureButton.click(); if (withActiveDeployment) { await page.locator('heading[aria-label="Warning Active Deployment"]').isVisible(); @@ -185,8 +185,8 @@ test.describe("Project Triggers Suite", () => { await createTriggerScheduler(page, "triggerName", "5 4 * * *", "program.py", "on_trigger"); await page.locator('button[aria-label="Return back"]').click(); - const configureButtons = page.locator(`button[aria-label="Edit ${triggerName}"]`); - await configureButtons.first().click(); + const configureButton = page.locator(`button[aria-label="Configure ${triggerName}"]`); + await configureButton.click(); await page.getByRole("textbox", { name: "Cron expression" }).click(); await page.getByRole("textbox", { name: "Cron expression" }).fill(""); diff --git a/e2e/project/variable.spec.ts b/e2e/project/variable.spec.ts index 9b8fb0c191..2f849744fd 100644 --- a/e2e/project/variable.spec.ts +++ b/e2e/project/variable.spec.ts @@ -1,66 +1,90 @@ -import randomatic from "randomatic"; +import { Page } from "playwright/test"; import { expect, test } from "../fixtures"; -import { waitForToast } from "../utils"; -import { waitForLoadingOverlayGone } from "../utils/waitForLoadingOverlayToDisappear"; -import { waitForMonacoEditorToLoad } from "../utils/waitForMonacoEditor"; - -const varName = "nameVariable"; - -let projectId: string; - -test.beforeAll(async ({ browser }) => { - const context = await browser.newContext(); - const page = await context.newPage(); - - await waitForLoadingOverlayGone(page); - await page.goto("/"); - await page.locator('nav[aria-label="Main navigation"] button[aria-label="New Project"]').hover(); - await page.locator('nav[aria-label="Main navigation"] button[aria-label="New Project"]').click(); - await page.getByRole("button", { name: "New Project From Scratch" }).hover(); - await page.getByRole("button", { name: "New Project From Scratch" }).click(); - const projectName = randomatic("Aa", 8); - await page.getByPlaceholder("Enter project name").fill(projectName); - await page.getByRole("button", { name: "Create", exact: true }).click(); - await expect(page.locator('button[aria-label="Open program.py"]')).toBeVisible(); - await page.getByRole("button", { name: "Open program.py" }).click(); - - await expect(page.getByRole("tab", { name: "program.py Close file tab" })).toBeVisible(); - - await waitForMonacoEditorToLoad(page, 6000); +import { DashboardPage } from "../pages/dashboard"; +import { ProjectPage } from "../pages/project"; +import { waitForToastToBeRemoved } from "../utils"; +import { getItemId } from "@src/utilities/generateItemIds.utils"; + +const newValueVariable = "newValueVariable"; + +let projectName: string; + +const createVariable = async ({ + page, + name, + value, + description, + activeDeployment = false, +}: { + activeDeployment?: boolean; + description?: string; + name: string; + page: Page; + value: string; +}) => { + await page.locator('button[aria-label="Add Variables"]').click(); + if (activeDeployment) { + await page.locator('button[aria-label="Ok"]').click(); + } + await page.getByLabel("Name", { exact: true }).fill(name); + if (description) { + await page.getByLabel("Description").fill(description); + } + await page.getByLabel("Value").fill(value); + await page.getByRole("button", { name: "Save", exact: true }).click(); + await waitForToastToBeRemoved(page, "Variable created successfully"); +}; + +const openConfigurationSidebar = async (page: Page) => { + const configureButton = page.locator('button[aria-label="Config"]'); + await configureButton.click(); + try { + await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible(); + } catch { + await configureButton.click(); + await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible(); + } +}; - await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible({ timeout: 1200 }); +test.describe("Project Variables Suite", () => { + test.beforeEach(async ({ page }) => { + const dashboardPage = new DashboardPage(page); + projectName = await dashboardPage.createProjectFromMenu(); + const projectId = page.url().match(/\/projects\/([^/]+)/)?.[1] || ""; + await page.goto(`/projects/${projectId}/explorer/settings`); + }); - projectId = page.url().match(/\/projects\/([^/]+)/)?.[1] || ""; + test.afterEach(async ({ page }) => { + await openConfigurationSidebar(page); - await page.goto(`/projects/${projectId}/explorer/settings`); - await page.locator('button[aria-label="Add Variables"]').click(); + const projectPage = new ProjectPage(page); + const deploymentExists = await page.locator('button[aria-label="Sessions"]').isEnabled(); - await page.getByLabel("Name", { exact: true }).click(); - await page.getByLabel("Name", { exact: true }).fill("nameVariable"); - await page.getByLabel("Value", { exact: true }).click(); - await page.getByLabel("Value").fill("valueVariable"); - await page.locator('button[aria-label="Save"]').click(); + await projectPage.deleteProject(projectName, !!deploymentExists); + }); - const toast = await waitForToast(page, "Variable created successfully"); - await expect(toast).toBeVisible(); + test("Create a valid variable", async ({ page }) => { + await page.locator('button[aria-label="Add Variables"]').click(); - await context.close(); -}); + await page.getByLabel("Name", { exact: true }).click(); + await page.getByLabel("Name", { exact: true }).fill("nameVariable"); + await page.getByLabel("Value", { exact: true }).click(); + await page.getByLabel("Value").fill("valueVariable"); + await page.locator('button[aria-label="Save"]').click(); -test.beforeEach(async ({ page }) => { - await page.goto(`/projects/${projectId}/explorer/settings`); -}); + await waitForToastToBeRemoved(page, "Variable created successfully"); + }); -test.describe("Project Variables Suite", () => { test("Create variable with empty fields", async ({ page }) => { await page.locator('button[aria-label="Add Variables"]').click(); await page.locator('button[aria-label="Save"]').click(); const nameErrorMessage = page.getByRole("alert", { name: "Name is required" }); - const valueErrorMessage = page.getByRole("alert", { name: "Value is required" }); await expect(nameErrorMessage).toBeVisible(); - await expect(valueErrorMessage).toBeVisible(); + const backButton = page.getByRole("button", { name: "Close Add new" }); + await expect(backButton).toBeVisible(); + await backButton.click(); }); test("Create variable with description", async ({ page }) => { @@ -74,8 +98,7 @@ test.describe("Project Variables Suite", () => { await page.getByLabel("Value").fill("testValue"); await page.getByRole("button", { name: "Save", exact: true }).click(); - const toast = await waitForToast(page, "Variable created successfully"); - await expect(toast).toBeVisible(); + await waitForToastToBeRemoved(page, "Variable created successfully"); }); test("Create variable without description", async ({ page }) => { @@ -87,88 +110,127 @@ test.describe("Project Variables Suite", () => { await page.getByLabel("Value").fill("testValue"); await page.getByRole("button", { name: "Save", exact: true }).click(); - const toast = await waitForToast(page, "Variable created successfully"); - await expect(toast).toBeVisible(); + await waitForToastToBeRemoved(page, "Variable created successfully"); }); test("Modify variable", async ({ page }) => { - const configureButtons = page.locator('button[aria-label="Edit"]'); - await configureButtons.first().click(); + const testVarName = "modifyTestVar"; + const testVarValue = "initialValue"; + await createVariable({ page, name: testVarName, value: testVarValue }); + + const configureButtonId = getItemId(testVarName, "variable", "configureButtonId"); + const configureButton = page.locator(`button[id="${configureButtonId}"]`); + await configureButton.click(); const valueInput = page.getByLabel("Value", { exact: true }); - await valueInput.fill("newValueVariable"); + await valueInput.fill(newValueVariable); const value = await valueInput.inputValue(); - expect(value).toEqual("newValueVariable"); + expect(value).toEqual(newValueVariable); await page.locator('button[aria-label="Save"]').click(); await page.waitForURL(/\/projects\/[^/]+\/explorer\/settings/); - await page.locator("button[aria-label='Variable information for \"nameVariable\"']").hover(); + await page.locator(`button[aria-label='Variable information for "${testVarName}"']`).hover(); + + await expect(page.getByText(newValueVariable)).toBeVisible(); - await expect(page.getByText("newValueVariable")).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(page.getByText(newValueVariable)).not.toBeVisible(); }); test("Modify variable description", async ({ page }) => { - const configureButtons = page.locator('button[aria-label="Edit"]'); - await configureButtons.first().click(); + const testVarName = "descTestVar"; + await createVariable({ page, name: testVarName, value: "someValue", description: "Initial description" }); + + const configureButtonId = getItemId(testVarName, "variable", "configureButtonId"); + const configureButton = page.locator(`button[id="${configureButtonId}"]`); + await configureButton.click(); await page.getByLabel("Description").click(); await page.getByLabel("Description").fill("Updated description text"); await page.locator('button[aria-label="Save"]').click(); - const toast = await waitForToast(page, "Variable edited successfully"); - await expect(toast).toBeVisible(); - await page.locator("button[aria-label='Variable information for \"nameVariable\"']").hover(); + await waitForToastToBeRemoved(page, "Variable edited successfully"); + await page.locator(`button[aria-label='Variable information for "${testVarName}"']`).hover(); await expect(page.getByText("Updated description text")).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(page.getByText("Updated description text")).not.toBeVisible(); + }); + + test("Modifying variable with empty value", async ({ page }) => { + const testVarName = "emptyValVar"; + await createVariable({ page, name: testVarName, value: "initialValue" }); + + const configureButtonId = getItemId(testVarName, "variable", "configureButtonId"); + const configureButton = page.locator(`button[id="${configureButtonId}"]`); + await configureButton.click(); + + await page.getByRole("textbox", { name: "Value", exact: true }).clear(); + await expect(page.getByRole("textbox", { name: "Value", exact: true })).toBeEmpty(); + + await page.locator('button[aria-label="Save"]').click(); + + await page.locator(`button[aria-label='Variable information for "${testVarName}"']`).hover(); + await expect(page.getByText("No value set")).toBeVisible(); }); test("Modify variable with active deployment", async ({ page }) => { + const testVarName = "deployTestVar"; + const deployButton = page.locator('button[aria-label="Deploy project"]'); await deployButton.click(); - const toast = await waitForToast(page, "Project successfully deployed with 1 warning"); - await expect(toast).toBeVisible(); - await expect(toast).not.toBeVisible({ timeout: 5000 }); + await waitForToastToBeRemoved(page, "Project successfully deployed with 1 warning"); + + await createVariable({ page, name: testVarName, value: "initialValue", activeDeployment: true }); - const configureButton = page.locator('button[id="nameVariable-variable-configure-button"]'); + const configureButtonId = getItemId(testVarName, "variable", "configureButtonId"); + const configureButton = page.locator(`button[id="${configureButtonId}"]`); await configureButton.click(); + await page.locator('heading[aria-label="Warning Active Deployment"]').isVisible(); const okButton = page.locator('button[aria-label="Ok"]'); - if (await okButton.isVisible()) { - await okButton.click(); - } + await okButton.isVisible(); + await okButton.click(); + + await expect(page.getByText("Changes might affect the currently running deployments.")).toBeVisible(); await page.getByLabel("Value", { exact: true }).click(); - await page.getByLabel("Value").fill("newValueVariable"); + await page.getByLabel("Value").fill(newValueVariable); await page.locator('button[aria-label="Save"]').click(); await page.waitForURL(/\/projects\/[^/]+\/explorer\/settings/); - await page.locator('button[aria-label="Config"]').click(); + await waitForToastToBeRemoved(page, "Variable edited successfully"); - await page.locator("button[aria-label='Variable information for \"nameVariable\"']").hover(); - await expect(page.getByText("newValueVariable")).toBeVisible(); - }); - - test("Modifying variable with empty value", async ({ page }) => { - const configureButtons = page.locator('button[aria-label="Edit"]'); - await configureButtons.first().click(); - - await page.getByRole("textbox", { name: "Value", exact: true }).clear(); - await page.locator('button[aria-label="Save"]').click(); - - const valueErrorMessage = page.getByRole("alert", { name: "Value is required" }); - await expect(valueErrorMessage).toBeVisible(); + await page.locator(`button[aria-label='Variable information for "${testVarName}"']`).hover(); + await expect(page.getByText(newValueVariable)).toBeVisible(); + await page.mouse.move(0, 0); + await expect(page.getByText(newValueVariable)).not.toBeVisible(); }); test("Delete variable", async ({ page }) => { - const deleteButton = page.locator('button[aria-label="Delete nameVariable"]'); - await deleteButton.click(); - - const confirmButton = page.locator('button[aria-label="Confirm and delete nameVariable"]'); + const testVarName = "deleteTestVar"; + await createVariable({ page, name: testVarName, value: "toBeDeleted" }); + + const deleteVarButton = page.getByRole("button", { name: `Delete ${testVarName}`, exact: true }); + await expect(deleteVarButton).toBeVisible(); + await deleteVarButton.click(); + await page.mouse.move(0, 0); + await page.waitForTimeout(300); + + await expect(page.locator("h3").filter({ hasText: "Delete Variable" })).toBeVisible(); + await expect(page.getByText(`Are you sure you want to delete ${testVarName}`)).toBeVisible(); + await expect( + page.getByText("This action cannot be undone, and all related data will be permanently removed.") + ).toBeVisible(); + + const cancelButton = page.locator('button[aria-label="Cancel"]'); + await expect(cancelButton).toBeVisible(); + + const confirmButton = page.locator(`button[aria-label="Confirm and delete ${testVarName}"]`); + await expect(confirmButton).toBeVisible(); await confirmButton.click(); - const toast = await waitForToast(page, "Variable removed successfully"); - await expect(toast).toBeVisible(); - await expect(page.getByText(varName, { exact: true })).not.toBeVisible(); - await expect(page.getByText("No variables found for this project")).toBeVisible(); + await waitForToastToBeRemoved(page, `${testVarName} removed successfully`); + await expect(page.getByText(testVarName, { exact: true })).not.toBeVisible(); }); }); diff --git a/e2e/utils/index.ts b/e2e/utils/index.ts index 8263cacdb8..ada142c8d8 100644 --- a/e2e/utils/index.ts +++ b/e2e/utils/index.ts @@ -1,3 +1,3 @@ -export { waitForToast } from "../utils/waitForToast"; +export { waitForToast, waitForToastToBeRemoved } from "../utils/waitForToast"; export { createNetworkListeners, logNetworkDiagnostics } from "../utils/networkMonitoring"; export type { NetworkCapture, NetworkRequest, NetworkResponse } from "../utils/networkMonitoring"; diff --git a/e2e/utils/randomName.ts b/e2e/utils/randomName.ts new file mode 100644 index 0000000000..ac7536bfdd --- /dev/null +++ b/e2e/utils/randomName.ts @@ -0,0 +1,5 @@ +import randomatic from "randomatic"; + +export const randomName = () => { + return randomatic("Aa", 1) + randomatic("Aa0", 9); +}; diff --git a/e2e/utils/waitForToast.ts b/e2e/utils/waitForToast.ts index 69538d2900..4bf0aef06f 100644 --- a/e2e/utils/waitForToast.ts +++ b/e2e/utils/waitForToast.ts @@ -1,12 +1,12 @@ import type { Page } from "@playwright/test"; export const waitForToast = async (page: Page, toastMessage: string, timeout = 10000) => { - const start = Date.now(); - while (Date.now() - start < timeout) { - const toast = page.locator(`[role="alert"]`, { hasText: toastMessage }).last(); - if (await toast.isVisible()) { - return toast; - } - } - throw new Error(`Toast with message "${toastMessage}" not found`); + const toast = page.locator(`[role="alert"]`, { hasText: toastMessage }).last(); + await toast.waitFor({ state: "visible", timeout }); + return toast; +}; + +export const waitForToastToBeRemoved = async (page: Page, toastMessage: string, timeout = 10000) => { + const toast = await waitForToast(page, toastMessage, timeout / 2); + await toast.waitFor({ state: "hidden", timeout }); }; diff --git a/src/utilities/generateItemIds.utils.ts b/src/utilities/generateItemIds.utils.ts index 39dddbc204..974dfb3276 100644 --- a/src/utilities/generateItemIds.utils.ts +++ b/src/utilities/generateItemIds.utils.ts @@ -1,7 +1,8 @@ import { ItemIds } from "@src/interfaces/utilities"; +import { Entity } from "@src/types/entities.type"; -export const generateItemIds = (itemId: string, type: "variable" | "trigger" | "connection"): ItemIds => { - const suffix = type === "variable" ? "variable" : type === "trigger" ? "trigger" : "connection"; +export const generateItemIds = (itemId: string, type: Entity): ItemIds => { + const suffix = type; return { containerId: `${itemId}-${suffix}-container`, @@ -15,3 +16,8 @@ export const generateItemIds = (itemId: string, type: "variable" | "trigger" | " webhookUrlButtonId: type === "trigger" ? `${itemId}-${suffix}-webhook-url-button` : undefined, }; }; + +export const getItemId = (itemId: string, type: Entity, key: keyof ItemIds): string => { + const itemIds = generateItemIds(itemId, type); + return itemIds[key] ?? ""; +};