Cztery pliki, jedno zadanie każdy

CDAT jest strukturalnie nudny. Cztery pliki na feature, każdy z jednym zadaniem, i graf zależności, który prowadzi tylko w jedną stronę. Nuda jest tu całym sensem.

Nazwa to układ. Components, Data, Actions, Tests.

Cztery warstwy

Components. Lokatory i nic więcej. Żadnych waitów, żadnej logiki, żadnych asercji. Jeśli znajdziesz tu click(), to już zacząłeś god-object w przebraniu.

export class LoginComponents {
  readonly usernameInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;

  constructor(private readonly page: Page) {
    this.usernameInput = page.getByLabel('Username');
    this.passwordInput = page.getByLabel('Password');
    this.submitButton = page.getByRole('button', { name: 'Sign in' });
    this.errorMessage = page.locator('[data-testid="error-message"]');
  }
}

Data. Typy, dane testowe, enumy, URL-e. Żadnych lokatorów, żadnego DOM. To jest też pierwszy plik, który czyta asystent AI, kiedy próbuje zrozumieć feature, i to nie jest przypadek.

export interface LoginCredentials {
  username: string;
  password: string;
  rememberMe?: boolean;
}

export enum LoginErrorType {
  InvalidCredentials = 'Invalid username or password',
  RequiredField = 'This field is required',
}

export const VALID_USER: LoginCredentials = { username: 'testuser', password: 'Password123!' };
export const INVALID_CREDENTIALS: LoginCredentials = { username: 'wronguser', password: 'wrongpassword' };

Actions. Logika biznesowa zbudowana ze smart-waitów. Wykonuje. Nigdy nie asertuje. Gettery stanu zwracają dane, które ocenia test.

export class LoginActions {
  private readonly components: LoginComponents;

  constructor(private readonly page: Page) {
    this.components = new LoginComponents(page);
  }

  async login(credentials: LoginCredentials): Promise<void> {
    await Cdat.waitAndFill(this.components.usernameInput, credentials.username);
    await Cdat.waitAndFill(this.components.passwordInput, credentials.password);
    await Cdat.waitAndClick(this.components.submitButton);
  }

  // zwraca dane, nigdy nie asertuje
  async getErrorMessage(): Promise<string> {
    const visible = await Cdat.checkState(this.components.errorMessage, LocatorState.Visible);
    if (!visible) return '';
    return Cdat.waitForText(this.components.errorMessage);
  }
}

Tests. Scenariusze i każdy expect() w całym kodzie. Jedyne miejsce, gdzie intencja biznesowa i asercja mieszkają razem.

test('TC_LOGIN_003: invalid credentials, error shown', async () => {
  await actions.login(INVALID_CREDENTIALS);

  const error = await actions.getErrorMessage();
  expect(error).toContain(LoginErrorType.InvalidCredentials);
});

Reguła zależności, która kupuje reużywalność

Graf zależności czterech warstw CDAT: Components, Data, Actions, Tests, importy tylko w jedną stronę

Niższe warstwy nigdy nie importują wyższych. Components i Data nie wiedzą nic o Actions. Actions nie wie nic o Tests. Ta jedna reguła sprawia, że jedna akcja login() jest reużywana w trzydziestu testach, a nikt nie eksponuje wnętrzności ani nie kopiuje metody.

Szew, na jednym przykładzie

Oto login jako jedna metoda od wszystkiego. Każda struktura zwija się do tego pod presją dowozu. To uniwersalny kształt, nie cecha jednego wzorca.

class LoginPage {
  async login(user: string, pass: string) {
    await this.usernameInput.fill(user);
    await this.passwordInput.fill(pass);
    await this.submitButton.click();
    await expect(this.dashboard).toBeVisible(); // asercja mieszka w środku metody
  }
}

Ta ostatnia linia to sygnał. Metoda ma teraz zdanie na temat tego, co znaczy sukces. Test, który chce zasertować coś innego, musi tę metodę owinąć albo skopiować.

Oto to samo zachowanie w CDAT. Akcja wykonuje, test ocenia.

// actions.ts - wykonuje, nie asertuje
async login(creds: LoginCredentials): Promise<void> {
  await Cdat.waitAndFill(this.components.username, creds.username);
  await Cdat.waitAndClick(this.components.submit);
}

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

