Les interfaces TypeScript définissent la forme des données de test et des paramètres de méthodes des page objects. Les erreurs dues à de mauvais arguments, qui apparaissaient à l'exécution, deviennent des erreurs à la compilation que l'éditeur détecte avant même de lancer les tests.
Pourquoi TypeScript dans le POM
Sans types :
class ProductPage {
async addToCart(product) { // product peut être n'importe quoi
await this.page.click(`[data-id="${product.id}"]`);
}
}
// Aucune aide de l'éditeur, erreur facile à commettre
await productPage.addToCart({ productId: 123 }); // oups, mauvais nom de champAvec une interface :
interface Product {
id: number;
name: string;
price: number;
}
class ProductPage {
async addToCart(product: Product) {
await this.page.click(`[data-id="${product.id}"]`);
}
}
// L'éditeur le détecte immédiatement
await productPage.addToCart({ productId: 123 }); // Erreur : productId n'existe pas sur Product
await productPage.addToCart({ id: 123, name: 'Laptop', price: 999 }); // ✅Syntaxe de base des interfaces
Une interface définit la forme d'un objet :
interface User {
id: number;
email: string;
role: 'admin' | 'member' | 'viewer';
isActive: boolean;
createdAt?: string; // Champ optionnel (le ?)
}Les champs requis doivent toujours être présents. Les champs optionnels (?) peuvent être présents ou non. Les types union ('admin' | 'member') n'acceptent que ces valeurs exactes.
Interfaces pour les données de test
L'usage le plus courant : typer les objets de données de test.
// data/users.ts
export interface UserCredentials {
email: string;
password: string;
}
export interface UserProfile extends UserCredentials {
name: string;
role: 'admin' | 'member';
}
export const TEST_USERS = {
admin: {
email: 'admin@test.com',
password: 'AdminPass1',
name: 'Test Admin',
role: 'admin' as const,
} satisfies UserProfile,
member: {
email: 'member@test.com',
password: 'MemberPass1',
name: 'Test Member',
role: 'member' as const,
} satisfies UserProfile,
};Le mot-clé satisfies (TypeScript 4.9+) vérifie que l'objet correspond à l'interface tout en préservant les types littéraux.
Interfaces pour les page objects
// pages/types.ts
import { Page, Locator } from '@playwright/test';
export interface PageObject {
page: Page;
navigate(): Promise<void>;
}
export interface LoginPageInterface extends PageObject {
emailInput: Locator;
passwordInput: Locator;
submitButton: Locator;
errorMessage: Locator;
login(email: string, password: string): Promise<void>;
}// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
import { LoginPageInterface } from './types';
export class LoginPage implements LoginPageInterface {
page: Page;
emailInput: Locator;
passwordInput: Locator;
submitButton: Locator;
errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByTestId('email-input');
this.passwordInput = page.getByTestId('password-input');
this.submitButton = page.getByTestId('submit-btn');
this.errorMessage = page.getByTestId('error-message');
}
async navigate(): Promise<void> {
await this.page.goto('/login');
}
async login(email: string, password: string): Promise<void> {
await this.navigate();
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}Le mot-clé implements indique à TypeScript que cette classe doit contenir tout ce que l'interface requiert. Si tu oublies une méthode, tu obtiens une erreur à la compilation.
type vs interface
Les deux définissent la forme d'objets. Les différences pratiques :
| | interface | type |
|---|---|---|
| Extension | mot-clé extends | intersection & |
| Fusion de déclarations | Oui (champs ajoutables en plusieurs endroits) | Non |
| Types union | Non | Oui |
| Pour les objets | ✅ Préféré | ✅ Fonctionne aussi |
Règle pratique : utiliseinterface pour les formes d'objets (page objects et modèles de données). Utilise type pour les unions, les primitives et les combinaisons complexes.
// interface : idéal pour les formes d'objets
interface ProductFilter {
category?: string;
minPrice?: number;
maxPrice?: number;
inStock?: boolean;
}
// type : nécessaire pour les unions
type TestEnvironment = 'local' | 'staging' | 'production';
type Callback = () => void | Promise<void>;
type UserOrAdmin = User | Admin;Interfaces génériques
Les generics permettent d'écrire des interfaces flexibles :
// Une réponse API paginée, compatible avec n'importe quel type de données
interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
// Utilisation avec des types spécifiques
type UsersResponse = PaginatedResponse<User>;
type ProductsResponse = PaginatedResponse<Product>;
type OrdersResponse = PaginatedResponse<Order>;// Dans un test
const response = await request.get('/api/users?page=1&limit=10');
const body: UsersResponse = await response.json();
expect(body.data).toHaveLength(10);
expect(body.totalPages).toBeGreaterThan(0);
// TypeScript sait que body.data[0] est un User
expect(body.data[0].email).toBeTruthy();Interfaces pour les réponses API
Toujours typer les réponses API pour bénéficier de l'autocomplétion et détecter les fautes sur les noms de champs :
// types/api.ts
export interface LoginResponse {
token: string;
expiresAt: string;
user: {
id: number;
email: string;
role: string;
};
}
export interface ErrorResponse {
error: string;
message: string;
field?: string; // Présent pour les erreurs de validation
}
export interface CreateUserRequest {
email: string;
password: string;
name?: string;
role?: 'admin' | 'member';
}// Dans les tests
test('la connexion retourne la bonne réponse', async ({ request }) => {
const response = await request.post('/api/auth/login', {
data: { email: 'user@test.com', password: 'ValidPass1' },
});
const body: LoginResponse = await response.json();
// TypeScript sait exactement ce que body contient
expect(body.token).toBeTruthy();
expect(body.user.id).toBeGreaterThan(0);
expect(body.user.role).toBe('member');
});Étendre les interfaces
extends permet de construire sur des interfaces existantes :
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
email: string;
name: string;
role: 'admin' | 'member';
}
interface Product extends BaseEntity {
name: string;
price: number;
category: string;
inStock: boolean;
}User et Product héritent tous les deux de id, createdAt et updatedAt via BaseEntity, en plus de leurs propres champs.
Interfaces pour les fixtures
Typer les fixtures Playwright personnalisées :
// fixtures/types.ts
import { Page } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
export interface AppFixtures {
loginPage: LoginPage;
dashboardPage: DashboardPage;
authenticatedPage: Page;
}
export interface TestUser {
id: number;
email: string;
token: string;
role: 'admin' | 'member';
}
export interface ApiFixtures {
testUser: TestUser;
adminToken: string;
}// fixtures/index.ts
import { test as base } from '@playwright/test';
import { AppFixtures, ApiFixtures } from './types';
export const test = base.extend<AppFixtures & ApiFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
// ...
});tsconfig.json adapté à Playwright
Active le mode strict pour une meilleure vérification de types :
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"outDir": "./dist",
"baseUrl": ".",
"paths": {
"@fixtures": ["./fixtures/index.ts"],
"@pages/*": ["./pages/*"],
"@data/*": ["./data/*"]
}
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}La config paths permet d'importer avec des alias :
import { test } from '@fixtures'; // au lieu de '../../fixtures/index'
import { LoginPage } from '@pages/LoginPage'; // au lieu de '../../pages/LoginPage'Résumé
interfacepour les page objects, les données de test et les formes de réponses APItypepour les types union et les combinaisons complexesextendspour construire sur des interfaces existantes (principe DRY)- Interfaces génériques (
PaginatedResponse) pour les formes réutilisables - Typer les réponses API pour que TypeScript aide à vérifier les champs
implementsdans les classes de page objects pour respecter le contrat
TypeScript dans l'automatisation des tests n'est pas une question de cérémonie. C'est l'éditeur qui détecte product.productId quand tu voulais dire product.id, avant que tu passes dix minutes à chercher pourquoi le clic ne fonctionne pas.