How to Capture Logged-In Pages on an Indian SaaS App with Playwright
Save Playwright authentication state, reuse it to capture protected SaaS pages, and troubleshoot cookies, storage, full-page shots, and sensitive data.
To capture a page after signing in to an Indian SaaS app with Playwright, authenticate an authorized test account, save the browser context’s authentication state, load that state into the context used for capture, navigate to the protected page, verify that the session is still signed in, then call page.screenshot(). Use fullPage: true to capture the full scrollable page; otherwise Playwright captures the current viewport.
The workflow is the same for SaaS apps in India as elsewhere. Replace the example URLs, login controls, and authenticated-page check with the target app’s actual flow. Use an account you are permitted to automate, preferably a non-production test account, and keep saved state and screenshots private if they contain sensitive information.
1. Set up a private authentication state
Install Playwright Test in a Node.js project if you have not already:
npm install --save-dev @playwright/test
npx playwright install
Create a setup test that signs in and saves the browser context state. Store credentials in environment variables or a secret manager, not in source code. The accessible labels and button name below are examples; adapt them to the app.
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
setup('authenticate test account', async ({ page }) => {
const loginUrl = process.env.APP_LOGIN_URL;
const email = process.env.APP_TEST_EMAIL;
const password = process.env.APP_TEST_PASSWORD;
if (!loginUrl || !email || !password) {
throw new Error('Set APP_LOGIN_URL, APP_TEST_EMAIL, and APP_TEST_PASSWORD');
}
await page.goto(loginUrl);
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: /sign in/i }).click();
// Choose a reliable app-specific signal that appears only after login.
await expect(page.getByRole('button', { name: /account|profile/i })).toBeVisible();
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
The state file can contain cookies and headers that let someone impersonate the account. Add it to .gitignore and restrict access to it and to generated screenshots.
# .gitignore
playwright/.auth/
2. Reuse the state when capturing a protected route
Configure a setup project and a capture project. The dependency runs authentication before the capture project, and storageState loads the saved cookies and local storage into its browser context.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
{
name: 'capture',
use: {
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
Navigate directly to the protected route, confirm an authenticated UI element is visible, and save the screenshot. This check catches expired state or a redirect to the login page before it produces a misleading image.
// tests/capture.spec.ts
import { test, expect } from '@playwright/test';
test('capture authenticated dashboard', async ({ page }) => {
await page.goto('https://app.example.in/dashboard');
await expect(page.getByRole('button', { name: /account|profile/i })).toBeVisible();
await page.screenshot({ path: 'artifacts/dashboard.png', fullPage: true });
});
Run the capture test with:
npx playwright test --project=capture
Change the example URL and locators to match the actual SaaS app. If the app uses a supported authentication API, Playwright documents API-based authentication as an alternative to filling in the UI.
3. Choose the screenshot extent and output
page.screenshot() returns image bytes and can also write directly to a path. By default it captures the visible viewport. Set options to control the captured area and rendering:
| Option | Use |
|---|---|
path |
Write the image to a file, such as artifacts/dashboard.png. |
fullPage: true |
Capture the full scrollable document instead of only the viewport. |
clip |
Capture a rectangle using its x, y, width, and height. |
mask |
Cover selected locator matches in the screenshot, for example dynamic or private fields. |
scale |
Choose CSS pixel or device pixel scaling. |
animations |
Control whether animations are allowed or disabled while capturing. |
For example, mask a private account value and capture the full page:
await page.screenshot({
path: 'artifacts/dashboard.png',
fullPage: true,
mask: [page.locator('[data-private]')],
});
Review the resulting artifact to verify that the intended private content is hidden. A mask covers pixels in the output; it does not remove the underlying data from the page or protect the original account.
4. Handle app-specific authentication storage
The simplest saved state covers cookies and local storage. Some applications use other browser storage or authentication mechanisms, which may need additional handling.
- IndexedDB: If the app stores authentication tokens there, check the installed Playwright version’s
storageStateAPI and enable its documented IndexedDB capture option where applicable. - sessionStorage: It is scoped to a domain and is not included in the ordinary reusable-state flow. Playwright’s authentication guide shows how to save it manually and restore it with an initialization script.
- WebAuthn: The current API documents support for virtual WebAuthn credentials in storage state; confirm that the installed version and app flow support the needed behavior.
- Origin-private file system: The API also documents an
opfsoption, with a limitation for ephemeral WebKit contexts. Check version-specific support before relying on it. - Browser-specific login: Some authentication is tied to a browser or project. A state file created in one browser may not work in another.
Playwright options can change between versions. Consult the installed version’s API documentation before enabling less common storage options.
5. Keep runs safe and repeatable
- Use an authorized account. Prefer a test account with minimal access and non-production data. Confirm that automation and capture are allowed for the app and pages involved.
- Refresh expired state. A saved session is not permanent. Rerun authentication when the app expires or revokes it.
- Isolate accounts when tests mutate shared data. A shared account can work when concurrent tests do not interfere. If parallel tests change server-side state, use a separate account for each worker.
- Protect artifacts. Screenshots may contain personal, employee, or customer information. Store and share them according to your organization’s data-handling rules.
- Use stable visual-regression environments. Playwright notes that screenshot rendering can vary by operating system, browser version, settings, hardware, power source, and headless mode. Keep those consistent between baseline and comparison runs.
For visual comparisons, expect(page).toHaveScreenshot() creates a baseline on its first run and compares later runs. Playwright waits for two consecutive screenshots to match before saving a new baseline. Dynamic content can be masked or styled out, and reference snapshots can be updated when a deliberate UI change is accepted.
6. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| The protected URL shows the login form. | The state expired, was not loaded, or the app stores its token outside the saved state. | Rerun setup, verify the state path, and check whether the app uses IndexedDB or sessionStorage. Assert an authenticated-only element before capture. |
| Setup times out after clicking Sign in. | The example locator does not match the app, login requires another step, or the chosen success signal never appears. | Use the actual accessible label and button name. Handle required MFA or consent steps through an authorized test flow, then wait for a reliable app-specific success signal. |
| The state file is missing. | The setup project did not run, the output directory differs, or the project points to the wrong path. | Run the setup project and make the save path and storageState path identical. Check project dependencies and working directory. |
| The full-page image is incomplete or awkwardly laid out. | Content may load on scroll, the page may use nested scroll containers, or the app may render content asynchronously. | Wait for a reliable page-ready signal, scroll the relevant container if needed to trigger lazy content, and confirm the app’s layout supports document-level full-page capture. |
| Private text remains visible in the image. | The mask selector did not match the element or covered only a different instance. | Check the locator count and resulting image. Mask the correct locator and inspect the artifact before sharing it. |
| Visual snapshots differ across machines. | Browser, operating system, hardware, rendering settings, headless mode, or dynamic page data changed. | Run baseline and comparison in the same environment, stabilize dynamic content, and update baselines only for intentional changes. |
| Parallel captures interfere with one another. | Workers share a mutable account or server-side state. | Use separate test accounts per worker when tests change shared state, or make captures read-only. |
7. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. For a public page that does not require your logged-in browser session, one request returns an image or PDF. See the ScreenshotNeo API documentation for options and setup.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. 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.
FAQ
Can Playwright capture an Indian SaaS page if I only have a screenshot URL?
A protected page usually needs an authenticated browser context. Use the app’s authorized login flow and saved state; a plain public URL alone does not provide your session.
Does a saved state file keep me logged in indefinitely?
No. The app can expire or revoke its session. Rerun the setup flow when the authenticated-page check fails.
Can I use one account for parallel screenshots?
Yes, when captures do not interfere with shared server-side state. Use separate accounts per worker when parallel tests make changes.
Is the workflow different because the app is based in India?
The Playwright steps are not country-specific. The app’s terms, page contents, and your organization’s data-handling requirements determine whether a particular capture is appropriate.


