Les classes JavaScript sont à la base du Page Object Model dans Playwright. Chaque page est une classe avec un constructeur qui reçoit la fixture page, des propriétés qui définissent les locators, et des méthodes qui encapsulent les interactions.

Qu'est-ce qu'une classe ?

Une classe est un modèle pour créer des objets. Chaque LoginPage créée depuis la classe dispose des mêmes méthodes, mais fonctionne indépendamment.

class LoginPage {
  constructor(page) {
    this.page = page;
    this.emailInput = page.getByTestId('email-input');
    this.passwordInput = page.getByTestId('password-input');
    this.submitButton = page.getByTestId('submit-btn');
  }

  async navigate() {
    await this.page.goto('/login');
  }

  async login(email, password) {
    await this.navigate();
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }
}

Utilisation :

const loginPage = new LoginPage(page);
await loginPage.login('user@test.com', 'ValidPass1');

Le constructor

Le constructeur s'exécute quand vous créez une nouvelle instance avec new. C'est là qu'on initialise les propriétés.

class ProductPage {
  constructor(page) {
    // 'this' désigne l'instance en cours de création
    this.page = page;
    this.productTitle = page.getByTestId('product-title');
    this.addToCartButton = page.getByTestId('add-to-cart');
    this.price = page.getByTestId('product-price');
    this.baseURL = '/products';
  }
}

// Quand vous appelez new ProductPage(page) :
// - le constructeur s'exécute immédiatement
// - 'this.page' prend la valeur de l'argument page
// - les locators sont stockés sur l'instance
const productPage = new ProductPage(page);

this dans les classes

this désigne l'instance courante. C'est ce qui permet aux méthodes d'accéder aux données de l'instance.

class DashboardPage {
  constructor(page) {
    this.page = page;
    this.welcomeMessage = page.getByTestId('welcome');
    this.userName = page.getByTestId('user-name');
    this.logoutButton = page.getByTestId('logout');
  }

  async getUserName() {
    // 'this.userName' est le locator stocké dans le constructeur
    return await this.userName.textContent();
  }

  async logout() {
    // 'this.logoutButton' est le locator du bouton
    await this.logoutButton.click();
  }

  async verifyUser(expectedName) {
    // Les méthodes peuvent en appeler d'autres via 'this'
    const name = await this.getUserName();
    if (name !== expectedName) {
      throw new Error(`Expected ${expectedName}, got ${name}`);
    }
  }
}

Les méthodes

Les méthodes sont des fonctions définies sur une classe. Elles peuvent être synchrones ou asynchrones.

class SearchPage {
  constructor(page) {
    this.page = page;
    this.searchInput = page.getByTestId('search-input');
    this.searchButton = page.getByTestId('search-button');
    this.results = page.getByTestId('search-result');
    this.noResultsMessage = page.getByTestId('no-results');
  }

  // Méthode async simple
  async search(query) {
    await this.searchInput.fill(query);
    await this.searchButton.click();
  }

  // Méthode qui retourne une valeur
  async getResultCount() {
    return await this.results.count();
  }

  // Méthode avec plusieurs étapes
  async searchAndVerify(query, expectedCount) {
    await this.search(query);
    await this.page.waitForLoadState('networkidle');
    const count = await this.getResultCount();
    return count === expectedCount;
  }
}

L'héritage avec extends

L'héritage permet à une classe de s'appuyer sur une autre. En POM, on crée souvent une BasePage que toutes les pages étendent.

// Classe de base avec les comportements communs
class BasePage {
  constructor(page) {
    this.page = page;
    this.header = page.getByTestId('header');
    this.footer = page.getByTestId('footer');
  }

  async navigate(path) {
    await this.page.goto(path);
  }

  async getTitle() {
    return await this.page.title();
  }

  async takeScreenshot(name) {
    await this.page.screenshot({ path: `screenshots/${name}.png` });
  }
}

// La classe enfant hérite de tout ce que contient BasePage
class LoginPage extends BasePage {
  constructor(page) {
    super(page);  // Obligatoire en premier — exécute le constructeur de BasePage
    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 login(email, password) {
    await this.navigate('/login');  // Hérité de BasePage
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }

  async getErrorText() {
    return await this.errorMessage.textContent();
  }
}

LoginPage dispose maintenant de ses propres méthodes ET de tout ce que contient BasePage :

const loginPage = new LoginPage(page);
await loginPage.login('user@test.com', 'pass');     // méthode de LoginPage
await loginPage.takeScreenshot('after-login');       // méthode de BasePage
const title = await loginPage.getTitle();            // méthode de BasePage

super : appeler le parent

super() appelle le constructeur de la classe parente. C'est obligatoire comme première ligne d'un constructeur enfant.

class AdminPage extends BasePage {
  constructor(page) {
    super(page);  // Exécute le constructeur de BasePage : définit this.page, this.header, this.footer
    // On ajoute ensuite ce qui est spécifique à AdminPage
    this.userTable = page.getByTestId('users-table');
    this.addUserButton = page.getByTestId('add-user');
  }
}

On peut aussi appeler les méthodes du parent avec super.methodName() :

class CheckoutPage extends BasePage {
  async navigate() {
    // Appelle le navigate parent, puis fait quelque chose de plus
    await super.navigate('/checkout');
    await this.page.waitForSelector('[data-testid="checkout-form"]');
  }
}

Les méthodes statiques

Les méthodes statiques appartiennent à la classe elle-même, pas aux instances. Utiles pour les fonctions utilitaires.

class TestData {
  static generateEmail() {
    return `user_${Date.now()}@test.com`;
  }

