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

推荐订阅源

N
News and Events Feed by Topic
S
Schneier on Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Scott Helme
Scott Helme
V
Vulnerabilities – Threatpost
Cyberwarzone
Cyberwarzone
C
Cybersecurity and Infrastructure Security Agency CISA
Latest news
Latest news
Google Online Security Blog
Google Online Security Blog
Google DeepMind News
Google DeepMind News
K
Kaspersky official blog
Forbes - Security
Forbes - Security
T
Tenable Blog
The Last Watchdog
The Last Watchdog
T
Tor Project blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Simon Willison's Weblog
Simon Willison's Weblog
Project Zero
Project Zero
O
OpenAI News
L
LINUX DO - 热门话题
P
Privacy International News Feed
月光博客
月光博客
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
C
Cyber Attacks, Cyber Crime and Cyber Security
量子位
博客园 - 【当耐特】
罗磊的独立博客
T
Threatpost
Application and Cybersecurity Blog
Application and Cybersecurity Blog
C
Cisco Blogs
S
SegmentFault 最新的问题
博客园 - 三生石上(FineUI控件)
N
News | PayPal Newsroom
腾讯CDC
Security Latest
Security Latest
J
Java Code Geeks
L
LINUX DO - 最新话题
N
Netflix TechBlog - Medium
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
The Exploit Database - CXSecurity.com
L
Lohrmann on Cybersecurity
D
Docker
Spread Privacy
Spread Privacy
S
Security @ Cisco Blogs
A
Arctic Wolf
H
Hacker News: Front Page
Help Net Security
Help Net Security
Recorded Future
Recorded Future
V2EX - 技术
V2EX - 技术

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
Your Playwright Tests Will Need Refactoring. Here's How to Make It Painless
Dmitry · 2026-05-26 · via DEV Community

You write 50 tests. Everything works. Six months later the team grows, tests become 300, and someone changes a constructor — and you spend two days updating imports across the entire project. Sound familiar?

This isn't a discipline problem. It's an architecture problem. And it's fixable before it happens.

Code examples are simplified for clarity — focus on the idea, not the boilerplate.


TL;DR

  1. Never instantiate Page Objects with new inside tests — use fixtures
  2. Use getters instead of constructor assignments in Page Objects
  3. Seed your test data with a combination of testId + RUN_ID + repeatEachIndex for reproducibility
  4. Split fixtures by domain when the file gets large — use mergeTests
  5. Use Namespacing to avoid silent fixture name collisions

What Is a Flow? (Quick Explainer)

Before we dive in — this article uses the term Flow, which might be unfamiliar.

In a well-structured Playwright project, tests are built in three layers:

  • Page Object (POM) — knows how to interact with elements on a specific page: find a button, fill a field, click a link
  • Flow — knows how to complete a business scenario: "checkout", "register a user", "reset a password". It orchestrates Page Objects in the right sequence so tests don't have to
  • Test — just calls the Flow and checks the result

So when you see checkoutFlow.submitOrder() in a test, that one line is hiding a sequence of page navigations, form fills, and button clicks — all managed by the Flow. The test doesn't need to know the details.


The Problem: Architecture That Fights You at Scale

At 50 tests, messy architecture is invisible. At 300 tests, it becomes expensive. Two separate problems compound each other:

Data isolation breaks in parallel runs. Two workers create a user named "Ivan", one test reads the other's data, both fail. You spend an hour debugging something that has nothing to do with your application. This is a data seeding problem — solved in Rule #3.

Refactoring takes days instead of hours. Someone changes a constructor signature. Now you have 150 files to update. With modern tools this is still risky — you might miss one. This is a dependency management problem — solved in Rule #1.

Tests are impossible to read. Ten lines of setup before the actual test logic. New team members can't tell what's being tested and what's just noise. This too is a dependency management problem — when setup lives in fixtures, tests read like specifications.


Rule #1: Stop Using new Inside Tests

