Skip to content
CDAT 4-layer dependency diagram Components and Data feed Actions, which combine with Components and Data to feed Tests. Arrows show dependency direction. Components locators only Data types · fixtures Actions business logic Tests scenarios assertions

CDAT Pattern

Test architecture for Playwright.
Battle-tested across 9 production systems over 2 years.

4 layers · 3 zero-rules · MIT licensed

  • 0 production systems
  • 0 months in production
  • 0 hardcoded waits

Three Zero Rules

Three bright lines that keep tests reliable, readable, and maintainable. Toggle each card to compare bad versus good code.

Zero any

Type Safety Without Compromise

TypeScript any defeats the entire purpose of type checking. ESLint rule @typescript-eslint/no-explicit-any enforced. Every function signature must specify its return type. Generics over any, always.

async getProductData(): Promise<any> {
  const data = await fetchJson('/api/product');
  return data;
}
interface ProductData {
  id: string;
  price: number;
  name: string;
}

async getProductData(): Promise<ProductData> {
  return fetchJson<ProductData>('/api/product');
}
Zero waitForTimeout

Smart Waits Only

Hardcoded delays are flaky and slow. They fail on slow CI, succeed on fast dev machines, and waste time when elements load fast. Smart waits target the actual condition, not arbitrary time.

await page.waitForTimeout(5000);
await button.click();
await Cdat.waitAndClick(button);
Zero else

Early Returns Over Pyramids

Nested if-else creates pyramids of indentation that obscure logic flow. Early returns flatten the code, making each precondition explicit and the happy path obvious.

if (user.isValid) {
  if (user.hasPermission) {
    processOrder(user);
  } else {
    throw new Error('No permission');
  }
} else {
  throw new Error('Invalid user');
}
if (!user.isValid) throw new Error('Invalid user');
if (!user.hasPermission) throw new Error('No permission');
processOrder(user);

See the 4 layers in real code

A working login feature, exactly as it lives in the cdat-pattern repository. Switch tabs, copy code, drop it into your project.

/**
 * Login Feature - Components Layer
 *
 * This file contains ONLY locators and selectors.
 * No business logic, no assertions, no waits.
 *
 * @layer Components (C in CDAT)
 */

import type { Page, Locator } from '@playwright/test';

/**
 * Login page component locators
 *
 * @example
 * ```typescript
 * const components = new LoginComponents(page);
 * await components.usernameInput.fill('user');
 * ```
 */
export class LoginComponents {
  // ─────────────────────────────────────────────────────────────────
  // FORM ELEMENTS
  // ─────────────────────────────────────────────────────────────────

  /** Username/email input field */
  readonly usernameInput: Locator;

  /** Password input field */
  readonly passwordInput: Locator;

  /** Submit/login button */
  readonly submitButton: Locator;

  // ─────────────────────────────────────────────────────────────────
  // FEEDBACK ELEMENTS
  // ─────────────────────────────────────────────────────────────────

  /** Error message container */
  readonly errorMessage: Locator;

  /** Success message or redirect indicator */
  readonly successIndicator: Locator;

  // ─────────────────────────────────────────────────────────────────
  // NAVIGATION ELEMENTS
  // ─────────────────────────────────────────────────────────────────

  /** Forgot password link */
  readonly forgotPasswordLink: Locator;

  /** Register/Sign up link */
  readonly registerLink: Locator;

  /** Remember me checkbox */
  readonly rememberMeCheckbox: Locator;

  constructor(private readonly page: Page) {
    // Form elements
    this.usernameInput = page.getByLabel('Username');
    this.passwordInput = page.getByLabel('Password');
    this.submitButton = page.getByRole('button', { name: 'Sign in' });

    // Feedback elements
    this.errorMessage = page.locator('[data-testid="error-message"]');
    this.successIndicator = page.locator('[data-testid="success-indicator"]');

    // Navigation elements
    this.forgotPasswordLink = page.getByRole('link', { name: /forgot password/i });
    this.registerLink = page.getByRole('link', { name: /sign up|register/i });
    this.rememberMeCheckbox = page.getByLabel(/remember me/i);
  }

  // ─────────────────────────────────────────────────────────────────
  // COMPOSED SELECTORS (Selector Composition Pattern)
  // ─────────────────────────────────────────────────────────────────

  /**
   * Get a specific field error by field name
   *
   * @param fieldName - Name of the field (username, password)
   * @returns Locator for the field-specific error message
   */
  getFieldError(fieldName: string): Locator {
    return this.page.locator(`[data-testid="${fieldName}-error"]`);
  }
}

Why split into 4 files?

  • Separation of concerns - locators, data, behavior, and assertions never share a file.
  • Dependency direction - Components and Data have zero dependencies; Actions consume both; Tests consume all three.
  • Reusability - Actions can compose other Actions, locators can be shared, data fixtures stay typed.

CDAT vs Classic POM vs Screenplay

Apples-to-apples on the seven decisions that matter.

Aspect Classic POM Screenplay CDAT
Organization Per page Per actor Per feature (vertical slice)
Layers Mixed (1-2) 5+ (Actor, Task, Question, Ability, Interaction) 4 separated (C/D/A/T)
Locators home Page Object class Targets module components.ts (dedicated)
Test data Hardcoded in tests Question objects data.ts (typed)
Assertions Often in PO Tasks may contain Only in test.ts
Learning curve Low Steep Gentle
Best for Small projects Enterprise BDD Any project size

Learn more in /docs/architecture →

9 production systems. 2 years. Zero regrets.

CDAT has shipped tests across nine domains. Categories below are the pattern's home turf - anonymized, but real.

  • B2B Platform A

    • 12K LOC
    • 340 tests
    • 24 months
  • E-commerce 1

    • 18K LOC
    • 520 tests
    • 9 months
  • CRM Application

    • 9K LOC
    • 280 tests
    • 23 months
  • Event Management

    • 7K LOC
    • 210 tests
    • 19 months
  • Education Platform

    • 11K LOC
    • 350 tests
    • 18 months
  • Invoicing System

    • 6K LOC
    • 180 tests
    • 24 months
  • Logistics App

    • 14K LOC
    • 410 tests
    • 11 months
  • Automotive Wholesale

    • 13K LOC
    • 380 tests
    • 22 months
  • B2B Platform B

    • 10K LOC
    • 300 tests
    • 21 months