TypeScript dans Playwright détecte une catégorie spécifique d'erreurs de test au moment de l'édition : await manquant, types d'arguments incorrects, propriétés indéfinies, et fautes de frappe dans les noms de méthodes. Ces erreurs ne se révèleraient sinon qu'à l'exécution.
Paramètres stricts du tsconfig pour le code de test
Commencez avec une configuration TypeScript stricte :
// 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 est le paramètre le plus précieux pour le code de test. Il oblige à gérer null et undefined explicitement. Ça évite les erreurs Cannot read properties of null à l'exécution.
noUnusedLocals et noUnusedParameters éliminent le code mort qui s'accumule silencieusement dans les fichiers de test.
Typer les données de test
Les données de test non typées sont une erreur courante : on se fie à la forme de l'objet au moment de l'écriture. Puis un nom de champ change et les tests passent silencieusement sur des données obsolètes.
// Mauvais
const user = {
email: 'test@example.com',
password: 'pass123',
};
// Bon — le type garantit que la forme reste correcte
interface TestUser {
email: string;
password: string;
role: 'admin' | 'user' | 'viewer';
}
const testUser: TestUser = {
email: 'test@example.com',
password: 'pass123',
role: 'user',
};Quand vous ajoutez un nouveau champ obligatoire à TestUser, TypeScript signale immédiatement chaque objet de données de test qui ne l'inclut pas.
Typer les méthodes des page objects
Les méthodes de page objects qui naviguent vers une nouvelle page doivent retourner le type du nouveau page object :
export class LoginPage {
// Le type de retour rend la navigation explicite
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 pour les actions qui restent sur la même page
async fillEmail(email: string): Promise<void> {
await this.emailInput.fill(email);
}
// Type de retour pour l'extraction de données
async getErrorMessage(): Promise<string | null> {
if (await this.errorMessage.isVisible()) {
return this.errorMessage.textContent();
}
return null;
}
}Le type de retour Promise oblige les appelants à gérer le cas null. Fini les const text = await page.getErrorMessage(); expect(text.length).toBeGreaterThan(0) qui plantent sur null.
Fixtures génériques
Les fixtures typées évitent les mauvais usages :
// fixtures/auth.ts
import { test as base } from '@playwright/test';
interface AuthFixtures {
userPage: Page; // Authentifié en tant qu'utilisateur standard
adminPage: Page; // Authentifié en tant qu'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 garantit que userPage et adminPage ne sont accessibles que dans les tests qui utilisent cette extension de fixture, pas dans les tests utilisant l'import test de base.
Const assertions pour les sélecteurs
Évitez de dupliquer les chaînes de sélecteurs à travers les tests :
// 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' rend les chaînes en types littéraux — pas de mutation accidentelleMieux encore : utilisez getByRole, getByLabel, etc. dans les page objects plutôt que des sélecteurs en chaîne. Mais quand les sélecteurs en chaîne sont nécessaires, as const empêche la réassignation accidentelle.
Unions discriminées pour les réponses API
Pour tester des APIs qui retournent des formes différentes selon le succès ou l'échec :
type ApiSuccess<T> = {
success: true;
data: T;
};
type ApiError = {
success: false;
error: string;
code: number;
};
type ApiResponse<T> = ApiSuccess<T> | ApiError;
// Narrowing de type dans les tests
const body = await response.json() as ApiResponse<{ orderId: string }>;
if (body.success) {
expect(body.data.orderId).toBeTruthy(); // TypeScript sait que data existe ici
} else {
expect(body.code).toBe(422); // TypeScript sait que error et code existent ici
}Éviter any — utiliser unknown pour les données non typées
Quand vous recevez des données d'une API sans type disponible, utilisez unknown plutôt que any :
// Mauvais — any désactive toute vérification de type
const body: any = await response.json();
body.nonExistentField.deeply.nested; // Pas d'erreur, silencieusement incorrect
// Bon — unknown oblige à valider avant d'utiliser
const body: unknown = await response.json();
// Doit valider avant d'accéder
if (typeof body === 'object' && body !== null && 'orderId' in body) {
console.log((body as { orderId: string }).orderId);
}
// Mieux : utiliser un 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(); // Typé
}Types utilitaires pour les données de test
Les types utilitaires intégrés de TypeScript réduisent la duplication dans les types de données de test :
interface User {
id: string;
email: string;
password: string;
role: 'admin' | 'user';
createdAt: Date;
}
// Input de création — sans id ni createdAt (générés par le serveur)
type CreateUserInput = Omit<User, 'id' | 'createdAt'>;
// Input de mise à jour — tous les champs optionnels
type UpdateUserInput = Partial<Pick<User, 'email' | 'role'>>;
// Objet d'assertion — uniquement les champs vérifiables
type UserAssertion = Pick<User, 'email' | 'role'>;Ces types rendent la gestion des données de test explicite : ce qu'on envoie au serveur et ce qu'on reçoit en retour ont des formes différentes, et TypeScript impose cette différence.
→ See also: Interfaces et Types TypeScript pour le Page Object Model | Types, Interfaces et Génériques en TypeScript pour les Fixtures de Tests | Page Object Model dans Playwright: Du Chaos à la Maintenabilité