Salesforce Testing with Playwright: The Complete Guide for QA Teams
Salesforce’s dynamic UI breaks most automation tools. This guide shows you exactly how to set up reliable salesforce testing with playwright from scratch.
Three mandatory Salesforce releases a year. Shadow DOM everywhere. Dynamic IDs that change every session. If your automation suite survives all of that without constant rewrites, you are in the minority. Most QA teams are not that lucky, and the flaky tests pile up fast.
Salesforce testing with playwright solves this by operating at the browser engine level, bypassing the DOM restrictions that break older tools. This guide covers the full setup: authentication, Shadow DOM piercing, stable locators, page objects, test data management, and CI/CD integration. Every pattern here comes from real test maintenance workflows we have validated across Salesforce Enterprise and Unlimited orgs.
Why Salesforce breaks most automation tools
Most test automation frameworks were designed for static, server-rendered HTML pages. Salesforce Lightning is the opposite of that. Its component architecture introduces three specific technical barriers that cause failures in tools like Selenium or Cypress.
Dynamic element IDs that change every session
Salesforce generates IDs such as id="21:1886;a" at runtime. These IDs shift between sessions, between sandbox and production, and after every release. Any test that relies on a fixed ID will break unpredictably.
Shadow DOM encapsulation in Lightning Web Components
Lightning Web Components (LWC) use the browser's native Shadow DOM to encapsulate internal markup, styles, and behavior. Standard document.querySelector calls cannot reach elements inside a shadow root. Older tools require complex JavaScript workarounds or custom plugins to traverse these boundaries.
Shadow DOM is a web standard that isolates a component's internal HTML tree from the rest of the page. Salesforce Lightning uses it to prevent style collisions and enforce component boundaries. Newer orgs use Lightning Web Security (LWS) instead, but Playwright handles both transparently.
Asynchronous rendering and lazy loading
Salesforce components do not render all at once. Many sections load only after user interaction or when they scroll into view. A test that clicks a button before the target element has rendered will throw a "not found" error. Worse, it might silently pass on the wrong element entirely.
These three challenges compound on each other. Salesforce's triannual updates are mandatory and automatic, so your tests must survive DOM changes you cannot control. Understanding test failure analysis becomes a survival skill.
What makes Playwright the right fit for Salesforce
Playwright addresses each of the Salesforce-specific problems listed above through built-in, zero-configuration features. Here is how it maps directly to those pain points.
Native Shadow DOM piercing
Playwright's locator engine automatically traverses shadow roots without special syntax, plugins, or JavaScript injection. A standard page.getByRole('button', { name: 'Save' }) call works whether the button sits inside one shadow root or three nested ones.
Auto-waiting for dynamic elements
Every Playwright action (click, fill, check) automatically waits for the target element to be visible, stable, and enabled before executing. No more sleep() calls or custom retry logic cluttering your Salesforce test scripts.
Semantic locators that ignore DOM structure
Playwright locators like getByRole, getByLabel, and getByText target elements by accessibility attributes, not DOM position. When Salesforce restructures its component hierarchy during a release, these locators continue to work because the visible label or ARIA role rarely changes.
Tip: Before writing a single locator manually, run npx playwright codegen your-salesforce-url.lightning.force.com. Playwright's code generator will record your clicks and produce locator suggestions that you can refine.
| Challenge | Selenium / Cypress | Playwright |
|---|---|---|
| Shadow DOM | Requires manual JS or plugins | Native piercing, zero config |
| Dynamic IDs | Fragile CSS/XPath selectors | Semantic locators (getByRole, getByLabel) |
| Async rendering | Explicit waits / sleep() | Built-in auto-waiting |
| Multi-browser support | WebDriver grid setup | Chromium, Firefox, WebKit out of the box |
| Parallel execution | External grid or plugin | Built-in workers + sharding |

