Salesforce Test Automation
with Playwright
Historically, automating Salesforce UI was a nightmare due to dynamic namespaces, complex Shadow roots, nested frames, and slow execution. Playwright changes everything. This course compiles 10+ years of Salesforce and automation experience to help you master Salesforce QA concepts and Playwright engineering patterns for competitive interviews.
- Salesforce Core Concepts: CRM fundamentals, SaaS vs PaaS platforms, multitenant metadata engines, standard objects lifecycles, and customization layers.
- Salesforce QA Domain Knowledge: Sandboxes, Profile permissions, sharing hierarchies, validation UI alerts, and Governor Limit constraints.
- Playwright Shadow DOM Piercing: Learn how Playwright locates components inside Salesforce LWC shadow roots natively.
- Bypassing MFA & Caching Sessions: Implement high-performance storageState state reuse to skip logins.
- API-First Test Seeding: Leverage Salesforce REST API within Playwright to seed test data in milliseconds.
- Hands-On Coding Project: Initialize and scaffold a working Playwright framework from scratch using Page Objects and session caches.
What is Salesforce & How It Works?
To test a Salesforce instance, you must understand what Salesforce is, how its unique cloud infrastructure operates, and the core CRM features that power business processes.
Salesforce is the world's leading cloud-based Customer Relationship Management (CRM) platform. It operates primarily as a Software as a Service (SaaS) model, where out-of-the-box business apps are ready to run, and a Platform as a Service (PaaS) model (Force.com), enabling developers to write custom code and build bespoke applications.
Ready-to-use business solutions like Sales Cloud (Lead and deal tracking), Service Cloud (Ticketing and customer cases), and Marketing Cloud (Journeys and email blasts).
Custom customization capability. Write backend apex triggers, design custom tables (objects), build Lightning Web Components, and configure complex automated Flows.
Understanding these two concepts separates junior automation engineers from true Salesforce SDET experts:
-
MMultitenant Database ArchitectureAll customers (tenants) share the same physical server and database infrastructure. A strict runtime security engine ensures that one tenant's users can never see another tenant's data, even though they sit on the same server rows.
-
DMetadata-Driven Customization EngineIn Salesforce, standard layout rendering, custom fields, validation rules, security levels, and sharing settings are stored entirely as metadata (declarative XML files). When a page loads, the compiler reads this metadata on-the-fly to render the custom LWC interface dynamically.
Salesforce structures database tables as **Objects**. To test effectively, you must understand the standard Lead conversion lifecycle:
| Standard Object | Database Concept | Business Purpose | Lifecycle Role |
|---|---|---|---|
| Lead | Table: Lead |
Raw prospect contact details. Unverified interest. | The starting point. After qualification, converting it auto-creates Account, Contact, and Opportunity records. |
| Account | Table: Account |
A company, customer, partner, or business entity. | The central record. Stores all related contacts, deals, and service tickets. |
| Contact | Table: Contact |
An individual person associated with an Account. | Contains email, phone, and professional titles. Linked to the Account record. |
| Opportunity | Table: Opportunity |
A specific active sales deal or revenue pipeline. | Tracks deal amounts, close dates, and progressive stages (e.g. Prospecting -> Closed Won). |
Salesforce QA & Domain Fundamentals
To build bulletproof Salesforce test automation, you must understand the architectural rules, sandboxes, and user sharing structures that govern the Salesforce CRM platform.
Salesforce development happens in isolated environments called Sandboxes. Automated suites are typically run against Developer or Partial sandboxes, and finally against Full Sandboxes during regression cycles.
| Sandbox Type | Data Limit | Refresh Interval | QA Automation Target |
|---|---|---|---|
| Developer | 200 MB | 1 Day | Perfect for smoke tests and isolated feature branch validations. |
| Developer Pro | 1 GB | 1 Day | Used for integration testing with external APIs. |
| Partial Copy | 5 GB (incl. sample data) | 5 Days | Standard candidate for daily automated regression runs. |
| Full Sandbox | Same as Production | 29 Days | Final stage validation. High-volume performance runs. |
Salesforce enforces security via **Profiles** (what a user can see), **Permission Sets** (additional privileges), and **Sharing Rules** (record-level access). A vital part of QA is verifying that Standard Users cannot edit records owned by other departments, while Admins can.
BrowserContext instances in parallel, simulating the Admin, Approver, and Standard User concurrently!
-
1Validation Rules & Error UISalesforce developers write Validation Rules to prevent bad data. Automated tests must verify both the "happy path" (successful record saves) and the "error path" (ensuring warning toasts appear when fields are invalid).
-
2Apex Triggers & Process FlowsUpdating a single field in UI can trigger a chain reaction: auto-creating related tasks, sending emails, or converting Leads. Automation should always do **UI+API verification**—checking both the front-end layout and the database side-effects.
-
3Governor Limit AwarenessSalesforce enforces strict multitenant limits (e.g., max 100 SOQL queries per transaction). Poorly designed automation that bombards pages or triggers bulk operations synchronously can crash the server instance, returning
System.LimitExceptionerrors.
Salesforce Architecture & Shadow DOM
Salesforce Lightning Experience is built entirely on modern web components that encapsulate DOM trees inside shadow roots. Traditional automation tools struggle to touch these elements—Playwright handles them natively.
Salesforce LWC (Lightning Web Components) use **Shadow DOM** in `open` mode. The shadow boundary prevents external styles and standard query selectors from accessing nested fields.
Fails to locate shadow elements using normal selectors. You must write deep, fragile custom JavaScript scripts using shadowRoot.querySelector() to chain and pierce each boundary manually.
Pierces shadow boundaries automatically! Playwright locators automatically drill through open shadow roots by default. Standard locators work out of the box without any code tricks.
Below is a typical LWC shadow structure in a Salesforce standard contact form. In Playwright, you can locate the inner input directly.
<lightning-input class="slds-form-element">
#shadow-root (open)
<div class="slds-form-element__control">
<input id="input-42" type="text" class="slds-input" />
</div>
</lightning-input>
// Playwright ignores the Shadow root completely and targets the LWC element or the input directly!
await page.locator('lightning-input input').fill('John Doe');
// A much more resilient role-based selector (which also pierces shadow DOM):
await page.getByLabel('First Name').fill('John Doe');
Answer: "You don't need any configuration. Playwright's locator engines, including CSS selectors, text selectors, and role queries, bypass open shadow roots natively. It is built directly into the core locator architecture."
Bypassing MFA & Caching Sessions
Standard Salesforce UI logins are incredibly slow (taking 8 to 15 seconds) and are frequently blocked by Multi-Factor Authentication (MFA). Master high-performance session reuse.
MFA is required in production, but in automated QA sandbox instances, you should bypass MFA using these techniques:
- IP Relaxation: Add the test execution IP ranges (e.g., your GitHub Actions runners) to the Salesforce Network Access Allowed IPs to prevent security verification prompts.
- API-based Auth: Authenticate via JWT or client-credentials API flows to retrieve an Access Token, then set this token directly into cookies/local storage.
- Custom System Profile settings: Disable "MFA for User Interface Logins" on the dedicated Integration/Automation User profile within sandbox setups.
Instead of logging into the Salesforce UI in each individual test spec, log in exactly once inside a **global setup** step, capture the resulting cookies and localStorage to a JSON file, and load this saved state instantly in all subsequent test context creations.
import { test as setup, expect } from '@playwright/test';
setup('login to salesforce and save session', async ({ page }) => {
await page.goto('https://test.salesforce.com');
await page.locator('#username').fill(process.env.SF_USERNAME!);
await page.locator('#password').fill(process.env.SF_PASSWORD!);
await page.locator('#Login').click();
// Wait for home page dashboard loading to ensure auth fully completes
await expect(page).toHaveURL(/lightning/);
// Write the authenticated state to storage JSON
await page.context().storageState({ path: 'playwright/.auth/salesforce-user.json' });
});
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
// Step 1: Execute global auth setup
{
name: 'setup',
testMatch: '**/*.setup.ts',
},
// Step 2: Main test suite reusing the saved auth
{
name: 'chromium',
use: {
browserName: 'chromium',
storageState: 'playwright/.auth/salesforce-user.json',
},
dependencies: ['setup'],
},
],
});
Resilient Selectors & Page Objects
Salesforce class names are dynamically built by LWC bundlers, and standard HTML IDs auto-increment (e.g., input-241) on page redraws. Writing selectors requires specific patterns.
| Pattern | Fragile / BAD Selector | Resilient / GOOD Selector |
|---|---|---|
| Direct Form Inputs | #input-241 | page.getByLabel('Opportunity Name') |
| LWC Dropdowns | div.combobox-input > button | page.locator('lightning-combobox').filter({ hasText: 'Stage' }) |
| Visualforce Iframes | iframe[id*='ext-gen'] | page.frameLocator('iframe.vf-iframe-class') |
This custom POM encapsulates finding opportunity elements inside the custom Salesforce Lightning interface.
import { Locator, Page } from '@playwright/test';
export class OpportunityPage {
private readonly page: Page;
private readonly newButton: Locator;
private readonly nameInput: Locator;
private readonly stageCombobox: Locator;
private readonly saveButton: Locator;
constructor(page: Page) {
this.page = page;
this.newButton = page.getByRole('button', { name: 'New' });
this.nameInput = page.getByLabel('*Opportunity Name'); // Salesforce adds '*' to mandatory fields
this.stageCombobox = page.locator('lightning-combobox').filter({ hasText: 'Stage' }).locator('button');
this.saveButton = page.getByRole('button', { name: 'Save', exact: true });
}
async createSimpleOpportunity(name: string, stage: string) {
await this.newButton.click();
await this.nameInput.fill(name);
// Open combobox and select target stage option
await this.stageCombobox.click();
await this.page.getByRole('option', { name: stage }).click();
await this.saveButton.click();
}
}
API-First Data Seeding
A major flaw in most Salesforce UI suites is using the UI to create prereq test accounts, which wastes minutes. Learn how to seed records via APIs in milliseconds.
If your UI test tests Opp stages, it first needs an Account and a Contact. Doing this via the UI involves loading heavy layouts, saving, waiting for spinners, and taking up to 30 seconds.
Playwright provides a built-in `request` fixture that makes it easy to issue backend REST calls. We authenticate, create our test Accounts, and launch our UI tests immediately.
import { test, expect } from '@playwright/test';
test.describe('Speed Optimized Salesforce UI Test', () => {
let accountId: string;
// 1. Seed data before UI execution
test.beforeAll(async ({ request }) => {
const tokenResponse = await request.post('https://login.salesforce.com/services/oauth2/token', {
form: {
grant_type: 'password',
client_id: process.env.SF_CONSUMER_KEY!,
client_secret: process.env.SF_CONSUMER_SECRET!,
username: process.env.SF_USERNAME!,
password: process.env.SF_PASSWORD!
}
});
const { access_token, instance_url } = await tokenResponse.json();
// Create record via SObjects REST API
const account = await request.post(`${instance_url}/services/data/v60.0/sobjects/Account`, {
headers: {
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/json'
},
data: {
Name: 'Playwright Automation Account Inc.',
Industry: 'Technology'
}
});
const accountData = await account.json();
accountId = accountData.id; // Capture the ID!
});
// 2. Run UI Test directly visiting the record ID!
test('UI validation on created record', async ({ page }) => {
await page.goto(`/lightning/r/Account/${accountId}/view`);
// Validate UI loaded matching the API details
await expect(page.getByText('Playwright Automation Account Inc.')).toBeVisible();
});
});
Build a Salesforce Test Suite From Scratch
Learn how to initialize, configure, and code a fully working Salesforce test automation project using Playwright, Page Objects, and Session Caching.
Open your terminal inside a new folder and execute the command below to initialize Playwright. Choose **TypeScript** and **tests** folder when prompted:
# Initialize a clean Playwright project
npm init playwright@latest
# Install dotenv to securely load sandbox credentials
npm install dotenv --save-dev
Create a .env file in your project root to secure credentials, then structure your Playwright configuration to run the auth setup first.
SF_URL=https://test.salesforce.com
SF_USERNAME=qa-sdet-bot@example.com.sandbox
SF_PASSWORD=MySecurePassword123!
import { defineConfig, devices } from '@playwright/test';
import * as dotenv from 'dotenv';
dotenv.config();
export default defineConfig({
testDir: './tests',
fullyParallel: false, // Keep sequential per file to avoid record row locks
workers: 1, // Restrict sandbox concurrency
reporter: 'html',
use: {
baseURL: process.env.SF_URL,
trace: 'on-first-retry',
},
projects: [
{
name: 'setup',
testMatch: '**/*.setup.ts',
},
{
name: 'e2e',
dependencies: ['setup'],
use: {
...devices['Desktop Chrome'],
// Load the session cookies dynamically
storageState: 'playwright/.auth/salesforce-user.json',
},
},
],
});
Write the authentication script in tests/auth.setup.ts. It logs in once and caches cookies/localStorage to avoid rate limits.
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/salesforce-user.json';
setup('authenticate salesforce', async ({ page }) => {
await page.goto('/');
await page.locator('#username').fill(process.env.SF_USERNAME!);
await page.locator('#password').fill(process.env.SF_PASSWORD!);
await page.locator('#Login').click();
// Confirm home page has successfully loaded
await expect(page).toHaveURL(/lightning/);
// Cache the authenticated storage state
await page.context().storageState({ path: authFile });
});
Create a class `LeadPage.ts` inside a `pages` directory. This utilizes stable labels and accessibility roles suitable for Lightning components.
import { Locator, Page } from '@playwright/test';
export class LeadPage {
private readonly page: Page;
private readonly lastNameInput: Locator;
private readonly companyInput: Locator;
private readonly leadStatusCombobox: Locator;
private readonly saveButton: Locator;
constructor(page: Page) {
this.page = page;
this.lastNameInput = page.getByLabel('*Last Name'); // Standard LWC label matching
this.companyInput = page.getByLabel('*Company');
this.leadStatusCombobox = page.locator('lightning-combobox').filter({ hasText: 'Lead Status' }).locator('button');
this.saveButton = page.getByRole('button', { name: 'Save', exact: true });
}
async createLead(lastName: string, company: string, status: string) {
// Fill text inputs
await this.lastNameInput.fill(lastName);
await this.companyInput.fill(company);
// Expand LWC picklist and select value role
await this.leadStatusCombobox.click();
await this.page.getByRole('option', { name: status }).click();
// Save Opportunity Record
await this.saveButton.click();
}
}
Now write your automated E2E test in tests/lead.spec.ts. It uses the `LeadPage` POM and asserts the successful save toast notification.
import { test, expect } from '@playwright/test';
import { LeadPage } from '../pages/LeadPage';
test.describe('Salesforce Lead Management Suite', () => {
test('should successfully create a new Lead in Sandbox org', async ({ page }) => {
const leadPage = new LeadPage(page);
// Go directly to Lead Creation page layout using Salesforce direct routing
await page.goto('/lightning/o/Lead/new');
// Wait for the modal popup elements to be visible
await expect(page.getByText('New Lead')).toBeVisible();
// Interact using Page Object methods
await leadPage.createLead('Smith', 'Acme Corp', 'Working - Contacted');
// Assert that the success Toast alert displays correctly
const toast = page.locator('.toastMessage');
await expect(toast).toContainText('Lead "Smith" was created.');
});
});
Top Salesforce + Playwright Interview Q&A
Review standard, challenging real-world interview scenarios compiled from top-tier tech SDET rounds.
Final Assessment Quiz
Test your understanding of Salesforce QA and Playwright automation frameworks. Complete the quiz to check your interview readiness tier.
Happy Testing! 🚀