Take Screenshots of Password-Protected Staging Pages with PHP Panther
Use PHP Panther to authenticate to a protected staging page, wait for it to be ready, and save a browser screenshot.
Use Symfony Panther to drive a real Chrome or Firefox browser: open the staging page, complete the authentication flow that the site actually uses, wait for a page-specific ready condition, then call takeScreenshot(). For an already-running staging host, configure external_base_uri. HTTP Basic, a login form, and a third-party identity provider are different flows; there is no single credential recipe that safely applies to all of them.
This guide saves the browser’s current viewport to a PNG. The screenshot dimensions depend on the browser window size; the basic screenshot call should not be treated as a guarantee of a full-page image. Panther supports Chrome and Firefox, JavaScript execution, and screenshot capture through WebDriver. Symfony’s end-to-end testing guide documents the setup and workflow.
1. Install Panther and prepare a browser
Add Panther as a development dependency:
composer require --dev symfony/panther
Panther needs a compatible browser and WebDriver setup for the environment. Choose Chrome or Firefox based on the browser and driver available in your local or CI environment. Keep the browser and driver versions compatible, and confirm that the browser can launch in the environment where the capture runs.
Store the staging URL and credentials in your approved local or CI secret configuration. Do not commit passwords, session cookies, or tokens to source control. Use a staging account with only the access needed for the page being captured.
2. Identify how the staging site authenticates
| Gate | How to recognize it | Panther approach |
|---|---|---|
| HTTP Basic | The server responds with a WWW-Authenticate challenge. |
Authenticate the browser using credentials for that challenge. Browser support and credential injection details can depend on the browser/driver version; verify against your installed setup. |
| Application form | The browser is redirected to a site login page or sees a username/password form. | Load the form, fill the actual fields, submit it, then wait for a page-specific authenticated marker. |
| Reverse proxy or access gateway | A proxy, VPN, or staging gate intercepts the request before the application page. | Use the gateway’s supported browser flow or network access. Application credentials may not satisfy this layer. |
| SSO / identity provider | The browser redirects to an external identity provider or organization login. | Follow the project’s approved test setup for that provider, including any required test account or session bootstrap. Do not assume a normal form login script covers MFA or federated redirects. |
Symfony’s HTTP Basic documentation describes the WWW-Authenticate challenge for HTTP Basic specifically. A form login or SSO flow needs its own browser interaction and configuration. Symfony Security: HTTP Basic and authenticators.
3. Capture a protected page with Panther
The following executable PHP script demonstrates the form-login path against an existing staging host. It expects environment variables for the base URL, login path, credentials, and a CSS selector that exists only after successful login. Adapt the selectors and submit action to the staging application. The script uses the external host directly and saves the viewport screenshot as staging.png.
<?php
// capture-staging.php
use Symfony\Component\Panther\Client;
require __DIR__ . '/vendor/autoload.php';
$baseUrl = getenv('STAGING_BASE_URL');
$username = getenv('STAGING_USERNAME');
$password = getenv('STAGING_PASSWORD');
$loginPath = getenv('STAGING_LOGIN_PATH') ?: '/login';
$readySelector = getenv('STAGING_READY_SELECTOR') ?: '[data-test="dashboard"]';
foreach (['STAGING_BASE_URL' => $baseUrl, 'STAGING_USERNAME' => $username, 'STAGING_PASSWORD' => $password] as $name => $value) {
if ($value === false || $value === '') {
throw new RuntimeException("Missing required environment variable: {$name}");
}
}
$baseUrl = rtrim($baseUrl, '/');
$client = Client::createChromeClient(null, [], [
'external_base_uri' => $baseUrl,
]);
try {
$client->request('GET', $loginPath);
// Replace these selectors with the actual login form fields.
$client->submitForm('Sign in', [
'email' => $username,
'password' => $password,
]);
// Wait for an authenticated, page-specific element; presence is stronger
// evidence than merely observing that navigation started.
$client->waitForVisibility($readySelector, 15);
$client->takeScreenshot(__DIR__ . '/staging.png');
fwrite(STDOUT, "Saved " . __DIR__ . "/staging.png\n");
} finally {
$client->quit();
}
Run it with values supplied by your shell or CI secret store:
STAGING_BASE_URL='https://staging.example.test' \
STAGING_LOGIN_PATH='/login' \
STAGING_USERNAME="$STAGING_USERNAME" \
STAGING_PASSWORD="$STAGING_PASSWORD" \
STAGING_READY_SELECTOR='[data-test="dashboard"]' \
php capture-staging.php
submitForm() needs the actual submit button label and field names recognized by the page. If the login form uses a different structure, use Panther’s browser interaction methods for that form: locate inputs, type values, click the submit control, then wait for the authenticated page. Do not silently treat a login page screenshot as a successful protected-page capture.
For a project-owned Symfony application
If the test belongs to the Symfony application itself, Panther’s test case can configure the external host explicitly:
<?php
namespace App\Tests;
use Symfony\Component\Panther\PantherTestCase;
final class StagingScreenshotTest extends PantherTestCase
{
public function testProtectedPageScreenshot(): void
{
$client = static::createPantherClient([
'external_base_uri' => $_SERVER['STAGING_BASE_URL'],
]);
// Perform the staging site's actual authentication flow here.
$client->request('GET', '/protected');
$client->waitForVisibility('[data-test="protected-page"]', 15);
$client->takeScreenshot(__DIR__ . '/protected.png');
}
}
With an external base URI, Panther does not start its built-in PHP web server. The Panther guide explains external server configuration.
HTTP Basic versus form login
When a server sends a Basic challenge, the browser is expected to authenticate in response to WWW-Authenticate. This is separate from an HTML login form. Avoid putting credentials in the URL: URLs can be copied into logs, browser history, or diagnostics. If your particular ChromeDriver setup supports a documented Basic-auth mechanism, use that mechanism with secrets supplied from the environment; otherwise handle the gate using your environment’s approved test configuration. The research sources do not establish a single universal Panther credential-injection API for Basic auth.
For form login, interact with the real form or use a project-approved authenticated session bootstrap. For SSO, determine whether the staging identity provider permits automated test accounts and whether the flow includes MFA, consent, or redirects that require special setup.
4. Choose the screenshot size and readiness condition
Panther’s ordinary screenshot captures what the browser screenshot implementation returns for the current window. Set an appropriate browser window size before capture if the viewport dimensions matter; Symfony notes that browser window sizing affects screenshot size. Do not infer full-page capture from takeScreenshot() alone. If you need a full-page image, verify the specific browser/driver capability or use a documented full-page capture implementation for your installed versions.
- Wait for meaning, not elapsed time: use a visible page-specific selector such as a dashboard heading or stable test attribute. Panther provides
waitFor()for presence andwaitForVisibility()for visibility. - Wait for asynchronous content: if the page renders data after initial navigation, wait for the data region or loading indicator to disappear before saving.
- Pick a stable viewport: keep the window dimensions consistent across runs if screenshots are compared or reviewed visually.
- Use an explicit output path: save screenshots to a known artifact directory and ensure the CI job retains that directory when needed.
5. cURL, Python, and Node.js alternatives
These examples are for checking reachability or retrieving a page response; they do not replace Panther’s browser for JavaScript rendering or a browser-based form/SSO flow. For a site protected by HTTP Basic, HTTP clients can send Basic credentials. A form login usually requires a cookie-aware session plus the site’s CSRF and form steps.
cURL: HTTP Basic response check
curl --fail --show-error --user "$STAGING_USERNAME:$STAGING_PASSWORD" \
'https://staging.example.test/protected' \
--output protected-response.html
Python: HTTP Basic response check
import os
import requests
url = os.environ.get("STAGING_URL", "https://staging.example.test/protected")
username = os.environ["STAGING_USERNAME"]
password = os.environ["STAGING_PASSWORD"]
response = requests.get(url, auth=(username, password), timeout=(5, 30))
response.raise_for_status()
with open("protected-response.html", "wb") as output:
output.write(response.content)
Node.js: HTTP Basic response check
const url = process.env.STAGING_URL || 'https://staging.example.test/protected';
const username = process.env.STAGING_USERNAME;
const password = process.env.STAGING_PASSWORD;
if (!username || !password) throw new Error('Set STAGING_USERNAME and STAGING_PASSWORD');
const token = Buffer.from(`${username}:${password}`).toString('base64');
const response = await fetch(url, {
headers: { Authorization: `Basic ${token}` },
signal: AbortSignal.timeout(30000),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('protected-response.html', Buffer.from(await response.arrayBuffer())));
These HTTP examples save response bodies, not screenshots. Use Panther when the requirement is a rendered browser image. Do not send credentials to an untrusted host; keep HTTPS certificate validation enabled.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser or driver fails to start | Browser/driver is missing, incompatible, or unavailable in the CI container. | Install the browser and compatible driver for the execution environment; confirm the binary is discoverable and can launch headlessly where required. |
| Screenshot shows the login page | Authentication did not complete, credentials were rejected, or the script waited for a selector present on both pages. | Check the actual redirect and login result; wait for a unique authenticated-page marker and fail if it never appears. |
| HTTP Basic dialog or repeated 401 | The gate is Basic auth and the browser did not receive valid credentials, or the credentials belong to a different proxy/application layer. | Confirm the response uses WWW-Authenticate, verify the correct credential source, and configure the browser/driver’s supported Basic-auth handling. |
| Form submit never succeeds | Selectors or field names do not match, the submit button label differs, or the form requires CSRF/JavaScript interaction. | Inspect the staging form, use its actual field names and submit control, and wait for the resulting authenticated state. |
| SSO redirect loops or MFA blocks automation | The identity flow requires an interactive step or disallows the test account. | Use the team’s approved test identity and automation setup; avoid weakening production identity controls for screenshots. |
| Wait times out although the page appears loaded | The selector is absent, hidden, changed, or the page has not finished async rendering. | Choose a stable marker, distinguish presence from visibility, and wait for the application-specific readiness state. |
| Image is clipped or too small | The browser window size is not the desired viewport, or the content exceeds the viewport. | Set the browser size explicitly and confirm whether viewport capture meets the requirement; use an explicitly supported full-page method if needed. |
| Screenshot file is missing in CI | Output path is relative to an unexpected working directory or artifacts are not retained. | Use an absolute path based on the script directory and configure CI to preserve the output artifact. |
7. Performance, reliability, and cost
Browser startup and page rendering dominate a one-off capture; reusing a browser client can reduce repeated startup overhead in a test suite, while also requiring careful session cleanup between tests. A fixed delay is usually slower and less reliable than waiting for a meaningful page condition. No source cited here publishes a benchmark comparing Chrome and Firefox for this exact staging screenshot workload, so choose based on driver compatibility and the browser environment you support.
Network speed, third-party scripts, authentication redirects, and application data loading can change completion time. Use bounded waits, fail visibly when the expected authenticated state is absent, and preserve failure screenshots or logs only in a protected artifact store because they may contain private staging data. Panther itself is installed as a development dependency; browser/driver provisioning and CI runtime are separate operational costs.
Or skip the browser setup
If you need a screenshot API instead of maintaining browser and driver setup, ScreenshotNeo accepts one GET request with a URL and returns an image or PDF. Its cookie/consent handling accepts banners and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the page verdict and billing state in headers. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents.
For this protected-staging use case, configure the API’s supported request options to match the access requirements of your staging site; do not send secrets to a service unless your organization permits it. See the ScreenshotNeo API documentation for parameters and authentication options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo includes 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account to get an API key.
FAQ
Can Panther capture a staging page on another host?
Yes. Configure external_base_uri for the running host; Panther then does not start its built-in web server.
Does takeScreenshot() capture the full page?
Do not assume so. The documented basic flow captures a browser screenshot, and screenshot dimensions depend on the browser window size. Confirm full-page behavior for your browser and driver.
Can I use one login script for Basic auth and SSO?
No. Basic auth is a browser challenge, while form login and identity-provider flows use different interactions and policies.
Can I use Panther for JavaScript-rendered pages?
Yes. Panther drives a real Chrome or Firefox browser, so it can execute page JavaScript; wait for the rendered state you need before capturing.


