strictNullChecks в TypeScript заставляет обрабатывать случай когда textContent() элемента возвращает null, вместо того чтобы text.length падало посреди теста с Cannot read properties of null. Типизированные возвращаемые значения методов page-объектов ловят другую распространённую ошибку: loginWith() возвращающий Page вместо DashboardPage означает что тесты не могут определить на какой странице оказались. Эта статья разбирает настройки tsconfig с наибольшим влиянием на качество тестов, типизированные фикстуры, unknown вместо any для данных API-ответов, и утилитарные типы для разделения того что отправляешь серверу и того что получаешь обратно.
Строгие настройки tsconfig для тестового кода
Начни со строгой конфигурации TypeScript:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true
},
"include": ["tests/**/*", "fixtures/**/*", "pages/**/*"]
}strictNullChecks самая ценная настройка для тестового кода. Она заставляет явно обрабатывать null и undefined. Предотвращает ошибки Cannot read properties of null в рантайме.
noUnusedLocals и noUnusedParameters устраняют мёртвый код который молча накапливается в тестовых файлах.
Типизация тестовых данных
Нетипизированные тестовые данные: распространённая ошибка. Полагаешься на правильную форму объекта в момент написания, потом имя поля меняется и тесты молча проходят на устаревших данных.
// Плохо
const user = {
email: 'test@example.com',
password: 'pass123',
};
// Хорошо — тип гарантирует что форма остаётся корректной
interface TestUser {
email: string;
password: string;
role: 'admin' | 'user' | 'viewer';
}
const testUser: TestUser = {
email: 'test@example.com',
password: 'pass123',
role: 'user',
};Когда добавляешь новое обязательное поле в TestUser, TypeScript сразу помечает все объекты тестовых данных которые его не включают.
Типизация методов page-объектов
Методы page-объектов которые переходят на новую страницу должны возвращать тип нового page-объекта:
export class LoginPage {
// Тип возврата делает навигацию явной
async loginWith(email: string, password: string): Promise<DashboardPage> {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
await this.page.waitForURL('/dashboard');
return new DashboardPage(this.page);
}
// void для действий которые остаются на той же странице
async fillEmail(email: string): Promise<void> {
await this.emailInput.fill(email);
}
// Тип возврата для извлечения данных
async getErrorMessage(): Promise<string | null> {
if (await this.errorMessage.isVisible()) {
return this.errorMessage.textContent();
}
return null;
}
}Тип возврата Promise заставляет вызывающий код обрабатывать случай null. Больше не будет ситуаций когда const text = await page.getErrorMessage(); expect(text.length).toBeGreaterThan(0) падает на null.
Типизированные фикстуры
Типизированные фикстуры предотвращают их неправильное использование:
// fixtures/auth.ts
import { test as base } from '@playwright/test';
interface AuthFixtures {
userPage: Page; // Предаутентифицирован как обычный пользователь
adminPage: Page; // Предаутентифицирован как admin
authToken: string;
}
export const test = base.extend<AuthFixtures>({
authToken: async ({ request }, use) => {
const response = await request.post('/api/auth/login', {
data: { email: process.env.TEST_USER_EMAIL!, password: process.env.TEST_USER_PASSWORD! },
});
expect(response.ok()).toBeTruthy();
const { token } = await response.json() as { token: string };
await use(token);
},
userPage: async ({ page, authToken }, use) => {
await page.context().addCookies([{
name: 'auth_token',
value: authToken,
domain: 'localhost',
path: '/',
}]);
await use(page);
},
});TypeScript гарантирует что userPage и adminPage доступны только в тестах использующих это расширение фикстур, но не в тестах с базовым импортом test.
Константные утверждения для селекторов
Не дублируй строки селекторов между тестами:
// selectors.ts
export const Selectors = {
login: {
emailInput: 'label:has-text("Email") >> input',
passwordInput: 'label:has-text("Password") >> input',
submitButton: 'button[type="submit"]',
},
checkout: {
cartTotal: '[data-testid="cart-total"]',
placeOrderButton: 'button:has-text("Place order")',
},
} as const; // 'as const' делает строки литеральными типами — исключает случайную мутациюЛучше: использовать getByRole, getByLabel и другие методы в page-объектах вместо строковых селекторов вообще. Но когда строковые селекторы необходимы, as const предотвращает случайное переприсваивание.
Дискриминированные объединения для API-ответов
Когда тестируешь API которые возвращают разные структуры в зависимости от успеха или ошибки:
type ApiSuccess<T> = {
success: true;
data: T;
};
type ApiError = {
success: false;
error: string;
code: number;
};
type ApiResponse<T> = ApiSuccess<T> | ApiError;
// Сужение типов в тестах
const body = await response.json() as ApiResponse<{ orderId: string }>;
if (body.success) {
expect(body.data.orderId).toBeTruthy(); // TypeScript знает что data существует
} else {
expect(body.code).toBe(422); // TypeScript знает что error и code существуют
}Избегай any, используй unknown для нетипизированных данных
Когда получаешь данные от API и типа ещё нет, используй unknown вместо any:
// Плохо — any отключает всю проверку типов
const body: any = await response.json();
body.nonExistentField.deeply.nested; // Нет ошибки, молча неправильно
// Хорошо — unknown заставляет проверить перед использованием
const body: unknown = await response.json();
// Обязательно проверить перед доступом
if (typeof body === 'object' && body !== null && 'orderId' in body) {
console.log((body as { orderId: string }).orderId);
}
// Лучше: использовать type guard
function isOrderResponse(data: unknown): data is { orderId: string; status: string } {
return typeof data === 'object' && data !== null && 'orderId' in data;
}
if (isOrderResponse(body)) {
expect(body.orderId).toBeTruthy(); // Типизировано
}Утилитарные типы для тестовых данных
Встроенные утилитарные типы TypeScript сокращают дублирование в типах тестовых данных:
interface User {
id: string;
email: string;
password: string;
role: 'admin' | 'user';
createdAt: Date;
}
// Входные данные для создания — без id и createdAt (генерирует сервер)
type CreateUserInput = Omit<User, 'id' | 'createdAt'>;
// Входные данные для обновления — все поля опциональны
type UpdateUserInput = Partial<Pick<User, 'email' | 'role'>>;
// Объект для проверки — только верифицируемые поля
type UserAssertion = Pick<User, 'email' | 'role'>;Эти типы делают работу с тестовыми данными явной: то что отправляешь серверу и то что получаешь обратно имеют разную форму, и TypeScript применяет это различие принудительно.
→ See also: TypeScript интерфейсы и типы для Page Object Model | Типы, интерфейсы и обобщения в TypeScript для тестовых фикстур | Page Object Model в Playwright: от хаоса к поддерживаемым тестам