惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
罗磊的独立博客
MyScale Blog
MyScale Blog
博客园 - 叶小钗
U
Unit 42
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
有赞技术团队
有赞技术团队
F
Fortinet All Blogs
WordPress大学
WordPress大学
美团技术团队
GbyAI
GbyAI
L
LangChain Blog
T
The Blog of Author Tim Ferriss
P
Proofpoint News Feed
Y
Y Combinator Blog
V
Visual Studio Blog
小众软件
小众软件
D
Docker
量子位
博客园_首页
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Engineering at Meta
Engineering at Meta
N
Netflix TechBlog - Medium
M
MIT News - Artificial intelligence
人人都是产品经理
人人都是产品经理
C
CERT Recently Published Vulnerability Notes
AI
AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Know Your Adversary
Know Your Adversary
Vercel News
Vercel News
C
Check Point Blog
I
InfoQ
NISL@THU
NISL@THU
Webroot Blog
Webroot Blog
S
Security Affairs
Stack Overflow Blog
Stack Overflow Blog
V
Vulnerabilities – Threatpost
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
TaoSecurity Blog
TaoSecurity Blog
L
Lohrmann on Cybersecurity
Hacker News: Ask HN
Hacker News: Ask HN
C
CXSECURITY Database RSS Feed - CXSecurity.com
N
News and Events Feed by Topic
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
W
WeLiveSecurity
V2EX - 技术
V2EX - 技术
SecWiki News
SecWiki News
PCI Perspectives
PCI Perspectives
S
Secure Thoughts
Apple Machine Learning Research
Apple Machine Learning Research

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
The Playwright Playbook — Part 1: Stop Writing Playwright Tests Like a Beginner
Faizal · 2026-06-15 · via DEV Community

The Playwright Playbook — Part 1: Stop Writing Playwright Tests Like a Beginner

"Most people aren't writing bad tests on purpose. They just never learned the right way."

I've reviewed a lot of Playwright test suites over 7.6 years in QA automation.

And I keep seeing the same mistakes. Over and over.

Not from junior engineers. From seniors. From teams that have been using Playwright for years.

The tests run. The CI is green. But the suite is secretly fragile — one UI change away from mass failure, one new feature away from a 40-minute login loop, one scale-up away from complete chaos.

This series is called The Playwright Playbook for a reason.

Playbooks aren't for beginners learning how to play. They're for players who already know the basics — and want to win. 🏆

By the end of this 8-part series, you'll have a production-grade Playwright framework in TypeScript — with network interception, multi-user testing, API integration, visual regression, a CI/CD pipeline, and AI-powered test agents on top.

But first — we need to tear down what's broken.

Let's go. 🎯


🏗️ What We're Building in This Series

Before we write a single line of code — let me show you the app we're going to test throughout all 8 parts.

We'll use a simple Task Manager app as our test target. It has:

  • Login / logout
  • Create, edit, delete tasks
  • A REST API underneath
  • Role-based access (admin vs regular user)
  • Real-time task updates

Every part of this series will add a new testing layer on top of this same project. By Part 8 — we'll have a complete, professional test framework built around it.

Here's the project structure we'll end up with by Part 1:

playwright-playbook/
├── tests/
│   ├── auth/
│   │   └── login.spec.ts
│   └── tasks/
│       └── task-management.spec.ts
├── pages/
│   ├── LoginPage.ts
│   └── TaskPage.ts
├── fixtures/
│   └── auth.fixture.ts
├── .auth/
│   ├── admin.json
│   └── user.json
├── playwright.config.ts
└── .env

Clean. Scalable. Ready to grow. Let's build it. 👇


❌ Mistake #1 — Fragile Selectors

This is the most common mistake in every codebase I've ever reviewed.

The bad way:

// 🔴 Brittle — breaks on any CSS refactor
await page.click('.btn-primary.submit-button-v2');

// 🔴 Brittle — breaks when DOM structure changes
await page.click('div > form > div:nth-child(3) > button');

// 🔴 Brittle — relies on auto-generated class names
await page.fill('#input_38', 'john@test.com');

These selectors are tied to implementation details. The moment a developer renames a class, restructures the DOM, or upgrades a UI library — your tests break. Not because the feature is broken. Because your test is brittle.

The right way — Playwright's built-in locators:

// ✅ Finds by accessible role — survives CSS changes
await page.getByRole('button', { name: 'Sign in' }).click();

// ✅ Finds by label — semantic and resilient
await page.getByLabel('Email address').fill('john@test.com');