For teams evaluating broader Playwright test automation strategies, Salesforce is one of the strongest use cases because of these built-in capabilities.
Prerequisites
Before starting, confirm that your environment meets these requirements:
- Node.js 18+ (LTS recommended; check with node -v)
- npm 9+ or yarn 1.22+
- A Salesforce sandbox or Developer Edition org with a dedicated automation user
- Basic TypeScript familiarity (all examples in this guide use TypeScript)
- Playwright 1.40+ (earlier versions have less reliable shadow DOM piercing)
If your Salesforce org uses SSO, you will also need access to the identity provider's test configuration or a connected app with the JWT Bearer Flow enabled.
Setting up your project for salesforce testing with playwright
Getting a project ready takes under five minutes. Below is the setup sequence for a TypeScript project targeting a Salesforce sandbox.
Installing Playwright and creating the config
# Initialize a new Node project and install Playwright
npm init -y
npm install -D @playwright/test
npx playwright install
Once installed, create a playwright.config.ts tailored to Salesforce's slower page loads.
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 60_000,
expect: { timeout: 10_000 },
fullyParallel: false,
retries: 1,
workers: 1,
use: {
baseURL: process.env.SF_BASE_URL,
headless: true,
viewport: { width: 1440, height: 900 },
actionTimeout: 15_000,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
});
Why these config choices matter
- timeout: 60_000: Salesforce pages routinely take 8 to 15 seconds to render. A 30 second default cuts it too close.
- fullyParallel: false: Salesforce orgs have concurrency limits. Parallel tests trigger "too many requests" errors. Start sequential, then scale with sharding.
- trace: 'on-first-retry': Traces capture every action, network call, and screenshot. Enabling them only on retries keeps storage manageable. See the Playwright trace viewer guide for advanced strategies.
- viewport: { width: 1440, height: 900 }: Lightning's responsive breakpoints shift below 1280px. A 1440px viewport ensures full desktop layout coverage.
You can also estimate your CI artifact storage overhead using TestDino's free tools, specifically the Trace Configurator, to fine-tune what Playwright captures.
Handling Salesforce login and authentication
Playwright authentication for Salesforce is one of the trickiest parts of the setup. Salesforce enforces MFA, SSO policies, and login rate limits that can derail your tests before they even reach a page.
Using storageState to log in once
The most reliable pattern is to authenticate once in a global setup script and reuse the session cookies across all tests.
import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
try {
await page.goto(process.env.SF_LOGIN_URL!);
await page.getByLabel('Username').fill(process.env.SF_USERNAME!);
await page.getByLabel('Password').fill(process.env.SF_PASSWORD!);
await page.getByRole('button', { name: 'Log In' }).click();
await page.waitForURL('**/lightning/**');
await page.context().storageState({ path: './auth.json' });
} catch (error) {
console.error('Salesforce login failed:', error);
throw error;
} finally {
await browser.close();
}
}
export default globalSetup;
Then reference it in your config:
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
use: {
storageState: './auth.json',
},
});
Note: Never hardcode Salesforce credentials. Store SF_USERNAME, SF_PASSWORD, and SF_LOGIN_URL as environment variables and use your CI provider's secrets manager for pipeline runs.
Handling MFA programmatically
If your Salesforce org enforces TOTP-based MFA on the automation user, install a library like otplib to generate codes at runtime.
import { authenticator } from 'otplib';
export function generateMfaCode(): string {
return authenticator.generate(process.env.SF_MFA_SECRET!);
}
Call generateMfaCode() in your global setup right after the password step. For orgs that mandate SSO, the OAuth 2.0 JWT Bearer Flow bypasses the UI login entirely by exchanging a signed JWT for a session token via Salesforce's API.
Note: Salesforce session tokens expire after the org's configured timeout (default: 2 hours). If your suite runs longer, later tests will fail with "session expired" errors. Add a session health check between test groups.

