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
11 changes: 10 additions & 1 deletion e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@
import { expect, test as base } from "@playwright/test";

import { ConnectionPage, DashboardPage, ProjectPage } from "./pages";
import { RateLimitHandler } from "./utils";

const test = base.extend<{ connectionPage: ConnectionPage; dashboardPage: DashboardPage; projectPage: ProjectPage }>({
const test = base.extend<{
connectionPage: ConnectionPage;
dashboardPage: DashboardPage;
projectPage: ProjectPage;
rateLimitHandler: RateLimitHandler;
}>({
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
Expand All @@ -13,5 +19,8 @@ const test = base.extend<{ connectionPage: ConnectionPage; dashboardPage: Dashbo
projectPage: async ({ page }, use) => {
await use(new ProjectPage(page));
},
rateLimitHandler: async ({ page }, use) => {
await use(new RateLimitHandler(page));
},
});
export { expect, test };
53 changes: 53 additions & 0 deletions e2e/pages/basePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { type Locator, type Page } from "@playwright/test";

import { RateLimitHandler } from "../utils";

export abstract class BasePage {
protected readonly page: Page;
protected readonly rateLimitHandler: RateLimitHandler;

constructor(page: Page) {
this.page = page;
this.rateLimitHandler = new RateLimitHandler(page);
}

async goto(url: string, waitTimeMinutes: number = 0.1): Promise<void> {
await this.rateLimitHandler.goto(url, waitTimeMinutes);
}

async click(selector: string, waitTimeMinutes: number = 0.1): Promise<void> {
await this.rateLimitHandler.click(selector, waitTimeMinutes);
}

async fill(selector: string, value: string, waitTimeMinutes: number = 0.1): Promise<void> {
await this.rateLimitHandler.fill(selector, value, waitTimeMinutes);
}

async hover(selector: string, waitTimeMinutes: number = 0.1): Promise<void> {
await this.rateLimitHandler.hover(selector, waitTimeMinutes);
}

async checkRateLimit(waitTimeMinutes: number = 0.1): Promise<void> {
await this.rateLimitHandler.checkAndHandleRateLimit(waitTimeMinutes);
}

protected getByRole(role: Parameters<Page["getByRole"]>[0], options?: Parameters<Page["getByRole"]>[1]): Locator {
return this.page.getByRole(role, options);
}

protected getByTestId(testId: string): Locator {
return this.page.getByTestId(testId);
}

protected getByPlaceholder(placeholder: string): Locator {
return this.page.getByPlaceholder(placeholder);
}

protected getByText(text: string): Locator {
return this.page.getByText(text);
}

protected locator(selector: string): Locator {
return this.page.locator(selector);
}
}
5 changes: 2 additions & 3 deletions e2e/pages/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ export class ConnectionPage extends DashboardPage {
async startCreateConnection(connectionName: string, connectionType: string) {
await this.createProjectFromMenu();

await this.getByRole("tab", { name: "Connections" }).click();

await this.getByRole("button", { name: "Add new" }).click();
await this.click('tab:has-text("Connections")');
await this.click('button:has-text("Add new")');

const nameInput = this.getByRole("textbox", { exact: true, name: "Name" });
await nameInput.click();
Expand Down
53 changes: 17 additions & 36 deletions e2e/pages/dashboard.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,23 @@
import { expect, type Locator, type Page } from "@playwright/test";
import { expect, type Page } from "@playwright/test";
import randomatic from "randomatic";

import { BasePage } from "./basePage";
import { waitForLoadingOverlayGone } from "e2e/utils/waitForLoadingOverlayToDisappear";
import { waitForMonacoEditorToLoad } from "e2e/utils/waitForMonacoEditor";

export class DashboardPage {
private readonly createButton: Locator;
private readonly page: Page;

export class DashboardPage extends BasePage {
constructor(page: Page) {
this.page = page;
this.createButton = this.page.locator('nav[aria-label="Main navigation"] button[aria-label="New Project"]');
}

protected getByRole(role: Parameters<Page["getByRole"]>[0], options?: Parameters<Page["getByRole"]>[1]) {
return this.page.getByRole(role, options);
}

protected getByTestId(testId: string) {
return this.page.getByTestId(testId);
}

protected getByPlaceholder(placeholder: string) {
return this.page.getByPlaceholder(placeholder);
}

protected getByText(text: string) {
return this.page.getByText(text);
super(page);
}

async createProjectFromMenu() {
await waitForLoadingOverlayGone(this.page);
await this.page.goto("/");
await this.createButton.hover();
await this.createButton.click();
await this.page.getByRole("button", { name: "Create from Scratch", exact: true }).click();
await this.page.getByPlaceholder("Enter project name").fill(randomatic("Aa", 8));
await this.page.getByRole("button", { name: "Create", exact: true }).click();
await this.goto("/");
await this.hover('nav[aria-label="Main navigation"] button[aria-label="New Project"]');
await this.click('nav[aria-label="Main navigation"] button[aria-label="New Project"]');
await this.click('button:has-text("Create from Scratch")');
await this.fill('input[placeholder="Enter project name"]', randomatic("Aa", 8));
await this.click('button:has-text("Create"):not([disabled])');

await expect(this.page.getByRole("cell", { name: "program.py" })).toBeVisible();
await expect(this.page.getByRole("tab", { name: "PROGRAM.PY" })).toBeVisible();
Expand All @@ -55,15 +36,15 @@ export class DashboardPage {
}

async createProjectFromTemplate(projectName: string) {
await this.page.goto("/");
await this.page.getByLabel("Categories").click();
await this.page.getByRole("option", { name: "Samples" }).click();
await this.goto("/");
await this.click('[aria-label="Categories"]');
await this.click('option:has-text("Samples")');
await this.page.locator("body").click({ position: { x: 0, y: 0 } });
await this.page.getByRole("button", { name: "Create Project From Template: HTTP" }).scrollIntoViewIfNeeded();
await this.page.getByRole("button", { name: "Create Project From Template: HTTP" }).click();
await this.page.getByPlaceholder("Enter project name").fill(projectName);
await this.page.getByRole("button", { name: "Create", exact: true }).click();
await this.page.getByRole("button", { name: "Close AI Chat" }).click();
await this.click('button:has-text("Create Project From Template: HTTP")');
await this.fill('input[placeholder="Enter project name"]', projectName);
await this.click('button:has-text("Create"):not([disabled])');
await this.click('button:has-text("Close AI Chat")');

try {
await this.page.getByRole("button", { name: "Skip the tour", exact: true }).click({ timeout: 2000 });
Expand Down
1 change: 1 addition & 0 deletions e2e/pages/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { BasePage } from "./basePage";
export { ConnectionPage } from "./connection";
export { DashboardPage } from "./dashboard";
export { ProjectPage } from "./project";
17 changes: 8 additions & 9 deletions e2e/pages/project.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { expect } from "@playwright/test";
import type { Page } from "@playwright/test";

import { BasePage } from "./basePage";
import { waitForToast } from "e2e/utils";

export class ProjectPage {
private readonly page: Page;

export class ProjectPage extends BasePage {
constructor(page: Page) {
this.page = page;
super(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();
await this.hover('button[aria-label="Project additional actions"]');
await this.click('button[aria-label="Delete project"]');
await this.click('button[aria-label="Ok"]');
const successToast = await waitForToast(this.page, "Project deletion completed successfully");
await expect(successToast).toBeVisible();

Expand All @@ -38,8 +37,8 @@ export class ProjectPage {
}

async stopDeployment() {
await this.page.locator('button[aria-label="Deployments"]').click();
await this.page.locator('button[aria-label="Deactivate deployment"]').click();
await this.click('button[aria-label="Deployments"]');
await this.click('button[aria-label="Deactivate deployment"]');

const toast = await waitForToast(this.page, "Deployment deactivated successfully");
await expect(toast).toBeVisible();
Expand Down
1 change: 1 addition & 0 deletions e2e/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { waitForToast } from "../utils/waitForToast";
export { RateLimitHandler, checkAndHandleRateLimit } from "../utils/rateLimitHandler";
150 changes: 150 additions & 0 deletions e2e/utils/rateLimitHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/* eslint-disable no-console */
import { expect, type Page } from "@playwright/test";

export class RateLimitHandler {
private readonly page: Page;
private readonly modalSelectors = [
'[data-modal-name="rateLimit"]',
'.modal:has-text("Rate Limit")',
'.modal:has-text("rate limit")',
'div:has(h3:text-matches(".*[Rr]ate.*[Ll]imit.*"))',
];
private readonly retryButtonSelectors = [
'button[aria-label*="Retry"]',
'button[aria-label*="retry"]',
'button:has-text("Retry")',
'button:has-text("retry")',
'button:has-text("Try Again")',
];
private readonly closeButtonSelectors = [
'button[aria-label="Close"]',
'button[aria-label="close"]',
"button.close",
'[data-testid="close-button"]',
];

constructor(page: Page) {
this.page = page;
}

async isRateLimitModalVisible(): Promise<boolean> {
try {
for (const selector of this.modalSelectors) {
const modal = this.page.locator(selector);
const isVisible = await modal.isVisible({ timeout: 1000 });
if (isVisible) {
return true;
}
}
return false;
} catch {
return false;
}
}

async handleRateLimitModal(waitTimeMinutes: number = 0.1): Promise<boolean> {
const isModalVisible = await this.isRateLimitModalVisible();

if (!isModalVisible) {
return false;
}

console.log(`Rate limit modal detected. Waiting ${waitTimeMinutes} minute(s) before retrying...`);

await this.page.waitForTimeout(waitTimeMinutes * 60 * 1000);

let buttonClicked = false;

for (const selector of this.retryButtonSelectors) {
try {
const retryButton = this.page.locator(selector).first();
if (await retryButton.isVisible({ timeout: 2000 })) {
await retryButton.click({ timeout: 5000 });
console.log(`Clicked retry button on rate limit modal using selector: ${selector}`);
buttonClicked = true;
break;
}
} catch {
continue;
}
}

if (!buttonClicked) {
console.log("Could not click retry button, trying to close modal manually");

for (const selector of this.closeButtonSelectors) {
try {
const closeButton = this.page.locator(selector).first();
if (await closeButton.isVisible({ timeout: 2000 })) {
await closeButton.click({ timeout: 5000 });
console.log(`Clicked close button using selector: ${selector}`);
buttonClicked = true;
break;
}
} catch {
continue;
}
}

if (!buttonClicked) {
console.log("Using Escape key as fallback to close modal");
await this.page.keyboard.press("Escape");
}
}

await this.page.waitForTimeout(2000);

let modalClosed = false;
for (const selector of this.modalSelectors) {
try {
await expect(this.page.locator(selector)).not.toBeVisible({ timeout: 6000 });
modalClosed = true;
break;
} catch {
continue;
}
}

if (!modalClosed) {
console.log("Warning: Could not verify that rate limit modal was closed");
}

console.log("Rate limit modal handled successfully");
return true;
}

async checkAndHandleRateLimit(waitTimeMinutes: number = 0.1): Promise<void> {
await this.page.waitForTimeout(500);

const handled = await this.handleRateLimitModal(waitTimeMinutes);

if (handled) {
await this.page.waitForTimeout(2000);
}
}

async goto(url: string, waitTimeMinutes: number = 0.1): Promise<void> {
await this.page.goto(url);
await this.checkAndHandleRateLimit(waitTimeMinutes);
}

async click(selector: string, waitTimeMinutes: number = 0.1): Promise<void> {
await this.page.locator(selector).click();
await this.checkAndHandleRateLimit(waitTimeMinutes);
}

async fill(selector: string, value: string, waitTimeMinutes: number = 0.1): Promise<void> {
await this.page.locator(selector).fill(value);
await this.checkAndHandleRateLimit(waitTimeMinutes);
}

async hover(selector: string, waitTimeMinutes: number = 0.1): Promise<void> {
await this.page.locator(selector).hover();
await this.checkAndHandleRateLimit(waitTimeMinutes);
}
}

export async function checkAndHandleRateLimit(page: Page, waitTimeMinutes: number = 0.1): Promise<void> {
const handler = new RateLimitHandler(page);
await handler.checkAndHandleRateLimit(waitTimeMinutes);
}
Loading