This is the most common pattern that makes refactoring painful:

// Every test manages its own dependencies
test('checkout', async ({ page }) => {
  const cartPage = new CartPage(page);
  const checkoutPage = new CheckoutPage(page);
  const checkoutFlow = new CheckoutFlow(cartPage, checkoutPage);

  await checkoutFlow.submitOrder();
});

Enter fullscreen mode Exit fullscreen mode

If CartPage needs a new dependency tomorrow — a logger, a config object, an API client — you update every single test that creates it. That's your two days of refactoring.

The fix: fixtures as a DI container

// fixtures.ts — one place to manage all object creation
export const test = base.extend({
  cartPage: async ({ page }, use) => {
    await use(new CartPage(page));
  },

  checkoutFlow: async ({ cartPage, checkoutPage }, use) => {
    await use(new CheckoutFlow(cartPage, checkoutPage));
  },
});

// The test reads like a specification
test('checkout', async ({ checkoutFlow }) => {
  await checkoutFlow.submitOrder();
});

Enter fullscreen mode Exit fullscreen mode

When CartPage constructor changes — you update fixtures.ts. One file. Done.

Why fixtures even when Flow seems stateless today:

Your CheckoutFlow might be pure today — no state, no side effects. But requirements change. Tomorrow it needs to track an order ID. Next month it opens a WebSocket connection that needs to be closed after the test.

If Flow is created via new in every test, adding teardown means updating hundreds of files. If it's in a fixture, you add after use cleanup in one place:

checkoutFlow: async ({ cartPage, checkoutPage }, use) => {
  const flow = new CheckoutFlow(cartPage, checkoutPage);
  await use(flow);
  await flow.cleanup(); // added in one place, applies everywhere
};

Enter fullscreen mode Exit fullscreen mode

The upfront investment is real — a few hours to set up fixtures properly. The cost of refactoring later: days, proportional to how many tests you have.

A note on pragmatism: Fixtures are for managing state and lifecycle. If you have a stateless utility function — like formatDate or a math helper — don't wrap it in a fixture. A simple ES6 import is faster and less complex. Use fixtures for things that hold a page context or require setup/teardown. Everything else is just a function.


Rule #2: Use Getters in Page Objects, Not Constructor Assignments

This is subtle but important. Most tutorials show this:

// Locator computed once at construction time
class CartPage {
  private submitButton: Locator;

  constructor(page: Page) {
    this.submitButton = page.locator('button#submit');
  }
}

Enter fullscreen mode Exit fullscreen mode

This looks fine. Playwright locators are lazy — they don't query the DOM at construction time, they query it when you interact with them. So assigning a locator in the constructor is technically safe.

The real danger is what this pattern enables — the temptation to capture actual state in the constructor:

// Never do this
constructor(page: Page) {
  (async () => {
    this.itemCount = await page.locator('.items').count(); // race condition bomb
  })();
}

Enter fullscreen mode Exit fullscreen mode

This creates an unmanaged race condition. Your test might read itemCount before the async function inside the constructor has resolved. This causes random CI failures that are nearly impossible to reproduce locally.

The fix: lazy getters

Getters are the architectural solution — not because they prevent stale locators (Playwright handles that), but because they make it structurally impossible to capture state at construction time. A getter can't be async, so you physically can't write this.itemCount = await something inside one.

// Fresh locator on every access, stateless by design
class CartPage {
  constructor(private page: Page) {}

  get submitButton() {
    return this.page.getByRole('button', { name: 'Place order' });
  }

  // Named cartItems, not itemCount — this returns a locator, not a number
  get cartItems() {
    return this.page.locator('.cart-item');
  }

  // For actual count — explicit async method, not a getter
  async getItemCount(): Promise<number> {
    return this.cartItems.count();
  }
}

Enter fullscreen mode Exit fullscreen mode

The Page Object stays stateless. Reading state is always an explicit async operation, never something that happens silently at construction time.


Rule #3: Isolate Test Data for Parallel Runs

