The Great Page Leak Crisis: When Isolation Becomes Isolation
How testing both sides of a conversation led to discovering Playwright page management nightmare on CI and building a solution that keeps tests clean.
Have you ever tried to test a conversation? Not the kind you have over coffee, but the digital kind—when User A sends a message to User B, and you need to verify that both the sender sees "Message sent ✓" and the recipient gets the actual message with the right notification badge.
This was my Tuesday morning reality: staring at a test suite that needed to verify real-time messaging across different user perspectives. Simple concept, complex implementation.
#The Two-Window Problem
Picture this scenario: You're testing a messaging application where:
- Sender side: User clicks "Send," message shows as delivered, chat history updates, typing indicator disappears
- Recipient side: New message appears, notification badge increments, message timestamp shows correctly, read status tracks properly
The UI for each side is completely different. The sender sees their own chat interface with delivery confirmations, while the recipient sees an incoming message interface with different styling, notifications, and interaction patterns.
As any good QA engineer knows, you can't just test one side and hope the other works. You need both perspectives.
test('user can send and receive messages', async ({ browser }) => {// Sender's perspectiveconst senderPage = await browser.newPage()await senderPage.goto('/chat/sender-view')// ... test sender logic// Recipient's perspectiveconst recipientPage = await browser.newPage()await recipientPage.goto('/chat/recipient-view')// ... test recipient logic})
Looks reasonable, right? Our tests ran beautifully on local development. Green checkmarks everywhere. We shipped with confidence.
Then came Monday morning.
#The CI Catastrophe
Our Jenkins builds started failing. Not the good kind of failing where a test catches a bug—the bad kind where the CI server itself starts choking.
ERROR: Test runner crashed after 45 minutesCAUSE: Out of memorySTATUS: 127 processes still runningBROWSER WINDOWS: 847 open tabs
Eight hundred and forty-seven browser tabs. Our CI server was hosting the world's most expensive browser session.
Here's what was happening: In Playwright's excellent isolation model, each test gets its own browser context. But when you create extra pages via browser.newPage(), those pages don't get cleaned up until the entire test suite finishes. Not individual tests—the whole suite.
So our test suite with 200+ messaging scenarios was creating:
- 200 tests × 2 pages per test = 400+ browser pages
- All staying open simultaneously
- Each consuming memory
- Jenkins eventually running out of RAM
- Tests timing out before they could even start
The cruel irony? Playwright's isolation was too good. Each test was perfectly isolated from others, but the pages themselves were living forever in browser limbo.
#The Local vs CI Paradox
Locally, everything worked fine. Why? Because developers typically run a subset of tests, and our laptops have enough RAM to handle a few dozen browser pages. But in CI, where the full suite runs uninterrupted, those pages accumulate like digital hoarding.
We discovered the pattern across our different test types:
Multi-user workflows: Admin approves, user receives notification (2 pages)
Cross-platform testing: Desktop view vs mobile view (2 pages)
A/B testing verification: Control vs variant experiences (2+ pages)
Real-time collaboration: Multiple users editing simultaneously (3-5 pages)
Each scenario was perfectly valid and necessary. But CI was treating our test suite like a browser tab addiction support group.
#The Cleanup Chaos
Our first attempt was the obvious one: manual cleanup.
test('messaging with manual cleanup', async ({ browser }) => {const senderPage = await browser.newPage()const recipientPage = await browser.newPage()try {// Test logic here...} finally {await senderPage.close()await recipientPage.close()}})
This worked... until it didn't. Tests that threw exceptions before reaching the finally block. Async operations that hung. Network timeouts that left pages in weird states.
We ended up with:
test('messaging with paranoid cleanup', async ({ browser }) => {let senderPage, recipientPagetry {senderPage = await browser.newPage()recipientPage = await browser.newPage()// Test logic...} finally {if (senderPage && !senderPage.isClosed()) {await senderPage.close().catch(console.error)}if (recipientPage && !recipientPage.isClosed()) {await recipientPage.close().catch(console.error)}}})
Multiply this everywhere. Every test became a defensive programming exercise. Our test code was 40% actual testing, 60% memory management ceremony.
#The Jenkins Memory Battle
Meanwhile, our DevOps team was frantically adjusting Jenkins configurations:
# Increase memory allocation (spoiler: didn't help)memory: 8GB → 16GB → 32GB# Reduce parallel workers (slowed everything down)workers: 8 → 4 → 2 → 1# Add aggressive timeouts (killed valid long-running tests)timeout: 30s → 10s
We were treating the symptoms, not the disease. More memory just meant we could accumulate more leaked pages before crashing. Fewer workers meant longer build times. Aggressive timeouts meant false negatives.
The real problem wasn't memory—it was lifecycle management.
#The Eureka Moment
During one particularly frustrating debugging session, I noticed something odd. When I manually closed a browser window in headed mode, the test would suddenly start behaving normally. It wasn't a test logic problem—it was a resource management problem.
That's when it hit me: What if page cleanup was automatic and reliable, just like Playwright's context isolation?
I wanted the same confidence I had with contexts—knowing that every test starts clean and finishes clean, regardless of what happens in between.
#Building the Solution
I spent the next few days building what would become Playwright PageMan. The concept was elegantly simple:
- Track every extra page created during test execution
- Auto-close them after each test via Playwright's fixture lifecycle
- Handle failures gracefully so cleanup always happens
- Make it automatic for the common case (
browser.newPage())
Here's how the same test looks with PageMan:
import { test, expect } from 'playwright-pageman'test('user can send and receive messages', async ({ browser }) => {// This page is automatically tracked!const senderPage = await browser.newPage()await senderPage.goto('/chat/sender-view')// This page is automatically tracked too!const recipientPage = await browser.newPage()await recipientPage.goto('/chat/recipient-view')// ... test both sides of the conversation ...// No cleanup needed! Pages auto-close after the test.})
That's it. No try-finally blocks. No manual cleanup. No defensive programming. Just the test logic that actually matters.
#Auto-Tracking Magic
The secret sauce is automatic tracking. PageMan intercepts browser.newPage() calls and automatically adds them to a cleanup queue. When the test finishes (whether it passes, fails, or times out), Playwright's fixture system ensures all tracked pages get closed.
For the less common cases—pages created via context.newPage() or popup windows—you can manually track them:
test('handle popup messages', async ({ page, context, extraPages }) => {// Open a popup windowconst [popup] = await Promise.all([context.waitForEvent('page'),page.click('#open-conversation-popup'),])// Track it for auto-cleanupextraPages.push(popup)// Test the popup interface...// Popup auto-closes after the test})
#The CI Transformation
After rolling out PageMan, our Jenkins builds went from disaster to delight:
Before PageMan:
- ❌ Builds timing out after 45 minutes
- ❌ Out of memory errors
- ❌ 800+ leaked browser windows
- ❌ Tests failing due to resource exhaustion
- ❌ Manual cleanup everywhere
After PageMan:
- ✅ Reliable 15-minute build times
- ✅ Stable memory usage (under 2GB)
- ✅ Zero leaked pages
- ✅ Tests failing only when they should
- ✅ Clean, focused test code
#Real-World Benefits
The impact went beyond just CI stability:
For QA Engineers:
- Write tests that focus on behavior, not cleanup
- No more defensive
try...finallyeverywhere - Reliable execution in both local and CI environments
For DevOps Teams:
- Predictable resource usage in CI pipelines
- Smaller Jenkins instances (saved actual money)
- No more 3 AM alerts about crashed test runners
For Development Teams:
- Faster feedback loops with stable CI
- More confidence in test results
- Multi-page testing patterns become trivial
#The Global Access Innovation
One of the most requested features came from page object models. Teams wanted to track pages created inside helper functions without passing fixtures around:
// helpers/conversation-helper.tsimport { extraPages } from 'playwright-pageman'export class ConversationHelper {async openNewChatWindow(context: BrowserContext, userId: string) {const page = await context.newPage()extraPages.push(page) // Global access - no fixture passing!await this.loginAsUser(page, userId)return page}}
This made PageMan even more seamless—no architectural changes needed, just better lifecycle management.
#The Open Source Journey
After seeing the transformation in our own testing workflows, I realized this was a universal problem. Every team doing complex end-to-end testing faces the same page management challenges:
- E-commerce sites: Testing both customer and admin interfaces
- Collaboration tools: Multiple users interacting simultaneously
- Real-time applications: Different viewport experiences
- Multi-tenant platforms: Various user role perspectives
So I open-sourced Playwright PageMan with:
- Zero configuration setup (just change your import)
- Auto-tracking enabled by default (handles 80% of cases automatically)
- Manual tracking for special cases (popups, context pages, etc.)
- Configurable timeouts and logging (for debugging edge cases)
- Full TypeScript support (because types prevent bugs)
#Configuration for Different Environments
PageMan adapts to your workflow:
// For local development - see what's happeningtest.use({pageManOptions: {logCleanup: true, // Log cleanup actionscloseTimeout: 5000, // Patient with slow pages},})// For CI - fast and silenttest.use({pageManOptions: {logCleanup: false, // No noise in CI logscloseTimeout: 1000, // Aggressive timeouts},})// For debugging - manual controltest.use({pageManOptions: {autoTrack: false, // Manual tracking only},})
#The Community Response
Since going open source, PageMan has attracted teams with even more creative use cases:
- Visual regression testing: Comparing designs across multiple viewport sizes
- Accessibility testing: Testing screen readers in multiple windows simultaneously
- Load testing: Simulating multiple user sessions from a single test
- Cross-browser workflows: Coordinating actions between different browser instances
The common thread? Every team was fighting the same page lifecycle battle, and PageMan made it disappear.
#Looking Forward
Today, PageMan manages page lifecycles for thousands of tests across dozens of CI environments. What started as a weekend fix for our Jenkins memory crisis has become a tool that makes multi-page testing humane.
The lesson? Sometimes the best tools aren't about adding new capabilities—they're about removing friction from what you already know how to do.
If your CI builds are mysteriously slow, if your Jenkins instances keep running out of memory, if you're writing more cleanup code than test logic—you're not alone. And maybe, just maybe, your test runner is hosting an involuntary browser tab convention too.
#Get Started
Want to stop the page leak crisis in your tests? PageMan takes 30 seconds to install:
npm install playwright-pageman
// Replace this:import { test, expect } from '@playwright/test'// With this:import { test, expect } from 'playwright-pageman'// That's it! Auto-tracking is enabled by default.test('your test', async ({ browser }) => {const page1 = await browser.newPage() // Automatically trackedconst page2 = await browser.newPage() // Also automatically tracked// Both pages auto-close after the test})
No more manual cleanup. No more leaked pages. No more Jenkins tab parties.
Sometimes the smartest thing you can do is let the computer handle the boring stuff, so you can focus on what actually matters—making sure your conversation works from both sides.
Fighting your own page leak crisis? Found PageMan useful for your multi-page testing scenarios? Share your experience or contribute to the project. Let's make testing cleaner, one auto-closed page at a time.