Navigating Shadow DOM in Lightning components
Shadow DOM handling is where salesforce testing with playwright truly differentiates itself from older tools. With Playwright, you do not need to think about shadow boundaries at all in most cases.
How Playwright pierces shadow roots automatically
When you write a locator like this:
await page.getByRole('button', { name: 'New Account' }).click();
Playwright's engine scans the entire DOM tree, including every shadow root, to find a button with the accessible name "New Account." There is no >>> shadow-piercing combinator, no page.evaluate() hack, and no special configuration flag.
This works because Playwright operates at the browser engine level through the Chrome DevTools Protocol (CDP), not through the DOM API that Shadow DOM restricts. Your locators stay clean regardless of nesting depth.
When you still need frameLocator
Salesforce sometimes embeds content in iframes (Visualforce pages, Classic-to-Lightning bridges, analytics dashboards). Shadow DOM piercing does not cross iframe boundaries. For those cases, use:
const vfFrame = page.frameLocator('iframe[title="Visualforce Page"]');
await vfFrame.getByRole('textbox', { name: 'Search' }).fill('Acme Corp');
Combining frame locators with shadow-piercing locators covers every element type in a Salesforce org. For the full locator toolkit, the Playwright e2e testing guide provides additional patterns.
Shadow DOM vs Lightning Web Security
In Salesforce orgs running Spring '24 and later, some components use Lightning Web Security (LWS) instead of traditional Shadow DOM. LWS uses JavaScript sandboxing rather than shadow roots for isolation. Playwright handles LWS components identically because it operates below the isolation layer.
Writing stable locators for dynamic Salesforce pages
Even with Playwright's smart locator engine, choosing the wrong selector strategy will produce flaky results. Here are the locator strategies ranked from most resilient to least, specifically for Salesforce.
Locator priority for Salesforce
- getByRole + accessible name: Best for buttons, links, headings, and form controls. Salesforce Lightning components generally expose correct ARIA roles.
- getByLabel: Best for input fields. Salesforce form fields almost always have visible labels.
- getByText: Useful for static text elements, but risky if the same text appears in multiple places.
- data-aura-class or data-testid: If your team controls custom LWC development, adding data-testid attributes provides the most stable anchor.
- CSS selectors: Last resort. Salesforce's generated class names change between releases.
Tip: Use TestDino's Locator Playground from the free tools page to test locator strategies with live match counts before committing them to your tests.
Example: creating an Account record
import { test, expect } from '@playwright/test';
test('create a new Account in Salesforce', async ({ page }) => {
await page.goto('/lightning/o/Account/list');
await page.getByRole('button', { name: 'New' }).click();
await page.getByLabel('Account Name').fill('Acme Test Corp');
await page.getByLabel('Phone').fill('555-0100');
await page.getByLabel('Industry').selectOption('Technology');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Acme Test Corp')).toBeVisible();
});
None of these locators reference a CSS class, an XPath, or a dynamic ID. They describe what a user sees. This test will survive Salesforce's next release without edits, which is one of the core Playwright best practices for Salesforce QA.
Building a page object model for Salesforce
As your test suite grows, raw locators scattered across tests become a maintenance problem. The Playwright page object model pattern centralizes selectors and actions into reusable classes.
Structuring page objects for Salesforce components
import { Page, Locator, expect } from '@playwright/test';
export class AccountPage {
private readonly page: Page;
private readonly newButton: Locator;
private readonly accountName: Locator;
private readonly phone: Locator;
private readonly industry: Locator;
private readonly saveButton: Locator;
constructor(page: Page) {
this.page = page;
this.newButton = page.getByRole('button', { name: 'New' });
this.accountName = page.getByLabel('Account Name');
this.phone = page.getByLabel('Phone');
this.industry = page.getByLabel('Industry');
this.saveButton = page.getByRole('button', { name: 'Save' });
}
async navigate() {
await this.page.goto('/lightning/o/Account/list');
}
async createAccount(name: string, phone: string, industry: string) {
await this.newButton.click();
await this.accountName.fill(name);
await this.phone.fill(phone);
await this.industry.selectOption(industry);
await this.saveButton.click();
}
async verifyAccountVisible(name: string) {
await expect(this.page.getByText(name)).toBeVisible();
}
}
Using the page object in a test
import { test } from '@playwright/test';
import { AccountPage } from '../pages/AccountPage';
test('create Account via page object', async ({ page }) => {
const accountPage = new AccountPage(page);
await accountPage.navigate();
await accountPage.createAccount('Acme Test Corp', '555-0100', 'Technology');
await accountPage.verifyAccountVisible('Acme Test Corp');
});
When Salesforce renames "Account Name" to "Company Name" in a future release, you update one line in AccountPage.ts instead of dozens of spec files. This pairs well with Playwright fixtures for injecting page objects automatically.