// ✅ Finds by placeholder text
await page.getByPlaceholder('Enter your password').fill('secret123');

// ✅ Finds by visible text
await page.getByText('Welcome back, John').waitFor();

// ✅ The best option when you control the code — test IDs never change
await page.getByTestId('submit-btn').click();

getByRole, getByLabel, getByTestId — these are semantic locators. They describe what the element IS, not what it looks like. They survive redesigns. They survive framework upgrades. They survive the inevitable "quick CSS cleanup" from the frontend team.

Rule of thumb: If a non-technical person couldn't describe the element using your selector — it's probably fragile. 🎯


❌ Mistake #2 — Logging In Through the UI on Every Test

This one kills performance and introduces flakiness at the same time.

The bad way:

// 🔴 Every single test file has this at the top
test.beforeEach(async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@test.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('/dashboard');
});

Multiply this by 50 tests. That's 50 UI login flows. Each one making real network requests, waiting for page loads, and praying the login page doesn't have a hiccup today.

The right way — storageState:

First, create a global setup file that logs in once and saves the authenticated session:

// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();

  // Save admin session
  const adminContext = await browser.newContext();
  const adminPage = await adminContext.newPage();
  await adminPage.goto('http://localhost:3000/login');
  await adminPage.getByLabel('Email').fill('admin@test.com');
  await adminPage.getByLabel('Password').fill('admin123');
  await adminPage.getByRole('button', { name: 'Sign in' }).click();
  await adminPage.waitForURL('/dashboard');
  await adminContext.storageState({ path: '.auth/admin.json' });

  // Save regular user session
  const userContext = await browser.newContext();
  const userPage = await userContext.newPage();
  await userPage.goto('http://localhost:3000/login');
  await userPage.getByLabel('Email').fill('user@test.com');
  await userPage.getByLabel('Password').fill('user123');
  await userPage.getByRole('button', { name: 'Sign in' }).click();
  await userPage.waitForURL('/dashboard');
  await userContext.storageState({ path: '.auth/user.json' });

  await browser.close();
}

export default globalSetup;

Now wire it into playwright.config.ts:

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  globalSetup: './global-setup.ts',

  projects: [
    // Admin tests — pre-authenticated
    {
      name: 'admin',
      use: {
        storageState: '.auth/admin.json',
      },
      testMatch: '**/admin/**/*.spec.ts',
    },
    // User tests — pre-authenticated
    {
      name: 'user',
      use: {
        storageState: '.auth/user.json',
      },
      testMatch: '**/user/**/*.spec.ts',
    },
  ],
});

Now your tests start already logged in. Zero UI login overhead. Zero auth flakiness. 🚀


❌ Mistake #3 — No Project Structure (Everything in One File)

I've seen test files with 800 lines. Every test copy-pasting the same page.goto, page.fill, page.click sequences.

When the URL changes — you update it in 47 places. When the selector changes — same story.

The right way — Page Object Model (POM) in TypeScript:

// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly signInButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email address');
    this.passwordInput = page.getByLabel('Password');
    this.signInButton = page.getByRole('button', { name: 'Sign in' });
    this.errorMessage = page.getByTestId('login-error');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.signInButton.click();
  }

  async loginAndWait(email: string, password: string) {
    await this.login(email, password);
    await this.page.waitForURL('/dashboard');
  }
}

// pages/TaskPage.ts
import { Page, Locator } from '@playwright/test';

export class TaskPage {
  readonly page: Page;
  readonly newTaskButton: Locator;
  readonly taskTitleInput: Locator;
  readonly saveTaskButton: Locator;

  constructor(page: Page) {
    this.page = page;
    this.newTaskButton = page.getByRole('button', { name: 'New Task' });
    this.taskTitleInput = page.getByLabel('Task title');
    this.saveTaskButton = page.getByRole('button', { name: 'Save task' });
  }

  async goto() {
    await this.page.goto('/tasks');
  }

  async createTask(title: string) {
    await this.newTaskButton.click();
    await this.taskTitleInput.fill(title);
    await this.saveTaskButton.click();
  }

  getTaskLocator(title: string): Locator {
    return this.page.getByRole('listitem').filter({ hasText: title });
  }
}

Now your tests are clean and readable:

// tests/tasks/task-management.spec.ts
import { test, expect } from '@playwright/test';
import { TaskPage } from '../../pages/TaskPage';

test('user can create a new task', async ({ page }) => {
  const taskPage = new TaskPage(page);

  await taskPage.goto();
  await taskPage.createTask('Write unit tests');

  await expect(taskPage.getTaskLocator('Write unit tests')).toBeVisible();
});