When you run 1000 tests in parallel across multiple CI shards, data collisions are inevitable — unless you design against them.

The common mistake is using workerIndex as a seed for test data. It seems logical: each worker gets a unique number, so data should be unique. The problem is that workerIndex resets per shard. On 10 parallel CI agents, each has its own "Worker 0". Collisions are guaranteed.

The fix: combine test identity with CI build ID — not worker index

// utils/faker.utils.ts
import { TestInfo } from '@playwright/test';
import { faker } from '@faker-js/faker';

function hashCode(str: string): number {
  return str.split('').reduce((acc, char) => {
    return (Math.imul(31, acc) + char.charCodeAt(0)) | 0;
  }, 0);
}

export function seedFaker(testInfo: TestInfo) {
  const RUN_ID = process.env.RUN_ID || 'local';
  const seed = hashCode(`${testInfo.testId}-${RUN_ID}-${testInfo.repeatEachIndex}`);
  faker.seed(seed);
  return faker;
}

// fixtures.ts
export const test = base.extend({
  faker: async ({}, use, testInfo) => {
    await use(seedFaker(testInfo));
  },
});

Enter fullscreen mode Exit fullscreen mode

Three components in the seed:

  • testId — unique hash of the test file path and test name
  • RUN_ID — the CI build ID (e.g. GITHUB_RUN_ID), so different builds get different data
  • repeatEachIndex — handles retries correctly

Note: RUN_ID is an environment variable provided by your CI system — for example, GITHUB_RUN_ID in GitHub Actions. If it's missing, the code falls back to 'local', so everything works on your machine without any extra setup.

The payoff: when a test fails in CI, grab the RUN_ID from the pipeline logs, run the test locally with the same ID, and you get the exact same names, emails, and UUIDs that were generated in CI. Reproducible failures instead of "I can't reproduce this locally."


Rule #4: Structure Test Data With Factories and Overrides

Random data everywhere creates noise. If a field doesn't affect the test outcome, it shouldn't be visible in the test.

// user.factory.ts — sensible defaults
export function createUser(overrides?: Partial<User>, f = faker): User {
  return {
    id: f.string.uuid(),
    email: f.internet.email(),
    name: f.person.fullName(),
    role: 'customer',
    ...overrides,
  };
}

Enter fullscreen mode Exit fullscreen mode

// In the test — only what matters
test('VIP discount applies at checkout', async ({ checkoutFlow, faker }) => {
  const user = createUser({ role: 'vip', discount: 0.15 }, faker);
  await checkoutFlow.asUser(user).applyPromo();
});

Enter fullscreen mode Exit fullscreen mode

The test declares intent, not implementation. When you read it, you know exactly what's being tested: VIP role and discount. Everything else — name, email, UUID — is noise that the factory handles.

For data that represents specific business cases and appears repeatedly, extract it as a named dataset:

// data/datasets/users.ts
export const VIP_USER = { role: 'vip', discount: 0.15 } as const;

// In tests
const user = createUser({ ...VIP_USER }, faker);

Enter fullscreen mode Exit fullscreen mode

Pro tip: Use the satisfies operator (TypeScript 4.9+) instead of as const for datasets. It ensures your data matches the User type without losing the specific literal values — catching type errors before you even run the test:

export const VIP_USER = {
  role: 'vip',
  discount: 0.15,
} satisfies Partial<User>;

If someone adds a required field to User and forgets to update the dataset, TypeScript will tell you at compile time, not at runtime.


Rule #5: Scale Fixtures With mergeTests and Namespacing

One fixtures.ts file is fine at the start. At 20+ fixtures it becomes a 400-line file that multiple people edit simultaneously.

Split by domain:

// auth.fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

export const authTest = base.extend<{ loginPage: LoginPage }>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
});

// cart.fixtures.ts
import { test as base } from '@playwright/test';
import { CartPage } from '../pages/CartPage';