Salesforce's own UTAM framework provides pre-built page objects for standard components, but adds complexity with its compiler and adapter layer. For heavily customized orgs, hand-rolled Playwright page objects offer more flexibility. UTAM works best when your org uses 80%+ standard components.
Managing test data in Salesforce orgs
Test data management is where many Salesforce automation projects quietly fail. Your tests need records, but those records cannot collide with each other or with manual testers sharing the sandbox.
Creating and cleaning test data via API
Instead of creating test records through the UI (slow and fragile), use Salesforce's REST API in a beforeAll hook or a global setup script:
import { request } from '@playwright/test';
export async function createTestAccount(baseUrl: string, sessionToken: string) {
const apiContext = await request.newContext({
baseURL: baseUrl,
extraHTTPHeaders: {
Authorization: `Bearer ${sessionToken}`,
'Content-Type': 'application/json',
},
});
const response = await apiContext.post('/services/data/v59.0/sobjects/Account', {
data: {
Name: `Test_Account_${Date.now()}`,
Industry: 'Technology',
},
});
return (await response.json()).id;
}
Isolation strategies
- Timestamp-based naming: Append Date.now() to record names so parallel runs never collide.
- Teardown hooks: Delete created records in afterAll to keep the sandbox clean.
- Dedicated test user: Use a separate Salesforce user with a role that limits visibility to test records only.
Neglecting cleanup is the top cause of "works on my machine" failures in Salesforce regression testing. Orphaned test records eventually hit governor limits and slow every test.
Running Salesforce Playwright tests in CI/CD
A test suite that only runs locally is incomplete. Moving your salesforce testing with playwright setup into a CI/CD pipeline makes it part of your deployment gate.
GitHub Actions example
name: Salesforce Playwright Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test
env:
SF_BASE_URL: ${{ secrets.SF_BASE_URL }}
SF_LOGIN_URL: ${{ secrets.SF_LOGIN_URL }}
SF_USERNAME: ${{ secrets.SF_USERNAME }}
SF_PASSWORD: ${{ secrets.SF_PASSWORD }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
CI/CD checklist for Salesforce
- Single browser: Install only Chromium (npx playwright install --with-deps chromium) to cut CI setup time. Salesforce Lightning is primarily tested on Chrome.
- Sequential workers: Start with workers: 1 to avoid concurrency limits. Scale using sharding across CI nodes. Calculate your ideal shard count with TestDino's Sharding Calculator from the free tools page.
- Artifact management: Upload traces and screenshots only on failure to keep storage costs low.
- Sandbox health check: Verify the automation user can log in before running the full suite.
- Schedule around refreshes: Avoid running tests during Salesforce sandbox refresh windows.
Note: Sandbox refresh schedules can invalidate your automation user's credentials. Set up a login health check before running the full suite.
Teams using TestDino's Playwright Skill can auto-generate Salesforce-specific page objects and test scaffolding from their org's component structure.
Monitoring test health across runs is equally important. Platforms that provide test automation analytics help you spot regressions before they reach production.
Use the Playwright debugging guide to triage failures using traces and console logs. For a structured Playwright framework setup, combine the patterns here into a scalable architecture.

Troubleshooting common failures
Even with a solid setup, Salesforce-specific issues will surface. These are the five failures we see most frequently, along with their fixes.
1. "Element not found" on elements that visually exist
Cause: The element is inside an iframe, not just a shadow root. Playwright's shadow piercing does not cross iframe boundaries.
Fix: Wrap your locator in a frameLocator:
const frame = page.frameLocator('iframe[title="accessibility title"]');
await frame.getByRole('button', { name: 'Submit' }).click();
2. Tests pass locally but fail in CI
Cause: CI runners are slower, and Salesforce pages take longer to render on shared infrastructure.
Fix: Increase actionTimeout to 20 to 30 seconds for CI. Add waitForLoadState('networkidle') after navigation to Salesforce pages that make many API calls on load.
3. Login fails intermittently
Cause: Salesforce rate-limits login attempts. Running tests frequently or in parallel triggers this.
Fix: Use storageState to log in once. Set your automation user's profile to exempt from login rate limits if your Salesforce edition allows it.
4. Picklist options not loading
Cause: Salesforce lazily loads picklist values. Your test clicks the dropdown before options are populated.
Fix: After clicking the combobox, wait for the listbox to appear:
await page.getByRole('combobox', { name: 'Industry' }).click();
await page.getByRole('listbox').waitFor();
await page.getByRole('option', { name: 'Technology' }).click();
5. "Session expired" errors mid-suite
Cause: The storageState token has exceeded the org's session timeout.
Fix: Either reduce suite runtime to under your org's timeout setting, or add a session refresh mechanism in a beforeEach hook that checks whether the session is still valid.
Conclusion
Key takeaways
- Shadow DOM is not a blocker: Playwright pierces shadow roots natively. No plugins, no workarounds, no special syntax.
- Log in once, run everything: The storageState pattern eliminates repeated login overhead and avoids Salesforce rate limits.
- Semantic locators survive releases: getByRole and getByLabel target what users see, not what Salesforce generates internally.
- API-driven test data is non-negotiable: Creating records through the UI is slow and fragile. Use Salesforce REST APIs.
Salesforce testing with playwright solves the core problems of Shadow DOM, dynamic IDs, and async rendering out of the box. By combining session-based auth, semantic locators, and a structured page object model, you can build a test suite that survives Salesforce's triannual releases without constant maintenance.
FAQs

Savan Vaghani
Product Developer


