Mastering Playwright Automation: From Stealth Bot-Bypass Scrapers to Contiguous Multi-Tenant E2E User Journeys

Modern web applications present two extreme challenges for browser automation:
- Hostile Ingestion: Institutional enterprise portals (such as government procurement systems, banking interfaces, and public registries) deploy sophisticated anti-bot shields—Cloudflare Turnstile, Akamai Bot Manager, and behavioral fingerprinting that immediately ban cloud headless browsers.
- Brittle E2E Test Suites: Traditional software QA relies on superficial, flaky tests that click one or two buttons, fail silently when an API rejects a payload, and abruptly terminate the browser window without diagnostics.
In this deep dive, I share the battle-tested Playwright automation protocols we use across our production systems—from our stealth tender intelligence scrapers to contiguous multi-tenant commerce test suites across Twaicy and Resto.
1. Stealth Mode & Bot Detection Evasion
Standard headless Chromium launched by Playwright broadcasts its automated nature through clear diagnostic signals:
navigator.webdriver === true- Missing or static Chrome DevTools Protocol (
CDP) properties - Unnatural mouse movements (instant linear coordinate jumps)
- Cloud datacenter IP ranges (AWS, GCP, Azure CIDR blocks)
To achieve 100% extraction reliability on institutional portals (such as India's GeM tender portal), we engineered a multi-layered stealth architecture:
A. Removing Automation Footprints (CDP & Pre-Init Scripts)
Before any page script evaluates, we inject an initialization script to mask internal properties:
// stealth-context.ts
import { chromium, BrowserContext } from 'playwright';
export async function createStealthContext(): Promise<BrowserContext> {
const browser = await chromium.launch({
headless: true,
args: [
'--disable-blink-features=AutomationControlled',
'--disable-features=IsolateOrigins,site-per-process',
'--no-sandbox',
'--disable-setuid-sandbox',
],
});
const context = await browser.newContext({
userAgent:
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
viewport: { width: 1920, height: 1080 },
locale: 'en-US',
timezoneId: 'Asia/Kolkata',
});
// Mask navigator.webdriver and runtime properties
await context.addInitScript(() => {
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
});
// Mock chrome runtime object
(window as any).chrome = {
runtime: {},
app: {},
csi: () => {},
loadTimes: () => {},
};
// Mask plugins & hardware concurrency
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5],
});
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en'],
});
});
return context;
}
B. Human-Emulated Micro-Jitter
Bots click buttons instantaneously at coordinate (0, 0) of the bounding box. Humans accelerate, decelerate, and click with variable dwell time. We wrap raw Playwright clicks in a humanized curve generator:
export async function humanClick(page: Page, selector: string) {
const element = page.locator(selector);
await element.scrollIntoViewIfNeeded();
const box = await element.boundingBox();
if (!box) throw new Error(`Element ${selector} not visible for click`);
// Target slightly randomized position inside the element bounding box
const targetX = box.x + box.width * (0.3 + Math.random() * 0.4);
const targetY = box.y + box.height * (0.3 + Math.random() * 0.4);
// Smooth mouse movement with intermediate steps
await page.mouse.move(targetX, targetY, { steps: Math.floor(10 + Math.random() * 15) });
await page.waitForTimeout(50 + Math.random() * 100);
await page.mouse.down();
await page.waitForTimeout(40 + Math.random() * 60);
await page.mouse.up();
}
2. Contiguous End-to-End User Journeys (Anti-Toy Testing)
Superficial unit tests or single-button E2E tests provide false confidence. In SaaS applications handling multi-tenant finances, a bug rarely occurs in isolation—it happens at the boundaries between state transitions.
In our Twaicy test suite (twaicy-full-flow.spec.ts), we enforce Contiguous Customer Stories:
[Merchant Onboarding]
│
▼
[Storefront & Catalog Creation]
│
▼
[Customer Cart Addition & Table Token Selection]
│
▼
[Checkout & PhonePe Webhook Emulation]
│
▼
[Real-Time Kitchen KDS Chime & Status Advance]
│
▼
[Super-Admin Audit & Balance Ledger Settlement]
If any link in this contiguous chain fails, the entire journey fails.
3. The Anti-Window-Vanish Invariant & Network Interception
The most frustrating aspect of broken CI/CD pipelines is when an E2E test fails and logs only:
Error: Timed out waiting for element [data-testid="order-success"].
Why did it fail? Did the API return a 500 error? Did a Zod schema validation reject a payload? Did a rate-limiter trip?
To eliminate this blindness, we enforce the Anti-Window-Vanish Protocol:
// e2e/helpers/network-guard.ts
import { Page, Response } from '@playwright/test';
import fs from 'fs';
import path from 'path';
export async function interceptCriticalAction(
page: Page,
triggerAction: () => Promise<void>,
apiEndpointSubstring: string
): Promise<Response> {
let matchedResponse: Response | null = null;
// Set up promise to intercept targeted backend response
const responsePromise = page.waitForResponse(
(res) => res.url().includes(apiEndpointSubstring),
{ timeout: 10000 }
);
try {
await triggerAction();
matchedResponse = await responsePromise;
if (!matchedResponse.ok()) {
const errorBody = await matchedResponse.text();
console.error(
`🚨 API FAILED [${matchedResponse.status()}] at ${matchedResponse.url()}:`,
errorBody
);
throw new Error(`API rejected with status ${matchedResponse.status()}: ${errorBody}`);
}
return matchedResponse;
} catch (err) {
// Capture full-page forensic snapshot before teardown
const timestamp = Date.now();
const screenshotDir = path.resolve(process.cwd(), 'test-artifacts');
if (!fs.existsSync(screenshotDir)) fs.mkdirSync(screenshotDir, { recursive: true });
await page.screenshot({
path: path.join(screenshotDir, `failure-${timestamp}.png`),
fullPage: true,
});
console.error(`📸 Forensic screenshot captured at test-artifacts/failure-${timestamp}.png`);
throw err;
}
}
4. Headed Human Audits in Brave Browser
While headless Chromium is ideal for CI servers, local developer audits require visual verification of CSS transitions, safe-area mobile viewports, and audio chimes.
We configure Playwright to run in Brave Browser with slow-motion delay:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
workers: 1, // Single-worker sequential execution prevents race conditions
use: {
browserName: 'chromium',
executablePath: '/snap/bin/brave', // Use native Brave browser
headless: false,
slowMo: 250, // 250ms human pacing
viewport: { width: 412, height: 915 }, // Mobile-first viewport (20:9 ratio)
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
});
Summary
Playwright is significantly more than a simple test runner—it is a complete browser runtime capable of industrial scraping and bulletproof user journey verification. By applying stealth pre-initialization scripts, network response interception, and contiguous user journeys, you turn flaky automation into an enterprise engineering superpower.
- Explore Projects: Tender Intelligence | Twaicy Commerce OS
- Built by: Sarthak Srivastava (@sarvasva-dev)