How to Detect Visual Changes on Password-Protected Web Pages
Use an authorized Playwright session to capture a protected page, compare it with a reviewed screenshot baseline, and keep authentication state secure.
To detect visual changes on a password-protected page, use browser automation with authorized access: save an authenticated browser state, load it in a repeatable environment, capture the page, and compare it with a reviewed screenshot baseline. Playwright Test provides screenshot assertions for that comparison. Keep the state file secret, wait for the page to settle, and review baseline updates before accepting them.
This guide uses Playwright with JavaScript. It shows a reusable login-state setup and a visual test, explains how to reduce noisy diffs, and covers common failure cases. The example assumes you are authorized to access the account and that the site permits automated access.
1. Set up Playwright
Use a consistent Node.js version, Playwright version, browser, operating system, and headless setting for both baseline creation and later runs. Differences in rendering environments can produce screenshot diffs unrelated to a site change. [Playwright visual comparisons] [Playwright test CLI]
mkdir protected-page-monitor
cd protected-page-monitor
npm init -y
npm install --save-dev @playwright/test
npx playwright install chromium
Add these scripts to the scripts object in package.json:
{
"scripts": {
"auth:setup": "playwright test --project=setup",
"test": "playwright test",
"test:update-snapshots": "playwright test --update-snapshots"
}
}
Create .gitignore and exclude both generated authentication state and any local secrets:
playwright/.auth/
.env
Playwright warns that saved browser state can contain sensitive cookies and headers that could impersonate an account. Do not commit it, upload it to an issue, or expose it in build logs. Limit access to the machine or CI secret store that needs it. [Playwright authentication]
2. Save an authorized login session
Playwright’s recommended pattern is to authenticate in a setup project and save the browser context state for later tests. State can include cookies, local storage, IndexedDB, and passkey authentication. It can expire, so the setup must be rerun when the saved session is no longer valid. [Playwright authentication]
Create playwright.config.js. Replace the example protected URL and login URL with the pages for your application. The example expects credentials in environment variables and uses accessible labels; adjust those locators to match the site’s login form.
const { defineConfig, devices } = require('@playwright/test');
const authFile = 'playwright/.auth/user.json';
module.exports = defineConfig({
testDir: './tests',
fullyParallel: false,
reporter: 'list',
use: {
baseURL: process.env.BASE_URL || 'https://example.com',
browserName: 'chromium',
headless: true,
...devices['Desktop Chrome'],
viewport: { width: 1440, height: 1000 },
locale: 'en-US',
timezoneId: 'UTC',
screenshot: 'only-on-failure',
trace: 'retain-on-failure'
},
projects: [
{
name: 'setup',
testMatch: /auth\.setup\.js/
},
{
name: 'visual-chromium',
testMatch: /visual\.spec\.js/,
dependencies: ['setup'],
use: { storageState: authFile }
}
]
});
Create tests/auth.setup.js. Replace the locators and post-login destination with the application’s real login flow. Use credentials for a dedicated, least-privilege monitoring account when possible.
const { test: setup, expect } = require('@playwright/test');
const fs = require('node:fs');
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
const username = process.env.MONITOR_USERNAME;
const password = process.env.MONITOR_PASSWORD;
if (!username || !password) {
throw new Error('Set MONITOR_USERNAME and MONITOR_PASSWORD before auth setup');
}
fs.mkdirSync('playwright/.auth', { recursive: true });
await page.goto('/login');
await page.getByLabel('Email').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('**/dashboard');
await expect(page).not.toHaveURL(/\/login/);
await page.context().storageState({ path: authFile, indexedDB: true });
});
Run the setup with credentials supplied through your shell or CI’s secret injection:
BASE_URL=https://your-app.example \
MONITOR_USERNAME='monitor@example.com' \
MONITOR_PASSWORD='secret-from-your-secret-store' \
npm run auth:setup
For local development, use a secret manager or a locally ignored environment file. Do not put real credentials directly in the test source. If your login requires a one-time code, approval, or a manual challenge, complete that authorized flow as appropriate and save state only after the account has reached the protected page. Avoid trying to bypass access controls.
3. Capture and compare the protected page
Create tests/visual.spec.js. The screenshot assertion creates a reference image on the first run, then compares subsequent captures against it. Review the first image before treating it as the approved baseline. A snapshot update changes what later runs consider normal. [Playwright visual comparisons]
const { test, expect } = require('@playwright/test');
test('protected dashboard matches its approved appearance', async ({ page }) => {
await page.goto('/dashboard');
// Fail clearly if authentication expired or the protected page redirected.
await expect(page).not.toHaveURL(/\/login/);
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
// Wait for a meaningful page element before capturing.
await expect(page.locator('[data-testid="dashboard-content"]')).toBeVisible();
await expect(page).toHaveScreenshot('dashboard.png', {
fullPage: true,
animations: 'disabled',
maxDiffPixelRatio: 0.001
});
});
Replace the heading and data-testid with stable selectors from your page. Choose a tolerance deliberately: too strict can flag rendering noise; too loose can conceal real changes. You can capture a focused region instead of the entire page with a locator screenshot assertion:
await expect(page.locator('[data-testid="billing-summary"]'))
.toHaveScreenshot('billing-summary.png', { animations: 'disabled' });
Run the test:
BASE_URL=https://your-app.example npm test
On the first run, Playwright stores the expected snapshot in the project’s snapshot directory. Inspect the image and diff output. To accept an intentional UI change, review the change and then run:
BASE_URL=https://your-app.example npm run test:update-snapshots
Commit reviewed baseline images alongside the test when they are appropriate for your repository. If baselines are stored elsewhere, ensure the test runner retrieves the same reviewed version on every run.
4. Make captures stable without hiding real changes
- Use the same environment. Keep browser version, operating system, viewport, device scale factor, locale, timezone, fonts, and headless configuration consistent. A containerized CI image can help keep the environment repeatable; pin versions used to create and compare snapshots. [Playwright visual comparisons] [Playwright in CI]
- Wait for readiness. Wait for a page-specific heading, chart, or content container. Playwright screenshot assertions wait for consecutive screenshots to stabilize before comparison. This helps with settling layout but does not guarantee that every external widget or delayed request has finished. [Playwright assertions]
- Disable animation where appropriate. The example sets
animations: 'disabled'. This can reduce timing differences, but do not use it if animation itself is what you need to monitor. - Mask known volatile values. A timestamp, rotating avatar, or unrelated live counter can be masked if its appearance is outside the monitoring goal. Prefer targeted masks over hiding broad sections. Playwright screenshot assertions support masks; see the assertion options in the official documentation. [Playwright assertion options]
- Use test data where possible. A dedicated account with stable data makes meaningful visual changes easier to distinguish from routine content churn.
- Capture the right scope. Full-page images catch changes below the fold but include more dynamic content. A selected element narrows the comparison and can reduce unrelated diffs.
Do not hide an area if changes there are part of the monitoring objective. Pixel comparison tells you that rendered output differs; it does not explain the cause or prove that the page’s behavior is correct. Pair it with functional assertions for important actions and data.
5. Run it reliably in CI
- Store the login credentials in the CI system’s secret facility and expose them only to the setup step.
- Generate fresh authentication state in the job, or retrieve a securely stored state if your process explicitly manages its expiry and access.
- Run setup before the visual project. The Playwright project dependency in the configuration enforces this order.
- Keep browser and operating-system versions aligned with the environment that generated the baseline.
- Retain failure screenshots, traces, and diffs only in access-controlled artifacts, since page content and traces may contain sensitive account data.
- Review snapshot changes before updating the baseline. Do not automatically bless every failed comparison.
If authentication involves an expiring session, rerunning the setup is usually the simplest recovery. If an account uses a passkey or a more complex identity provider, follow the site’s supported sign-in flow and verify that the saved state actually reaches the protected page before relying on it. [Playwright authentication]
6. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Redirected to login | Saved cookies or storage expired, setup did not complete, or the test is using the wrong base URL. | Rerun npm run auth:setup; assert the post-login destination and protected content are visible; verify the same host is used for setup and test. |
| Login locator times out | The example labels or button name do not match the actual form, or the login flow has an intermediate step. | Inspect the page and use locators matching the site’s accessible names. Handle its normal authorized steps before saving state. |
| Storage state file missing | The setup project did not run or failed before saving. | Run the setup project, check its output, and confirm the configured auth path matches the saved path. |
| Baseline mismatch on every run | Browser or OS differs, fonts are unavailable, dynamic content varies, or the page is captured before it settles. | Align environments, wait on meaningful page content, stabilize test data, and mask only known irrelevant volatility. |
| Screenshot is blank or incomplete | The page navigated to an error or login state, content was not ready, or the target selector is wrong. | Assert the expected page heading and content before capture; inspect failure screenshots and trace artifacts. |
| Snapshot update changes too much | A broad or unreviewed baseline update accepted unrelated changes. | Review the diff, restore unrelated snapshots, and update only after confirming the visual change is intended. |
| Works locally, fails in CI | Rendering environment, fonts, browser build, viewport, or secrets differ. | Pin and align the environment; verify secret injection and retain protected diagnostic artifacts. |
| Authentication state appears in Git | The generated state file was not excluded before staging. | Remove it from the index, ensure the auth directory is ignored, and treat exposed state as compromised: revoke sessions or rotate credentials according to the application’s process. |
7. Performance, reliability, and cost
Each visual run must launch a browser, load the page, authenticate or reuse valid state, and produce a screenshot. Reusing saved state avoids repeating the login flow on every test, but it adds state expiry and secret-management work. A full-page capture and broad test suite generally involve more page content and comparisons than a focused element capture; choose the smallest area that still answers the monitoring question.
For reliable comparisons, prioritize stable test data and identical rendering conditions over increasing pixel tolerance. Retries can help diagnose intermittent infrastructure failures, but a retry should not silently convert a visual mismatch into an accepted baseline. Playwright itself is the browser automation and comparison workflow here; execution cost depends on your CI or machine resources, which this guide does not quantify.
Or skip the browser setup
For pages your capture request can access, ScreenshotNeo is a website screenshot API and MCP server: one GET request returns a screenshot or PDF. It can accept custom cookies, headers, and authorization, but this short example is for a URL that is directly capturable; use the documentation for request options and handle any credentials as secrets. [ScreenshotNeo API documentation]
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}`);
- Cookie banners are accepted and removed before capture; newsletter popups and chat widgets are removed too, and each cleanup step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Responses identify the page verdict and billing status in headers.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for AI agents. - 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Can I use a screenshot baseline as a complete regression test?
No. It detects rendered differences, but it does not establish why they happened or whether interactions and data are correct. Add functional checks for behavior that matters.
Should I save a different baseline for every browser?
If you intentionally compare across different rendering environments, keep their baselines separate. Otherwise environment-driven rendering differences can be mistaken for application changes.
What should I do when the monitored account requires MFA?
Use the site’s authorized sign-in process and save state only after successful access. Do not bypass the site’s access controls; coordinate an approved monitoring account or flow with the application owner.
Can I monitor content that changes constantly?
Yes, if the changing part is not itself the target of the check, stabilize its test data or mask that specific region. If it is the target, preserve it in the comparison.


