Production-grade end-to-end test framework for multi-brand dealer web apps — one codebase, any number of sites.
| Feature | What it gives you | |
|---|---|---|
| 🏗️ | Page Object Model with inheritance | One base class (SitePage) shared by all pages — fix a nav locator once, every test benefits |
| 🔌 | Custom Playwright fixtures | Page objects auto-injected into every test; no new MyPage(page) boilerplate in specs |
| 🏷️ | Tag-based prod safety | @smoke tests run on production; @functional tests are dev-only — enforced in config, not convention |
| 🌐 | Multi-site switchable | Swap the target site with SITE=brand-name — no code changes, no duplicated test files |
| 🔗 | Dynamic navigation | Tests find and click into product pages at runtime — no hardcoded detail URLs |
| 📊 | Parameterized validation | Data arrays in one file generate individually-named tests for every phone and email format |
| 🧪 | Allure TestOps integration | Every test maps to a case ID for structured reporting and traceability |
| ⚙️ | Multi-project config | dev and prod Playwright projects with independent base URLs and grep locks |
playwright-multi-site-automation/
│
├── playwright.config.js # Multi-project config (dev + prod, Allure reporter)
├── package.json
├── .gitignore
│
├── config/
│ ├── constants.js # Shared timeout values (navigation, assert, test)
│ ├── env.js # Base URLs per environment (DEV_URL / PROD_URL)
│ └── site-config.js # Per-brand config (name, address pattern, footer regex)
│
├── pages/ # Page Object Model layer
│ ├── site.page.js # Base class — header, footer, nav, goto(), assertShellLoaded()
│ ├── home.page.js # Home page locators and helpers
│ ├── listing.page.js # Inventory listing page (openFirstProduct, filters)
│ └── detail.page.js # Product detail page (carousel, form, accordion, CTAs)
│
└── tests/
├── support/
│ ├── testContext.js # Custom test fixture — injects site/home/listing/detail
│ ├── allureTestOps.js # Allure TestOps case ID helper (brand / group / caseId)
│ └── validation-data.js # Phone and email test data arrays (valid + invalid)
│
└── acme/ # Test suite for "Acme Dealer" (one folder per brand)
├── home.spec.js # Home page smoke + functional tests
├── listing.spec.js # Inventory listing tests
└── detail.spec.js # Product detail — 27 smoke + parameterized functional tests
npm install && npx playwright install --with-deps chromium
npm test # all tests on dev
npm run report # open HTML report| Requirement | Version | Notes |
|---|---|---|
| Node.js | 18 or higher | node --version to check |
| npm | Bundled with Node | npm --version to check |
| Playwright browsers | Installed via CLI | Chromium only by default |
| A Playwright-targeted site | Any | Configure the URL in config/env.js |
Mac / Linux
git clone https://github.com/YOUR_USERNAME/playwright-multisite-e2e.git
cd playwright-multisite-e2eWindows (PowerShell)
git clone https://github.com/YOUR_USERNAME/playwright-multisite-e2e.git
cd playwright-multisite-e2enpm installThis installs
@playwright/test,allure-playwright, andallure-js-commonsas dev dependencies.
npx playwright install --with-deps chromium
--with-depsalso installs OS-level browser dependencies. On CI or Docker you may need to runnpx playwright install-depsseparately.
Edit config/env.js:
module.exports = {
dev: process.env.DEV_URL || 'https://your-site-dev.example.com',
prod: process.env.PROD_URL || 'https://your-site.example.com',
};Or pass URLs inline without touching the file:
Mac / Linux
DEV_URL=https://staging.mysite.com npm testWindows (PowerShell)
$env:DEV_URL="https://staging.mysite.com"; npm testWindows (Command Prompt)
set DEV_URL=https://staging.mysite.com && npm testNote: Never commit real credentials to
env.js. Use a.envfile (already in.gitignore) or CI secrets for sensitive values.
Edit config/site-config.js to match your site's brand name and content patterns:
const SITE_CONFIGS = {
acme: {
brandName: 'Acme Dealer',
footerDealerPattern: /Acme\s*Dealer|Primary\s*Location/i,
addressPattern: /123\s+Main\s+St|Anytown/i,
},
// Add your brand here:
mybrand: {
brandName: 'My Brand',
footerDealerPattern: /My\s*Brand|Main\s*Location/i,
addressPattern: /456\s+Oak\s+Ave|Somecity/i,
},
};After npm test you should see:
Running 33 tests using 4 workers
✓ detail page loads with a valid URL and title (1.2s)
✓ H1 product title is visible and non-empty (0.9s)
✓ carousel has a next button and at least one thumbnail (1.1s)
...
33 passed (28.4s)
An HTML report is generated at playwright-report/index.html. Open it with:
npm run report| Command | What it does |
|---|---|
npm test |
All tests against the dev project |
npm run test:prod |
@smoke tests only against prod (grep-locked in config) |
npm run test:smoke |
@smoke tests on dev |
npm run test:func |
@functional tests on dev (form validation, state mutations) |
npm run test:ui |
Playwright UI mode — step through tests visually |
npm run report |
Open the last HTML report in your browser |
npm run report:allure |
Serve the Allure report (requires Allure CLI installed) |
Switch sites at runtime:
Mac / Linux
SITE=mybrand npm testWindows (PowerShell)
$env:SITE="mybrand"; npm testRun a specific spec file:
npx playwright test tests/acme/detail.spec.jsRun tests by tag:
npx playwright test --grep "@smoke"
npx playwright test --grep "@functional"Page Object Model with inheritance
All page objects extend SitePage (pages/site.page.js), which owns shared shell locators — header, navigation, footer — and the goto() and assertShellLoaded() helpers. Individual page classes only define what's unique to that page.
This means a change to the nav selector fixes every test at once. It also means the base class can evolve independently of the page-specific logic.
class ListingPage extends SitePage {
get firstViewDetailsLink() {
return this.page.getByRole('main').getByRole('link', { name: /view details/i }).first();
}
async openFirstProduct() {
await expect(this.firstViewDetailsLink).toBeVisible();
await this.firstViewDetailsLink.scrollIntoViewIfNeeded();
await this.firstViewDetailsLink.click();
}
}Custom fixtures — zero boilerplate in specs
tests/support/testContext.js extends Playwright's base test with four fixtures: site, home, listing, and detail. Every spec imports test from testContext and receives page objects automatically — no new MyPage(page) in tests.
// tests/support/testContext.js
const test = base.test.extend({
home: async ({ page }, use) => { await use(new HomePage(page)); },
listing: async ({ page }, use) => { await use(new ListingPage(page)); },
detail: async ({ page }, use) => { await use(new DetailPage(page)); },
});// In any spec file
const { test, expect } = require('../support/testContext');
test('product detail page loads', async ({ listing, detail }) => {
await listing.openFirstProduct();
await expect(detail.titleH1).toBeVisible();
});To add a new page object: create the class, import it in testContext.js, add one fixture entry. No changes needed in individual spec files.
Tag-based prod safety
Every test suite is tagged at the describe level:
// Safe to run on production — read-only, no side effects
test.describe('Acme — Detail', { tag: ['@ui', '@smoke'] }, () => { ... });
// Dev only — submits forms, mutates state
test.describe('Acme — Detail (Functional)', { tag: ['@ui', '@functional'] }, () => {
test.fixme(true, 'Re-enable once form bug is resolved on dev');
...
});playwright.config.js locks the prod project to @smoke via a grep filter, checked at startup:
const prodSafeMode = process.env.PLAYWRIGHT_GREP_OVERRIDE !== 'true';
// ...
{ name: 'prod', grep: prodSafeMode ? /@smoke/ : undefined }No one has to remember to add a flag — production is safe by default.
Parameterized validation tests
Phone and email test data lives in one file (tests/support/validation-data.js). A for loop over the array generates a distinctly-named test per entry — so 6 invalid phone formats becomes 6 test results, each clearly labeled in the report.
for (const [phone, reason] of INVALID_PHONES) {
test(`invalid phone: ${reason} — "${phone}" shows error`, async ({ detail }) => {
await detail.phoneInput.fill(phone);
await detail.submitButton.click();
await expect(detail.formErrorBanner).toBeVisible();
});
}Adding a new validation case means adding one entry to the data array — the test is generated automatically.
Multi-site architecture
The framework is designed to test multiple brands from one codebase. The SITE environment variable selects the active brand at runtime. playwright.config.js auto-discovers site folders under tests/ and scopes test matching to the selected brand:
const siteKey = (process.env.SITE || 'acme').toLowerCase().trim();
const hasDedicatedFolder = fs.existsSync(path.join(testsDir, siteKey));
module.exports = defineConfig({
testMatch: hasDedicatedFolder ? `${siteKey}/**/*.spec.js` : undefined,
...
});config/site-config.js stores per-brand patterns (brand name, footer regex, address regex) so page objects can use flexible locators without hardcoding brand-specific strings.
To add a new brand: create tests/mybrand/, add its config to site-config.js, and run SITE=mybrand npm test. Page objects require no changes unless the new site has a different UI structure.
| Variable | Default | Description |
|---|---|---|
DEV_URL |
https://acme-dealer-dev.example.com |
Base URL for the dev Playwright project |
PROD_URL |
https://acme-dealer.example.com |
Base URL for the prod Playwright project |
BASE_URL |
— | Override both projects with a single URL (useful in CI) |
SITE |
acme |
Selects the active brand folder under tests/ |
PLAYWRIGHT_GREP_OVERRIDE |
false |
Set to true to run functional tests on prod (use with caution) |
CI |
— | When set, enables 1 retry per test |
All timeouts are defined in config/constants.js:
const TIMEOUTS_MS = {
navigation: 30_000, // page.goto() and waitForURL()
assert: 15_000, // expect() and actionTimeout
test: 90_000, // overall test timeout
};Allure results are written to allure-results/ after each run. To view them:
# Install Allure CLI once
npm install -g allure-commandline
# Serve the report
npm run report:allureEach test includes a structured case ID set via applyAllureTestOpsMapping():
await applyAllureTestOpsMapping({
brand: 'Acme',
group: 'Acme-Detail',
caseId: 'Acme-Detail-012', // 001-099: smoke, 100+: functional
});| Tool | Version | Purpose |
|---|---|---|
| Playwright | v1.56+ | Browser automation and test runner |
| Allure Playwright | v3.0+ | Rich test reporting with case traceability |
| allure-js-commons | v3.0+ | Allure label/annotation helpers |
| Node.js | 18+ | Runtime |
| CommonJS modules | — | No build step or transpilation required |
Built with Playwright · Reported with Allure · Structured for real-world scale