One change to TaskPage.ts — every test that uses it gets updated. That's the power of POM. 💪


❌ Mistake #4 — Hard-Coded Waits

Nothing says "I don't trust my tests" like waitForTimeout.

The bad way:

// 🔴 Hoping 3 seconds is enough. It isn't. On a slow CI machine, it never is.
await page.waitForTimeout(3000);
await page.click('#submit');

// 🔴 Even worse — guessing at load time
await page.goto('/dashboard');
await page.waitForTimeout(5000);
await expect(page.locator('.tasks-list')).toBeVisible();

Hard-coded waits are the definition of flaky. Fast machine? Test passes. Slow CI? Fails. High network load? Fails. The test isn't measuring anything — it's just hoping.

The right way — Playwright's auto-wait and explicit conditions:

// ✅ Wait for a specific element to appear
await page.getByTestId('tasks-list').waitFor({ state: 'visible' });

// ✅ Wait for a network response to complete
await Promise.all([
  page.waitForResponse(resp => resp.url().includes('/api/tasks') && resp.status() === 200),
  page.getByRole('button', { name: 'Load tasks' }).click(),
]);

// ✅ Wait for URL to change — confirms navigation completed
await page.waitForURL('/dashboard');

// ✅ Wait for load state — page fully loaded
await page.waitForLoadState('networkidle');

// ✅ Playwright auto-waits before most actions — this just works
await expect(page.getByRole('heading', { name: 'My Tasks' })).toBeVisible();

Playwright's built-in locator methods already auto-wait. click(), fill(), check() — they all wait for the element to be actionable before acting. You rarely need to manually wait for anything.

If you find yourself writing waitForTimeout — stop. Find the condition you're actually waiting for. Wait for that instead. 🎯


❌ Mistake #5 — Hardcoded Config Everywhere

// 🔴 Scattered across 30 test files
await page.goto('http://localhost:3000/login');
await request.post('http://localhost:3000/api/tasks');

The moment you need to run tests against staging — you're doing a find-and-replace across your entire codebase.

The right way — centralized playwright.config.ts + .env:

# .env
BASE_URL=http://localhost:3000
API_URL=http://localhost:3000/api
ADMIN_EMAIL=admin@test.com
ADMIN_PASSWORD=admin123
USER_EMAIL=user@test.com
USER_PASSWORD=user123

// playwright.config.ts
import { defineConfig } from '@playwright/test';
import dotenv from 'dotenv';

dotenv.config();

export default defineConfig({
  use: {
    baseURL: process.env.BASE_URL,
    // Screenshot on failure — always
    screenshot: 'only-on-failure',
    // Video on failure — invaluable for CI debugging
    video: 'retain-on-failure',
    // Full trace on first retry
    trace: 'on-first-retry',
  },
  // Retry flaky tests once in CI
  retries: process.env.CI ? 1 : 0,
  // Run tests in parallel
  workers: process.env.CI ? 4 : undefined,
  reporter: [
    ['html', { open: 'never' }],
    ['list'],
  ],
});

Now your tests use relative paths:

// ✅ baseURL is resolved from config — works in any environment
await page.goto('/login');
await page.goto('/dashboard');

Switch environments by changing one line in .env. Or one environment variable in CI. That's it. ✅


❌ Mistake #6 — Writing Assertions That Don't Actually Assert Anything

// 🔴 This passes even if the element is invisible, disabled, or wrong
const element = await page.locator('.task-item');
expect(element).toBeTruthy(); // The locator object always exists!

// 🔴 Asserting the wrong thing
await page.click('button[type="submit"]');
// No assertion after this — just hoping the action worked

The right way — Playwright's web-first assertions:

// ✅ Checks actual visibility in the DOM
await expect(page.getByTestId('task-item')).toBeVisible();

// ✅ Checks the actual text content
await expect(page.getByRole('heading')).toHaveText('My Tasks');

// ✅ Checks element count
await expect(page.getByRole('listitem')).toHaveCount(3);

// ✅ Checks URL after navigation
await expect(page).toHaveURL('/dashboard');

// ✅ Checks input value
await expect(page.getByLabel('Task title')).toHaveValue('Write unit tests');

// ✅ Checks element is NOT visible (for negative assertions)
await expect(page.getByTestId('error-message')).not.toBeVisible();

// ✅ Checks element is disabled
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled();

Playwright's expect() assertions are web-first — they automatically retry until the condition is met or the timeout is reached. No manual waiting. No false passes.