Teraz trzydzieści testów może wywołać login(VALID_USER) i każdy asertuje to, czego potrzebuje. Nic zduplikowane. Nic wyeksponowane. To jest cały handel, zrobiony raz, na granicy warstwy.

Jeden test, wszystkie cztery warstwy

Prześledź TC_LOGIN_003 i przekraczasz każdą warstwę dokładnie raz.

Test prosi o INVALID_CREDENTIALS i enum LoginErrorType.InvalidCredentials, oba z data. Uruchamia login() i getErrorMessage() z actions. Te sięgają po lokatory usernameInput, submitButton i errorMessage w components. Waity pod spodem pochodzą ze smart-wait helperów Cdat, więc żaden hardkodowany delay nie dotyka testu. Jedyny expect() zostaje w test.

Cztery pliki, jedna ścieżka, każda warstwa robi dokładnie jedno.

Sześć reguł, które trzymają to w ryzach

Lokatory mieszkają w components.ts. Nigdy nie buduj lokatora w teście ani w akcji.

Actions wykonują, testy asertują. Żadnego expect() poza test.ts.

Dane testowe mieszkają w data.ts. Żadnych hardkodowanych credentiali w teście.

Zero waitForTimeout. Czekaj na warunek, nie na zegar.

Zero any. Jeden any w data.ts jest zaraźliwy; każdy konsument w dół traci typy.

Komponuj, nie dziedzicz. Jeśli checkout potrzebuje koszyka, wstrzyknij koszyk, nie dziedzicz po nim.

To są wytyczne, nie religia. Uczciwe dane z dziewięciu systemów: licznik else chodzi od 9 do 45 na repo, a trzy z pięciu zespołów trzymają regułę zero-any jako ostrzeżenie, nie blokadę builda. Traktuj reguły jako smell na code review, nie jako bramkę CI, a zachowasz i zespół, i większość korzyści.

Dowód, że nie budujesz kolejnego monolitu

W zestawie przykładów warstwy ważą podobnie. Data 1636 linii. Components 1343. Actions 1574. Tests 1234.

Żaden pojedynczy plik nie zjada feature’a. Ta równowaga to strukturalne przeciwieństwo god-objectu, i to jest rzecz, którą realnie kupujesz. Monolit ma jeden plik na cztery tysiące linii i trzy na pięćdziesiąt. CDAT rozkłada ciężar celowo.

Gdzie agent zarabia na siebie

To jest argument context-before-LLM postawiony konkretnie.

Wyceluj agenta w feature CDAT i zadanie jest dobrze postawione. “Dodaj negatywny test dla zablokowanego konta.” Wie, że lokator idzie do components.ts, dane do data.ts, flow do actions.ts, asercja do test.ts. Cztery małe jednozadaniowe pliki to cztery małe dobrze zakresowane prompty.

Wyceluj tego samego agenta w 1200-linijkową klasę od wszystkiego i zadanie staje się “dodaj test gdzieś tutaj i postaraj się nie popsuć pozostałych trzydziestu”. Jedno jest kontraktem. Drugie jest nadzieją. Model jest tym samym modelem. To architektura zmieniła szanse.

Koszt, powiedziany wprost

Cztery pliki na feature, który kiedyś trzymała jedna metoda. Nawet jednotestowy feature dostaje wszystkie cztery, celowo, żeby nowy mógł przewidzieć układ zamiast uczyć się każdego wyjątku. Jeśli twój zespół nie będzie pilnował granic, CDAT nie uratuje cię w niczym. Napiszesz 1500-linijkowy actions.ts zamiast 1500-linijkowego page object i nauczysz się tylko nowej nazwy folderu.

Jest też opiniowany pod TypeScript. Wersja JavaScript działa, ale tracisz regułę zero-any i mniej więcej jedną trzecią wartości.

Część 3 to ta część, na której zależy twojemu Head of QA. Ile to kosztuje na wejściu, co zwraca w utrzymaniu, i dlaczego decyzja o architekturze jest teraz decyzją o AI.

From the Field to jest to, co realnie buduję, co się psuje i czego się uczę. Realne projekty, realne liczby, realne bugi. Bez tutoriali.