Skip to content

3 min read

Anti-patterns

Eight mistakes that show up when teams adopt CDAT, drawn from production audits across nine systems. None of these will break your build, but each one makes the suite harder to maintain over time.

1. Locators in actions.ts

// ❌ bad
async submit() {
await this.page.locator('button[type=submit]').click(); // locator inline
}

// ✅ good
async submit() {
await Cdat.waitAndClick(this.components.submitButton);
}

Locators belong in components.ts. If you put them in actions, you’ve collapsed two layers and lost the maintenance benefit.

2. Assertions in actions.ts

// ❌ bad - actions assert
async login(creds: LoginCredentials) {
await Cdat.waitAndFill(this.components.username, creds.username);
await Cdat.waitAndClick(this.components.submit);
expect(this.page).toHaveURL(/dashboard/); // wrong layer
}

// ✅ good - actions execute, tests assert
// actions.ts
async login(creds: LoginCredentials): Promise<void> {
await Cdat.waitAndFill(this.components.username, creds.username);
await Cdat.waitAndClick(this.components.submit);
}

// test.ts
test('login succeeds', async ({ page }) => {
const actions = new LoginActions(page);
await actions.login(VALID_USER);
await expect(page).toHaveURL(/dashboard/);
});

Actions return data; tests assert on it. This separation is what lets you reuse the same login() action in 30 tests.

3. Hardcoded strings in test.ts

// ❌ bad
test('login', async ({ page }) => {
await actions.login({ username: 'testuser', password: 'pw123' });
// ...
});

// ✅ good - fixtures live in data.ts
test('login', async ({ page }) => {
await actions.login(VALID_USER);
// ...
});

Test data has gravity. Hardcoded strings make refactors painful and obscure what scenario you’re testing.

4. page.locator() inside test.ts

// ❌ bad
test('error shown on bad login', async ({ page }) => {
await actions.login(INVALID_USER);
const error = page.locator('[data-testid=error]'); // locator in test
await expect(error).toBeVisible();
});

// ✅ good - locator stays in components, test reads it via actions
test('error shown on bad login', async ({ page }) => {
const actions = new LoginActions(page);
await actions.login(INVALID_USER);
await expect(actions.components.errorMessage).toBeVisible();
});

Tests should never construct locators. They consume them via the Actions/Components hierarchy.

5. any in fixtures

// ❌ bad
export const TEST_DATA: any = { ... };

// ✅ good
interface TestData { ... }
export const TEST_DATA: TestData = { ... };

any in data.ts is contagious - every consumer loses type safety.

6. Page Objects + CDAT coexistence forever

A failed migration. Half the features are CDAT, half are the old PageObject. The team re-debates conventions every sprint. Pick a deadline and finish.

7. Sub-classed PageObjects ported as sub-classed Actions

// ❌ bad
class CheckoutActions extends CartActions {
async checkout() { ... }
}

CDAT favors composition over inheritance. If CheckoutActions needs cart behavior, inject CartActions:

// ✅ good
class CheckoutActions {
private readonly cart: CartActions;
constructor(private readonly page: Page) {
  this.cart = new CartActions(page);
}
async checkout(): Promise<void> {
  await this.cart.addProduct(...);
  await Cdat.waitAndClick(this.components.checkoutButton);
}
}

8. Skipping data.ts for “trivial” features

Even a single-test feature gets all 4 files. The convention exists so newcomers can predict the layout - bypassing it for “quick wins” creates exception cases that breed more exceptions.

Production audit findings

Across nine systems, the failure modes ranked by frequency:

RankAnti-patternAudit hits
1Hardcoded strings in tests5/9
2page.waitForTimeout() calls4/9
3Locators in actions3/9
4any in data fixtures3/9
5Assertions in actions2/9

The first two account for ~70% of test debt. Fix those first.

Next steps