  static generateUser() {
    return {
      email: TestData.generateEmail(),
      password: 'ValidPass1',
      name: 'Test User',
    };
  }

  static generateProduct(overrides = {}) {
    return {
      name: 'Test Product',
      price: 99.99,
      category: 'electronics',
      ...overrides,
    };
  }
}

// Utilisation sans 'new' — appel direct sur la classe
const email = TestData.generateEmail();
const user = TestData.generateUser();
const product = TestData.generateProduct({ price: 49.99 });

Les getters

Les getters ressemblent à des propriétés, mais exécutent une fonction à l'accès :

class CartPage extends BasePage {
  constructor(page) {
    super(page);
    this.items = page.getByTestId('cart-item');
    this.totalElement = page.getByTestId('cart-total');
  }

  get url() {
    return '/cart';  // Propriété calculée
  }

  async navigate() {
    await super.navigate(this.url);  // Utilise le getter
  }

  async getItemCount() {
    return await this.items.count();
  }
}

const cartPage = new CartPage(page);
console.log(cartPage.url);  // '/cart' — pas de () nécessaire

Exemple complet de page object

Voici un page object réaliste qui combine tout ce qui précède :

class ProductListPage extends BasePage {
  constructor(page) {
    super(page);
    this.productCards = page.getByTestId('product-card');
    this.filterDropdown = page.getByTestId('category-filter');
    this.sortDropdown = page.getByTestId('sort-select');
    this.searchInput = page.getByTestId('product-search');
    this.loadingSpinner = page.getByTestId('loading');
  }

  async navigate() {
    await super.navigate('/products');
    await this.loadingSpinner.waitFor({ state: 'hidden' });
  }

  async filterByCategory(category) {
    await this.filterDropdown.selectOption(category);
    await this.loadingSpinner.waitFor({ state: 'hidden' });
  }

  async sortBy(sortOption) {
    await this.sortDropdown.selectOption(sortOption);
  }

  async search(query) {
    await this.searchInput.fill(query);
    await this.searchInput.press('Enter');
    await this.loadingSpinner.waitFor({ state: 'hidden' });
  }

  async getProductCount() {
    return await this.productCards.count();
  }

  async getProductNames() {
    return await this.productCards.getByTestId('product-name').allTextContents();
  }

  async clickProduct(index) {
    await this.productCards.nth(index).click();
  }
}

// Dans les tests
test('filtrer les produits par catégorie', async ({ page }) => {
  const products = new ProductListPage(page);
  await products.navigate();
  await products.filterByCategory('electronics');

  const count = await products.getProductCount();
  expect(count).toBeGreaterThan(0);
});

Classe ou objet littéral ?

Pour quelque chose d'unique, un objet littéral est plus simple. Pour quelque chose réutilisé dans plusieurs tests, une classe est plus adaptée.

// Objet littéral — convient pour des constantes
const TEST_USERS = {
  admin: { email: 'admin@test.com', password: 'AdminPass1' },
  member: { email: 'member@test.com', password: 'MemberPass1' },
};

// Classe — adaptée aux page objects utilisés dans de nombreux tests
class LoginPage extends BasePage {
  // ...
}

Récapitulatif

  • constructor(page) : s'exécute avec new, stocker les locators ici
  • this : désigne l'instance, c'est ce qui permet aux méthodes d'accéder aux données de l'objet
  • extends : hériter d'une autre classe (par exemple BasePage)
  • super() : appelle le constructeur parent, obligatoire en première ligne d'un constructeur enfant
  • static : méthodes au niveau de la classe, aucune instance nécessaire
  • Les méthodes peuvent être async : elles retournent des promises, utilisez await à l'intérieur

Les classes dans les page objects Playwright ne sont pas de la magie. Ce n'est que la façon qu'a JavaScript de regrouper des locators et des méthodes liés dans une unité réutilisable. Une fois que vous comprenez constructor, this et extends, le reste suit naturellement.

→ See also: Page Object Model dans Playwright: Du Chaos à la Maintenabilité | Interfaces et Types TypeScript pour le Page Object Model | JavaScript pour les QA Engineers: Le Minimum pour Commencer à Automatiser