Skip to content

Commit 31ef948

Browse files
committed
test: add rate-limiter handler
1 parent cc6263f commit 31ef948

File tree

8 files changed

+242
-49
lines changed

8 files changed

+242
-49
lines changed

e2e/fixtures.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
import { expect, test as base } from "@playwright/test";
33

44
import { ConnectionPage, DashboardPage, ProjectPage } from "./pages";
5+
import { RateLimitHandler } from "./utils";
56

6-
const test = base.extend<{ connectionPage: ConnectionPage; dashboardPage: DashboardPage; projectPage: ProjectPage }>({
7+
const test = base.extend<{
8+
connectionPage: ConnectionPage;
9+
dashboardPage: DashboardPage;
10+
projectPage: ProjectPage;
11+
rateLimitHandler: RateLimitHandler;
12+
}>({
713
dashboardPage: async ({ page }, use) => {
814
await use(new DashboardPage(page));
915
},
@@ -13,5 +19,8 @@ const test = base.extend<{ connectionPage: ConnectionPage; dashboardPage: Dashbo
1319
projectPage: async ({ page }, use) => {
1420
await use(new ProjectPage(page));
1521
},
22+
rateLimitHandler: async ({ page }, use) => {
23+
await use(new RateLimitHandler(page));
24+
},
1625
});
1726
export { expect, test };

e2e/pages/basePage.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { type Locator, type Page } from "@playwright/test";
2+
3+
import { RateLimitHandler } from "../utils";
4+
5+
export abstract class BasePage {
6+
protected readonly page: Page;
7+
protected readonly rateLimitHandler: RateLimitHandler;
8+
9+
constructor(page: Page) {
10+
this.page = page;
11+
this.rateLimitHandler = new RateLimitHandler(page);
12+
}
13+
14+
async goto(url: string, waitTimeMinutes: number = 0.1): Promise<void> {
15+
await this.rateLimitHandler.goto(url, waitTimeMinutes);
16+
}
17+
18+
async click(selector: string, waitTimeMinutes: number = 0.1): Promise<void> {
19+
await this.rateLimitHandler.click(selector, waitTimeMinutes);
20+
}
21+
22+
async fill(selector: string, value: string, waitTimeMinutes: number = 0.1): Promise<void> {
23+
await this.rateLimitHandler.fill(selector, value, waitTimeMinutes);
24+
}
25+
26+
async hover(selector: string, waitTimeMinutes: number = 0.1): Promise<void> {
27+
await this.rateLimitHandler.hover(selector, waitTimeMinutes);
28+
}
29+
30+
async checkRateLimit(waitTimeMinutes: number = 0.1): Promise<void> {
31+
await this.rateLimitHandler.checkAndHandleRateLimit(waitTimeMinutes);
32+
}
33+
34+
protected getByRole(role: Parameters<Page["getByRole"]>[0], options?: Parameters<Page["getByRole"]>[1]): Locator {
35+
return this.page.getByRole(role, options);
36+
}
37+
38+
protected getByTestId(testId: string): Locator {
39+
return this.page.getByTestId(testId);
40+
}
41+
42+
protected getByPlaceholder(placeholder: string): Locator {
43+
return this.page.getByPlaceholder(placeholder);
44+
}
45+
46+
protected getByText(text: string): Locator {
47+
return this.page.getByText(text);
48+
}
49+
50+
protected locator(selector: string): Locator {
51+
return this.page.locator(selector);
52+
}
53+
}

e2e/pages/connection.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ export class ConnectionPage extends DashboardPage {
44
async startCreateConnection(connectionName: string, connectionType: string) {
55
await this.createProjectFromMenu();
66

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

1110
const nameInput = this.getByRole("textbox", { exact: true, name: "Name" });
1211
await nameInput.click();

e2e/pages/dashboard.ts

Lines changed: 17 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,23 @@
1-
import { expect, type Locator, type Page } from "@playwright/test";
1+
import { expect, type Page } from "@playwright/test";
22
import randomatic from "randomatic";
33

4+
import { BasePage } from "./basePage";
45
import { waitForLoadingOverlayGone } from "e2e/utils/waitForLoadingOverlayToDisappear";
56
import { waitForMonacoEditorToLoad } from "e2e/utils/waitForMonacoEditor";
67

7-
export class DashboardPage {
8-
private readonly createButton: Locator;
9-
private readonly page: Page;
10-
8+
export class DashboardPage extends BasePage {
119
constructor(page: Page) {
12-
this.page = page;
13-
this.createButton = this.page.locator('nav[aria-label="Main navigation"] button[aria-label="New Project"]');
14-
}
15-
16-
protected getByRole(role: Parameters<Page["getByRole"]>[0], options?: Parameters<Page["getByRole"]>[1]) {
17-
return this.page.getByRole(role, options);
18-
}
19-
20-
protected getByTestId(testId: string) {
21-
return this.page.getByTestId(testId);
22-
}
23-
24-
protected getByPlaceholder(placeholder: string) {
25-
return this.page.getByPlaceholder(placeholder);
26-
}
27-
28-
protected getByText(text: string) {
29-
return this.page.getByText(text);
10+
super(page);
3011
}
3112

3213
async createProjectFromMenu() {
3314
await waitForLoadingOverlayGone(this.page);
34-
await this.page.goto("/");
35-
await this.createButton.hover();
36-
await this.createButton.click();
37-
await this.page.getByRole("button", { name: "Create from Scratch", exact: true }).click();
38-
await this.page.getByPlaceholder("Enter project name").fill(randomatic("Aa", 8));
39-
await this.page.getByRole("button", { name: "Create", exact: true }).click();
15+
await this.goto("/");
16+
await this.hover('nav[aria-label="Main navigation"] button[aria-label="New Project"]');
17+
await this.click('nav[aria-label="Main navigation"] button[aria-label="New Project"]');
18+
await this.click('button:has-text("Create from Scratch")');
19+
await this.fill('input[placeholder="Enter project name"]', randomatic("Aa", 8));
20+
await this.click('button:has-text("Create"):not([disabled])');
4021

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

5738
async createProjectFromTemplate(projectName: string) {
58-
await this.page.goto("/");
59-
await this.page.getByLabel("Categories").click();
60-
await this.page.getByRole("option", { name: "Samples" }).click();
39+
await this.goto("/");
40+
await this.click('[aria-label="Categories"]');
41+
await this.click('option:has-text("Samples")');
6142
await this.page.locator("body").click({ position: { x: 0, y: 0 } });
6243
await this.page.getByRole("button", { name: "Create Project From Template: HTTP" }).scrollIntoViewIfNeeded();
63-
await this.page.getByRole("button", { name: "Create Project From Template: HTTP" }).click();
64-
await this.page.getByPlaceholder("Enter project name").fill(projectName);
65-
await this.page.getByRole("button", { name: "Create", exact: true }).click();
66-
await this.page.getByRole("button", { name: "Close AI Chat" }).click();
44+
await this.click('button:has-text("Create Project From Template: HTTP")');
45+
await this.fill('input[placeholder="Enter project name"]', projectName);
46+
await this.click('button:has-text("Create"):not([disabled])');
47+
await this.click('button:has-text("Close AI Chat")');
6748

6849
try {
6950
await this.page.getByRole("button", { name: "Skip the tour", exact: true }).click({ timeout: 2000 });

e2e/pages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { BasePage } from "./basePage";
12
export { ConnectionPage } from "./connection";
23
export { DashboardPage } from "./dashboard";
34
export { ProjectPage } from "./project";

e2e/pages/project.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import { expect } from "@playwright/test";
22
import type { Page } from "@playwright/test";
33

4+
import { BasePage } from "./basePage";
45
import { waitForToast } from "e2e/utils";
56

6-
export class ProjectPage {
7-
private readonly page: Page;
8-
7+
export class ProjectPage extends BasePage {
98
constructor(page: Page) {
10-
this.page = page;
9+
super(page);
1110
}
1211

1312
async deleteProject(projectName: string) {
14-
await this.page.locator('button[aria-label="Project additional actions"]').hover();
15-
await this.page.locator('button[aria-label="Delete project"]').click();
16-
await this.page.locator('button[aria-label="Ok"]').click();
13+
await this.hover('button[aria-label="Project additional actions"]');
14+
await this.click('button[aria-label="Delete project"]');
15+
await this.click('button[aria-label="Ok"]');
1716
const successToast = await waitForToast(this.page, "Project deletion completed successfully");
1817
await expect(successToast).toBeVisible();
1918

@@ -38,8 +37,8 @@ export class ProjectPage {
3837
}
3938

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

4443
const toast = await waitForToast(this.page, "Deployment deactivated successfully");
4544
await expect(toast).toBeVisible();

e2e/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { waitForToast } from "../utils/waitForToast";
2+
export { RateLimitHandler, checkAndHandleRateLimit } from "../utils/rateLimitHandler";

e2e/utils/rateLimitHandler.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/* eslint-disable no-console */
2+
import { expect, type Page } from "@playwright/test";
3+
4+
export class RateLimitHandler {
5+
private readonly page: Page;
6+
private readonly modalSelectors = [
7+
'[data-modal-name="rateLimit"]',
8+
'.modal:has-text("Rate Limit")',
9+
'.modal:has-text("rate limit")',
10+
'div:has(h3:text-matches(".*[Rr]ate.*[Ll]imit.*"))',
11+
];
12+
private readonly retryButtonSelectors = [
13+
'button[aria-label*="Retry"]',
14+
'button[aria-label*="retry"]',
15+
'button:has-text("Retry")',
16+
'button:has-text("retry")',
17+
'button:has-text("Try Again")',
18+
];
19+
private readonly closeButtonSelectors = [
20+
'button[aria-label="Close"]',
21+
'button[aria-label="close"]',
22+
"button.close",
23+
'[data-testid="close-button"]',
24+
];
25+
26+
constructor(page: Page) {
27+
this.page = page;
28+
}
29+
30+
async isRateLimitModalVisible(): Promise<boolean> {
31+
try {
32+
for (const selector of this.modalSelectors) {
33+
const modal = this.page.locator(selector);
34+
const isVisible = await modal.isVisible({ timeout: 1000 });
35+
if (isVisible) {
36+
return true;
37+
}
38+
}
39+
return false;
40+
} catch {
41+
return false;
42+
}
43+
}
44+
45+
async handleRateLimitModal(waitTimeMinutes: number = 0.1): Promise<boolean> {
46+
const isModalVisible = await this.isRateLimitModalVisible();
47+
48+
if (!isModalVisible) {
49+
return false;
50+
}
51+
52+
console.log(`Rate limit modal detected. Waiting ${waitTimeMinutes} minute(s) before retrying...`);
53+
54+
await this.page.waitForTimeout(waitTimeMinutes * 60 * 1000);
55+
56+
let buttonClicked = false;
57+
58+
for (const selector of this.retryButtonSelectors) {
59+
try {
60+
const retryButton = this.page.locator(selector).first();
61+
if (await retryButton.isVisible({ timeout: 2000 })) {
62+
await retryButton.click({ timeout: 5000 });
63+
console.log(`Clicked retry button on rate limit modal using selector: ${selector}`);
64+
buttonClicked = true;
65+
break;
66+
}
67+
} catch {
68+
continue;
69+
}
70+
}
71+
72+
if (!buttonClicked) {
73+
console.log("Could not click retry button, trying to close modal manually");
74+
75+
for (const selector of this.closeButtonSelectors) {
76+
try {
77+
const closeButton = this.page.locator(selector).first();
78+
if (await closeButton.isVisible({ timeout: 2000 })) {
79+
await closeButton.click({ timeout: 5000 });
80+
console.log(`Clicked close button using selector: ${selector}`);
81+
buttonClicked = true;
82+
break;
83+
}
84+
} catch {
85+
continue;
86+
}
87+
}
88+
89+
if (!buttonClicked) {
90+
console.log("Using Escape key as fallback to close modal");
91+
await this.page.keyboard.press("Escape");
92+
}
93+
}
94+
95+
await this.page.waitForTimeout(2000);
96+
97+
let modalClosed = false;
98+
for (const selector of this.modalSelectors) {
99+
try {
100+
await expect(this.page.locator(selector)).not.toBeVisible({ timeout: 6000 });
101+
modalClosed = true;
102+
break;
103+
} catch {
104+
continue;
105+
}
106+
}
107+
108+
if (!modalClosed) {
109+
console.log("Warning: Could not verify that rate limit modal was closed");
110+
}
111+
112+
console.log("Rate limit modal handled successfully");
113+
return true;
114+
}
115+
116+
async checkAndHandleRateLimit(waitTimeMinutes: number = 0.1): Promise<void> {
117+
await this.page.waitForTimeout(500);
118+
119+
const handled = await this.handleRateLimitModal(waitTimeMinutes);
120+
121+
if (handled) {
122+
await this.page.waitForTimeout(2000);
123+
}
124+
}
125+
126+
async goto(url: string, waitTimeMinutes: number = 0.1): Promise<void> {
127+
await this.page.goto(url);
128+
await this.checkAndHandleRateLimit(waitTimeMinutes);
129+
}
130+
131+
async click(selector: string, waitTimeMinutes: number = 0.1): Promise<void> {
132+
await this.page.locator(selector).click();
133+
await this.checkAndHandleRateLimit(waitTimeMinutes);
134+
}
135+
136+
async fill(selector: string, value: string, waitTimeMinutes: number = 0.1): Promise<void> {
137+
await this.page.locator(selector).fill(value);
138+
await this.checkAndHandleRateLimit(waitTimeMinutes);
139+
}
140+
141+
async hover(selector: string, waitTimeMinutes: number = 0.1): Promise<void> {
142+
await this.page.locator(selector).hover();
143+
await this.checkAndHandleRateLimit(waitTimeMinutes);
144+
}
145+
}
146+
147+
export async function checkAndHandleRateLimit(page: Page, waitTimeMinutes: number = 0.1): Promise<void> {
148+
const handler = new RateLimitHandler(page);
149+
await handler.checkAndHandleRateLimit(waitTimeMinutes);
150+
}

0 commit comments

Comments
 (0)