❌ Mistake #7 — Tests That Depend on Each Other

// 🔴 Test 2 depends on Test 1 having run first
test('create a task', async ({ page }) => {
  await taskPage.createTask('Buy groceries');
});

test('delete the task', async ({ page }) => {
  // What if Test 1 failed? Or ran in a different order?
  await taskPage.deleteTask('Buy groceries');
});

Dependent tests are a maintenance nightmare. Run them in parallel — they break. Run them in a different order — they break. One failure cascades into many.

The right way — fully independent, self-contained tests:

// ✅ Each test creates its own data, asserts, and cleans up
test('user can delete a task', async ({ page }) => {
  const taskPage = new TaskPage(page);

  // Arrange — create the task this test needs
  await taskPage.goto();
  await taskPage.createTask('Temporary task for deletion test');
  await expect(taskPage.getTaskLocator('Temporary task for deletion test')).toBeVisible();

  // Act — delete it
  await taskPage.deleteTask('Temporary task for deletion test');

  // Assert — confirm it's gone
  await expect(taskPage.getTaskLocator('Temporary task for deletion test')).not.toBeVisible();
});

Every test owns its lifecycle. Setup → action → assertion → done. No shared state. No assumptions about what ran before. ✅


🏁 The Full Project Setup

Here's everything pulled together. This is what your project should look like by the end of Part 1:

// playwright.config.ts — the complete config
import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';

dotenv.config();

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 1 : 0,
  workers: process.env.CI ? 4 : undefined,
  globalSetup: './global-setup.ts',

  reporter: [
    ['html', { open: 'never' }],
    ['list'],
  ],

  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'on-first-retry',
  },

  projects: [
    {
      name: 'admin',
      use: {
        ...devices['Desktop Chrome'],
        storageState: '.auth/admin.json',
      },
    },
    {
      name: 'user',
      use: {
        ...devices['Desktop Chrome'],
        storageState: '.auth/user.json',
      },
    },
  ],
});

# Final project structure after Part 1

playwright-playbook/
├── tests/
│   ├── auth/
│   │   └── login.spec.ts
│   └── tasks/
│       └── task-management.spec.ts
├── pages/
│   ├── LoginPage.ts
│   └── TaskPage.ts
├── .auth/                    ← git-ignored, generated by globalSetup
│   ├── admin.json
│   └── user.json
├── global-setup.ts
├── playwright.config.ts
├── .env                      ← git-ignored
├── .env.example              ← committed to repo
└── package.json


🗺️ What's Coming in This Series

We've set the foundation. Now we build. 🏗️

Part 1 — Stop Writing Tests Like a Beginner              ← You are here
Part 2 — Network Interception: The Complete Guide
Part 3 — Multi-User, Multi-Tab & Context Testing
Part 4 — API Testing (The Underrated Superpower)
Part 5 — Visual Regression Testing
Part 6 — Debugging Like a Pro: Trace Viewer & Inspector
Part 7 — The CI/CD Setup Nobody Shows You
Part 8 — Playwright Meets AI: Agents, MCP & Self-Healing Tests

In Part 2, we go deep on network interception — mocking APIs, simulating failures, asserting on real network calls, and recording HAR files. It's where the real power of Playwright starts to show.


🎯 Who Is This Series For?

  • QA engineers who are already using Playwright but know their suite could be better
  • Automation engineers who want to level up from "it works" to "it scales"
  • Developers who write Playwright tests and want to do it properly
  • Anyone who has inherited a Playwright codebase and wondered why it keeps breaking

No beginner content here. We skip the "what is Playwright" intro.

If you can write a test — this series will make you dangerous. 🔥


🔖 Before You Go

Every pattern in this article is something I've fixed in a real codebase.

The hard selectors. The login loops. The waitForTimeout scattered everywhere. The 800-line test files. I've seen all of it — and I've cleaned all of it.

The good news: fixing these isn't complicated. It's just about knowing the right patterns.

Now you know them. 💪


Follow me so you don't miss Part 2 — where we get hands-on with Playwright's most underused feature: network interception. We'll mock APIs, simulate 500 errors, and test frontend behaviour without touching a single line of backend code.

Drop a comment below 👇

  • Which of these mistakes is your team making right now?
  • What's the most painful part of maintaining your Playwright suite?
  • Or are you starting fresh — building a new framework from scratch?

All levels welcome here. Let's build something solid together. 🙌


Faizal Shaikh | Senior Automation Engineer | Playwright & AI Testing
Connect with me on LinkedIn