How to Handle Windows in Selenium with PHP
Switch between tabs and windows reliably in Selenium for PHP: compare handles, wait for new contexts, and return safely to the original page.
Selenium treats browser tabs and windows as the same kind of browsing context. In PHP, save the current handle with $driver->getWindowHandle(), get the available handles with $driver->getWindowHandles(), and switch with $driver->switchTo()->window($handle). To identify a newly opened context reliably, compare the handle lists from before and after the action; do not assume the new handle is last. Selenium’s windows and tabs documentation explains the shared handle model.
Install the PHP WebDriver client
The examples below use php-webdriver/php-webdriver, installed as the Composer package php-webdriver/webdriver. A WebDriver server or browser driver must also be running and reachable by the PHP client. Check the package’s current requirements and compatibility guidance for your PHP, browser, driver, and Selenium Server versions before choosing versions; the project README documents the binding and setup context.
composer require php-webdriver/webdriver
The following session setup is representative for a local ChromeDriver. Adapt the server address and capabilities to your environment:
<?php
require __DIR__ . '/vendor/autoload.php';
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
$serverUrl = 'http://localhost:9515'; // ChromeDriver address
$options = new ChromeOptions();
$options->addArguments(['--headless=new', '--no-sandbox']);
$capabilities = DesiredCapabilities::chrome();
$capabilities->setCapability(ChromeOptions::CAPABILITY, $options);
$driver = RemoteWebDriver::create($serverUrl, $capabilities, 5000, 120000);
try {
$driver->get('https://example.com');
// Run a browser test here.
} finally {
$driver->quit();
}
If using Selenium Server, point $serverUrl at that server and configure capabilities for the browser node. Session creation differs across Selenium deployments, but the window-handle operations below are the same.
Switch to a newly opened tab or window
Save both the original handle and the existing handle set before clicking a link or triggering JavaScript that opens another context. Wait for the handle set to change, find the new handle by set difference, and switch explicitly.
<?php
use Facebook\WebDriver\Remote\RemoteWebDriver;
$driver->get('https://example.com');
$originalHandle = $driver->getWindowHandle();
$handlesBefore = $driver->getWindowHandles();
// Example: replace this selector with the link that opens a new context.
$driver->findElement(Facebook\WebDriver\WebDriverBy::cssSelector('a[target="_blank"]'))->click();
$driver->wait(10, 250)->until(function (RemoteWebDriver $driver) use ($handlesBefore) {
return count($driver->getWindowHandles()) > count($handlesBefore);
});
$handlesAfter = $driver->getWindowHandles();
$newHandles = array_values(array_diff($handlesAfter, $handlesBefore));
if (count($newHandles) !== 1) {
throw new RuntimeException('Expected exactly one newly opened window or tab.');
}
$driver->switchTo()->window($newHandles[0]);
// Assert destination state before interacting with the new page.
if (strpos($driver->getCurrentURL(), 'expected.example') === false) {
throw new RuntimeException('The new context did not open the expected destination.');
}
$driver->close();
$driver->switchTo()->window($originalHandle);
// Continue on the original page.
The wait confirms that another handle exists, but it does not guarantee the destination has finished loading. After switching, wait for a page-specific condition such as the expected URL, title, or element before interacting. If the action may open multiple contexts, do not require exactly one new handle: inspect each new context for application-specific evidence such as its URL or title.
Choosing the right handle and returning to the original page
getWindowHandles() gives the handles available to the session. Handle ordering is not a reliable indication of opening order. Use array_diff($handlesAfter, $handlesBefore) to identify additions. For multiple new contexts, switch through the candidates and match a known destination or page state.
<?php
$originalHandle = $driver->getWindowHandle();
$before = $driver->getWindowHandles();
// Trigger behavior that may create one or more tabs/windows.
$driver->wait(10, 250)->until(function ($driver) use ($before) {
return count(array_diff($driver->getWindowHandles(), $before)) > 0;
});
$created = array_values(array_diff($driver->getWindowHandles(), $before));
$matchingHandle = null;
foreach ($created as $handle) {
$driver->switchTo()->window($handle);
if (strpos($driver->getCurrentURL(), 'reports.example') !== false) {
$matchingHandle = $handle;
break;
}
}
if ($matchingHandle === null) {
throw new RuntimeException('No newly opened context matched the expected page.');
}
// Work in the matching context, then close it if appropriate.
$driver->close();
$remaining = $driver->getWindowHandles();
if (in_array($originalHandle, $remaining, true)) {
$driver->switchTo()->window($originalHandle);
} elseif (count($remaining) > 0) {
$driver->switchTo()->window($remaining[0]);
} else {
throw new RuntimeException('No browsing contexts remain in this session.');
}
If the original context itself may have been closed or replaced, check that its handle remains in the current handle list before switching back. A stale handle is not a usable destination.
Close one context or end the session
$driver->close()closes the currently selected tab or window. Afterward, switch to a handle that remains open before sending more browser commands.$driver->quit()closes all associated windows and ends the WebDriver session. Use it for test teardown, usually in afinallyblock.
Leaving the driver selected on a context that was just closed can cause a No Such Window error. Save a valid handle and switch to it after closing a secondary context.
Wait for the context and the page
There are two separate synchronization points: the browser must expose the new handle, and the page in that context must reach the state your test needs. Poll the handle set first, then switch, then wait for a page condition. Keep waits bounded so a blocked popup or failed navigation produces a clear test failure instead of hanging indefinitely.
<?php
$before = $driver->getWindowHandles();
// Trigger the new tab/window.
try {
$driver->wait(10, 200)->until(function ($driver) use ($before) {
return count(array_diff($driver->getWindowHandles(), $before)) > 0;
});
} catch (Facebook\WebDriver\Exception\TimeoutException $e) {
throw new RuntimeException('No new browser context appeared within 10 seconds.', 0, $e);
}
$newHandles = array_values(array_diff($driver->getWindowHandles(), $before));
$driver->switchTo()->window($newHandles[0]);
try {
$driver->wait(10, 200)->until(function ($driver) {
return strpos($driver->getCurrentURL(), 'expected.example') !== false;
});
} catch (Facebook\WebDriver\Exception\TimeoutException $e) {
throw new RuntimeException('The new context appeared, but its URL did not become expected.', 0, $e);
}
Use the condition that expresses the application’s readiness. A URL change may happen before client-side content is ready; an element wait may be more appropriate for a single-page application.
Or skip the browser setup
If the goal is to capture a page image or PDF rather than test browser behavior, ScreenshotNeo provides a one-request screenshot API. The PHP example uses cURL, and the ScreenshotNeo API docs cover request options.
<?php
$url = 'https://stripe.com';
$query = http_build_query([
'access_key' => 'YOUR_API_KEY',
'url' => $url,
]);
$ch = curl_init('https://api.screenshotneo.com/v1/shot?' . $query);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 90,
]);
$image = curl_exec($ch);
if ($image === false) {
throw new RuntimeException('Screenshot request failed: ' . curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status < 200 || $status >= 300) {
throw new RuntimeException('Screenshot API returned HTTP ' . $status);
}
file_put_contents('shot.webp', $image);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.
Equivalent requests with cURL, Python, and Node.js
These examples are for ScreenshotNeo’s page-capture API. They capture a page image; they do not switch Selenium’s active browser context.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
API keys should be supplied through environment or secret storage in production, rather than committed to source control. See the API documentation for options and response details.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No new handle appears before timeout | The click did not open a context, a popup was blocked, or the action has not completed. | Confirm the test triggered the intended behavior in the same browser session; check the application’s popup behavior; keep a bounded wait and report the failure clearly. |
| The test switches to the wrong page | It assumed handle order matched creation order, or multiple contexts opened. | Compare old and new handle sets, then choose by URL, title, or a page element. |
| No Such Window after closing a tab | The driver remained selected on a context that no longer exists. | Switch to a saved handle that is still returned by getWindowHandles(). |
| Timeout waiting for page content | The context appeared but navigation or client-side rendering did not reach the expected state. | Check the destination URL and application state separately; wait on a meaningful page condition and inspect navigation errors. |
| Commands affect the original page | The test never switched, or switched back earlier than intended. | Switch explicitly to the discovered handle before interacting and verify the current URL or title. |
| All windows disappear | quit() ended the entire session. |
Use close() to close just the selected context; reserve quit() for teardown. |
Reliability, performance, and cost notes
- Reliability: Handle-set comparison avoids dependence on array ordering. If several contexts can open, match a destination condition and verify the context is still open before switching back.
- Synchronization: Polling for handle changes and then page readiness makes timing explicit. Keep each wait bounded and choose polling intervals that do not flood a remote WebDriver server.
- Performance: Opening extra contexts and waiting for full navigations adds browser and network work. Close temporary contexts when finished and use the smallest page condition that proves readiness.
- Cost: Selenium itself does not establish a universal per-capture cost; infrastructure, browser sessions, and test execution are deployment-specific. ScreenshotNeo’s stated plans are Free at 1,000 shots/month, Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. Only clean shots are billed; cache hits and failed or unsuitable captures cost nothing.
FAQ
Does Selenium use a different API for tabs and windows?
No. Both are addressed as window handles through WebDriver.
Can I get the parent handle after opening a tab?
Store it before opening the new context. Selenium does not define a separate parent-window lookup in this workflow.
Should I use the last item in the handle array?
No. Handle ordering is not guaranteed to represent opening order. Compare before and after sets and identify the candidate by page state.
When should I use ScreenshotNeo instead of Selenium?
Use Selenium when you need to exercise browser interactions and assert application behavior. Use ScreenshotNeo when you need a screenshot or PDF without managing a browser session.