export const cartTest = base.extend<{ cartPage: CartPage }>({
  cartPage: async ({ page }, use) => {
    await use(new CartPage(page));
  },
});

// fixtures.ts — merge everything
import { mergeTests } from '@playwright/test';
import { authTest } from './auth.fixtures';
import { cartTest } from './cart.fixtures';

export const test = mergeTests(authTest, cartTest);

Enter fullscreen mode Exit fullscreen mode

Tests don't change at all — they still import from fixtures.ts. The split is purely organizational.

Watch out for name collisions:

If auth.fixtures.ts and cart.fixtures.ts both define a fixture called user, Playwright won't warn you. The last one wins silently. This creates subtle bugs that are very hard to track down.

The fix is namespacing — group fixtures by domain:

// No collision possible
import { test as base } from '@playwright/test';
import { Admin } from '../pages/Admin';
import { User } from '../pages/User';

export const test = base.extend<{ auth: { admin: Admin; user: User } }>({
  auth: async ({ page }, use) => {
    await use({
      admin: new Admin(page),
      user: new User(page),
    });
  },
});

// In tests
test('admin can manage users', async ({ auth }) => {
  await auth.admin.login();
  await auth.user.register();
});

Enter fullscreen mode Exit fullscreen mode


Rule #6: Write Business Steps, Not Technical Logs

If you use Allure or any step-based reporter, the quality of your step descriptions determines how useful the report is.

The native Playwright way is test.step():

// Technical log — describes implementation
async login() {
  await test.step('Click the login button', async () => {
    await this.page.getByRole('button', { name: 'Login' }).click();
  });
}

// Business intent — describes what happened
async loginAs(user: User) {
  await test.step(`Authenticate as "${user.username}"`, async () => {
    await this.loginPage.login(user.username, user.password);
  });
}

Enter fullscreen mode Exit fullscreen mode

The first version breaks when you rename the button. The second version remains valid even if the entire login mechanism changes from a form to SSO. The report reads like a scenario, not a DOM manipulation log.

In BDR methodology we use a @Step decorator instead of wrapping every method manually — same result, cleaner syntax. If you're interested in that approach, check it out.


ESLint: Enforce the Architecture Automatically

The best rule is one that doesn't require a code review comment:

// .eslintrc.js
module.exports = {
  overrides: [
    {
      // Only applies inside test files — won't flag Page Object factories or helpers
      files: ['tests/**/*.ts', '**/*.spec.ts'],
      rules: {
        'no-restricted-syntax': [
          'error',
          {
            selector: 'NewExpression[callee.name=/.*Page$/]',
            message: 'Use fixtures instead of new for Page Objects. See fixtures.ts.',
          },
          {
            selector: 'NewExpression[callee.name=/.*Flow$/]',
            message: 'Use fixtures instead of new for Flow objects. See fixtures.ts.',
          },
        ],
      },
    },
  ],
};

Enter fullscreen mode Exit fullscreen mode

Scoping to tests/** prevents false positives — new Pagination() in your app code won't trigger this. Only new LoginPage() inside test files will.


Architecture Cheat Sheet

Symptom Root cause Fix
Refactoring takes days new PageObject() in every test Move to fixtures
Parallel tests corrupt each other's data workerIndex as seed Seed with testId + RUN_ID
Can't reproduce CI failures locally Non-deterministic test data Seeded faker fixture
fixtures.ts is 400 lines No domain separation mergeTests + domain files
Fixture collision, wrong object used Flat fixture namespace Namespace by domain
Report is unreadable Technical step descriptions test.step() with business intent (or @Step in BDR)

What's Next?

This architecture handles the object lifecycle and data isolation. The next layer is async reliability — expect.poll, idempotency keys for parallel API calls, and cleaning up test data without relying on afterEach.

Want to go deeper? Check out the advanced version: Playwright Architecture at Scale: What Senior Engineers Do Differently


All patterns in this article are implemented in the Playwright BDR Template on GitHub.