mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 17:22:38 +02:00
289 lines
8.4 KiB
Markdown
289 lines
8.4 KiB
Markdown
|
|
# Multi-Tab, Window & Popup Testing
|
||
|
|
|
||
|
|
This file covers **single-user scenarios** with multiple browser tabs, windows, and popups. For **multi-user collaboration testing** (multiple users interacting simultaneously), see [multi-user.md](multi-user.md).
|
||
|
|
|
||
|
|
## Table of Contents
|
||
|
|
|
||
|
|
1. [Popup Handling](#popup-handling)
|
||
|
|
2. [New Tab Navigation](#new-tab-navigation)
|
||
|
|
3. [OAuth Flows](#oauth-flows)
|
||
|
|
4. [Multiple Windows](#multiple-windows)
|
||
|
|
5. [Tab Coordination](#tab-coordination)
|
||
|
|
|
||
|
|
## Popup Handling
|
||
|
|
|
||
|
|
### Basic Popup
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
test("handle popup window", async ({ page }) => {
|
||
|
|
await page.goto("/");
|
||
|
|
|
||
|
|
// Start waiting for popup before triggering it
|
||
|
|
const popupPromise = page.waitForEvent("popup");
|
||
|
|
await page.getByRole("button", { name: "Open Support Chat" }).click();
|
||
|
|
const popup = await popupPromise;
|
||
|
|
|
||
|
|
// Wait for popup to load
|
||
|
|
await popup.waitForLoadState();
|
||
|
|
|
||
|
|
// Interact with popup
|
||
|
|
await popup.getByLabel("Message").fill("Need help");
|
||
|
|
await popup.getByRole("button", { name: "Send" }).click();
|
||
|
|
|
||
|
|
await expect(popup.getByText("Message sent")).toBeVisible();
|
||
|
|
|
||
|
|
// Close popup
|
||
|
|
await popup.close();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Popup with Authentication
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
test("popup login flow", async ({ page }) => {
|
||
|
|
await page.goto("/dashboard");
|
||
|
|
|
||
|
|
const popupPromise = page.waitForEvent("popup");
|
||
|
|
await page.getByRole("button", { name: "Connect Account" }).click();
|
||
|
|
const popup = await popupPromise;
|
||
|
|
|
||
|
|
await popup.waitForLoadState();
|
||
|
|
|
||
|
|
// Complete login in popup
|
||
|
|
await popup.getByLabel("Email").fill("user@example.com");
|
||
|
|
await popup.getByLabel("Password").fill("password123");
|
||
|
|
await popup.getByRole("button", { name: "Log In" }).click();
|
||
|
|
|
||
|
|
// Popup should close automatically after auth
|
||
|
|
await popup.waitForEvent("close");
|
||
|
|
|
||
|
|
// Main page should reflect connected state
|
||
|
|
await expect(page.getByText("Account connected")).toBeVisible();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Handle Blocked Popups
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
test("handle popup blocker", async ({ page }) => {
|
||
|
|
await page.goto("/share");
|
||
|
|
|
||
|
|
// Listen for console messages about blocked popup
|
||
|
|
page.on("console", (msg) => {
|
||
|
|
if (msg.text().includes("popup blocked")) {
|
||
|
|
console.log("Popup was blocked");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const popupPromise = page.waitForEvent("popup").catch(() => null);
|
||
|
|
await page.getByRole("button", { name: "Share to Twitter" }).click();
|
||
|
|
const popup = await popupPromise;
|
||
|
|
|
||
|
|
if (!popup) {
|
||
|
|
// Popup blocked - app should show fallback
|
||
|
|
await expect(page.getByText("Copy share link instead")).toBeVisible();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## New Tab Navigation
|
||
|
|
|
||
|
|
### Link Opens in New Tab
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
test("external link opens in new tab", async ({ page, context }) => {
|
||
|
|
await page.goto("/resources");
|
||
|
|
|
||
|
|
// Wait for new page in context
|
||
|
|
const pagePromise = context.waitForEvent("page");
|
||
|
|
await page.getByRole("link", { name: "Documentation" }).click();
|
||
|
|
const newPage = await pagePromise;
|
||
|
|
|
||
|
|
await newPage.waitForLoadState();
|
||
|
|
|
||
|
|
expect(newPage.url()).toContain("docs.example.com");
|
||
|
|
await expect(newPage.getByRole("heading", { level: 1 })).toBeVisible();
|
||
|
|
|
||
|
|
// Original page still there
|
||
|
|
expect(page.url()).toContain("/resources");
|
||
|
|
|
||
|
|
await newPage.close();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Intercept New Tab
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
test("prevent new tab for testing", async ({ page }) => {
|
||
|
|
await page.goto("/links");
|
||
|
|
|
||
|
|
// Remove target="_blank" to keep navigation in same tab
|
||
|
|
await page.evaluate(() => {
|
||
|
|
document.querySelectorAll('a[target="_blank"]').forEach((a) => {
|
||
|
|
a.removeAttribute("target");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Now link opens in same tab
|
||
|
|
await page.getByRole("link", { name: "External Site" }).click();
|
||
|
|
|
||
|
|
// Can test the destination page
|
||
|
|
await expect(page).toHaveURL(/external-site\.com/);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## OAuth Flows
|
||
|
|
|
||
|
|
### Google OAuth Popup
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
test("Google OAuth login", async ({ page }) => {
|
||
|
|
await page.goto("/login");
|
||
|
|
|
||
|
|
const popupPromise = page.waitForEvent("popup");
|
||
|
|
await page.getByRole("button", { name: "Sign in with Google" }).click();
|
||
|
|
const popup = await popupPromise;
|
||
|
|
|
||
|
|
await popup.waitForLoadState();
|
||
|
|
|
||
|
|
// Handle Google's OAuth flow
|
||
|
|
await popup.getByLabel("Email or phone").fill("test@gmail.com");
|
||
|
|
await popup.getByRole("button", { name: "Next" }).click();
|
||
|
|
|
||
|
|
await popup.getByLabel("Enter your password").fill("password");
|
||
|
|
await popup.getByRole("button", { name: "Next" }).click();
|
||
|
|
|
||
|
|
// Wait for redirect back and popup close
|
||
|
|
await popup.waitForEvent("close");
|
||
|
|
|
||
|
|
// Verify logged in on main page
|
||
|
|
await expect(page.getByText("Welcome, Test User")).toBeVisible();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Mock OAuth (Recommended)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
test("mock OAuth flow", async ({ page, context }) => {
|
||
|
|
// Mock the OAuth callback instead of real flow
|
||
|
|
await page.route("**/auth/callback**", async (route) => {
|
||
|
|
// Simulate successful OAuth
|
||
|
|
const url = new URL(route.request().url());
|
||
|
|
url.searchParams.set("code", "mock-auth-code");
|
||
|
|
await route.fulfill({
|
||
|
|
status: 302,
|
||
|
|
headers: { Location: "/dashboard" },
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Mock token exchange
|
||
|
|
await page.route("**/api/auth/token", (route) =>
|
||
|
|
route.fulfill({
|
||
|
|
json: {
|
||
|
|
access_token: "mock-token",
|
||
|
|
user: { name: "Test User", email: "test@example.com" },
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
await page.goto("/login");
|
||
|
|
await page.getByRole("button", { name: "Sign in with Google" }).click();
|
||
|
|
|
||
|
|
// Should redirect to dashboard without actual OAuth
|
||
|
|
await expect(page).toHaveURL("/dashboard");
|
||
|
|
await expect(page.getByText("Welcome, Test User")).toBeVisible();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### OAuth Fixture
|
||
|
|
|
||
|
|
> **For comprehensive OAuth mocking patterns** (fixtures, multiple providers, SAML SSO), see [third-party.md](third-party.md#oauthsso-mocking). This section focuses on popup window handling mechanics for OAuth flows.
|
||
|
|
|
||
|
|
## Multiple Windows
|
||
|
|
|
||
|
|
### Test Across Multiple Windows
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
test("sync between windows", async ({ context }) => {
|
||
|
|
// Open two pages
|
||
|
|
const page1 = await context.newPage();
|
||
|
|
const page2 = await context.newPage();
|
||
|
|
|
||
|
|
await page1.goto("/dashboard");
|
||
|
|
await page2.goto("/dashboard");
|
||
|
|
|
||
|
|
// Make change in first window
|
||
|
|
await page1.getByRole("button", { name: "Add Item" }).click();
|
||
|
|
await page1.getByLabel("Name").fill("New Item");
|
||
|
|
await page1.getByRole("button", { name: "Save" }).click();
|
||
|
|
|
||
|
|
// Should sync to second window (if app supports real-time sync)
|
||
|
|
await expect(page2.getByText("New Item")).toBeVisible({ timeout: 10000 });
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Different Users in Different Windows
|
||
|
|
|
||
|
|
> **For multi-user collaboration patterns** (admin/user interactions, real-time collaboration, role-based testing, concurrent actions), see [multi-user.md](multi-user.md). This file focuses on single-user scenarios with multiple tabs/windows/popups.
|
||
|
|
|
||
|
|
## Tab Coordination
|
||
|
|
|
||
|
|
### Switch Between Tabs
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
test("manage multiple tabs", async ({ context }) => {
|
||
|
|
const page1 = await context.newPage();
|
||
|
|
await page1.goto("/editor");
|
||
|
|
|
||
|
|
const page2 = await context.newPage();
|
||
|
|
await page2.goto("/preview");
|
||
|
|
|
||
|
|
// Edit in first tab
|
||
|
|
await page1.bringToFront();
|
||
|
|
await page1.getByLabel("Content").fill("Hello World");
|
||
|
|
|
||
|
|
// Check preview in second tab
|
||
|
|
await page2.bringToFront();
|
||
|
|
await page2.reload(); // If preview needs refresh
|
||
|
|
await expect(page2.getByText("Hello World")).toBeVisible();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Close All Tabs Except One
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
test("cleanup tabs after test", async ({ context }) => {
|
||
|
|
const mainPage = await context.newPage();
|
||
|
|
await mainPage.goto("/");
|
||
|
|
|
||
|
|
// Open several popups during test
|
||
|
|
for (let i = 0; i < 3; i++) {
|
||
|
|
const popup = await context.newPage();
|
||
|
|
await popup.goto(`/popup/${i}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Close all except main page
|
||
|
|
for (const page of context.pages()) {
|
||
|
|
if (page !== mainPage) {
|
||
|
|
await page.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
expect(context.pages()).toHaveLength(1);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Anti-Patterns to Avoid
|
||
|
|
|
||
|
|
| Anti-Pattern | Problem | Solution |
|
||
|
|
| ----------------------- | ------------------------------ | ------------------------------------------ |
|
||
|
|
| Not waiting for popup | Race condition | Use `waitForEvent("popup")` before trigger |
|
||
|
|
| Testing real OAuth | Slow, flaky, needs credentials | Mock OAuth endpoints |
|
||
|
|
| Assuming popup opens | May be blocked | Handle both open and blocked cases |
|
||
|
|
| Not closing extra pages | Resource leak | Close pages in cleanup |
|
||
|
|
|
||
|
|
## Related References
|
||
|
|
|
||
|
|
- **Authentication**: See [fixtures-hooks.md](../core/fixtures-hooks.md) for auth patterns
|
||
|
|
- **Network**: See [network-advanced.md](network-advanced.md) for mocking OAuth
|