自動テストチュートリアル

Playwright、Seleniumなどの自動テストツールを学ぼう


title: 演習: ECサイトのPOM化

演習: ECサイトのPOM化

複数のページとコンポーネントを持つECサイトをPOM化する総合演習です。

対象サイトの構成

  1. 商品一覧ページ - 商品リスト、検索、カテゴリフィルター
  2. 商品詳細ページ - 商品情報、カートに追加ボタン
  3. カートページ - カート内商品、数量変更、購入ボタン
  4. 共通コンポーネント - ヘッダー(カートアイコン、検索)

演習: 設計と実装

以下のPage ObjectとComponentを作成してください。

1. Headerコンポーネント

// pages/components/Header.ts
export class Header {
  // - 検索機能
  // - カートアイコン(商品数表示)
  // - カートページへの遷移
}

2. ProductListPage

// pages/ProductListPage.ts
export class ProductListPage {
  // - 商品リストの取得
  // - カテゴリでフィルター
  // - 商品をクリックして詳細へ
}

3. ProductDetailPage

// pages/ProductDetailPage.ts
export class ProductDetailPage {
  // - 商品名、価格の取得
  // - 数量選択
  // - カートに追加
}

4. CartPage

// pages/CartPage.ts
export class CartPage {
  // - カート内商品の確認
  // - 数量変更
  // - 商品削除
  // - 合計金額の取得
  // - 購入手続きへ
}

解答例

Headerコンポーネント
// pages/components/Header.ts
import { Page, Locator } from '@playwright/test';

export class Header {
  readonly page: Page;
  readonly searchInput: Locator;
  readonly searchButton: Locator;
  readonly cartIcon: Locator;
  readonly cartCount: Locator;

  constructor(page: Page) {
    this.page = page;
    this.searchInput = page.locator('header input[type="search"]');
    this.searchButton = page.locator('header button[aria-label="検索"]');
    this.cartIcon = page.locator('header .cart-icon');
    this.cartCount = page.locator('header .cart-count');
  }

  async search(keyword: string) {
    await this.searchInput.fill(keyword);
    await this.searchButton.click();
  }

  async goToCart() {
    await this.cartIcon.click();
  }

  async getCartItemCount(): Promise<number> {
    const text = await this.cartCount.textContent();
    return parseInt(text || '0', 10);
  }
}
ProductListPage
// pages/ProductListPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';
import { Header } from './components/Header';

export class ProductListPage extends BasePage {
  readonly header: Header;
  readonly productCards: Locator;
  readonly categoryFilter: Locator;

  constructor(page: Page) {
    super(page);
    this.header = new Header(page);
    this.productCards = page.locator('.product-card');
    this.categoryFilter = page.locator('.category-filter');
  }

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

  async getProductCount(): Promise<number> {
    return await this.productCards.count();
  }

  async filterByCategory(category: string) {
    await this.categoryFilter.selectOption(category);
  }

  async clickProduct(productName: string) {
    await this.productCards.filter({ hasText: productName }).click();
  }

  async getProductNames(): Promise<string[]> {
    return await this.productCards.locator('.product-name').allTextContents();
  }
}
ProductDetailPage
// pages/ProductDetailPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';
import { Header } from './components/Header';

export class ProductDetailPage extends BasePage {
  readonly header: Header;
  readonly productName: Locator;
  readonly productPrice: Locator;
  readonly quantityInput: Locator;
  readonly addToCartButton: Locator;
  readonly addedMessage: Locator;

  constructor(page: Page) {
    super(page);
    this.header = new Header(page);
    this.productName = page.locator('.product-name');
    this.productPrice = page.locator('.product-price');
    this.quantityInput = page.locator('input[name="quantity"]');
    this.addToCartButton = page.getByRole('button', { name: 'カートに追加' });
    this.addedMessage = page.locator('.added-to-cart-message');
  }

  async getName(): Promise<string> {
    return await this.productName.textContent() ?? '';
  }

  async getPrice(): Promise<number> {
    const text = await this.productPrice.textContent() ?? '0';
    return parseInt(text.replace(/[^0-9]/g, ''), 10);
  }

  async setQuantity(quantity: number) {
    await this.quantityInput.fill(quantity.toString());
  }

  async addToCart(quantity: number = 1) {
    await this.setQuantity(quantity);
    await this.addToCartButton.click();
  }
}
CartPage
// pages/CartPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';

export class CartPage extends BasePage {
  readonly cartItems: Locator;
  readonly totalPrice: Locator;
  readonly checkoutButton: Locator;
  readonly emptyMessage: Locator;

  constructor(page: Page) {
    super(page);
    this.cartItems = page.locator('.cart-item');
    this.totalPrice = page.locator('.total-price');
    this.checkoutButton = page.getByRole('button', { name: '購入手続きへ' });
    this.emptyMessage = page.locator('.empty-cart-message');
  }

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

  async getItemCount(): Promise<number> {
    return await this.cartItems.count();
  }

  async getTotalPrice(): Promise<number> {
    const text = await this.totalPrice.textContent() ?? '0';
    return parseInt(text.replace(/[^0-9]/g, ''), 10);
  }

  async removeItem(productName: string) {
    const item = this.cartItems.filter({ hasText: productName });
    await item.getByRole('button', { name: '削除' }).click();
  }

  async updateQuantity(productName: string, quantity: number) {
    const item = this.cartItems.filter({ hasText: productName });
    await item.locator('input[name="quantity"]').fill(quantity.toString());
  }

  async proceedToCheckout() {
    await this.checkoutButton.click();
  }
}

E2Eテスト例

test('商品を検索してカートに追加し購入手続きへ進む', async ({ page }) => {
  const productListPage = new ProductListPage(page);
  const productDetailPage = new ProductDetailPage(page);
  const cartPage = new CartPage(page);

  // 商品一覧を開く
  await productListPage.goto();

  // 検索
  await productListPage.header.search('Tシャツ');

  // 商品を選択
  await productListPage.clickProduct('コットンTシャツ');

  // カートに追加
  await productDetailPage.addToCart(2);

  // カートへ移動
  await productDetailPage.header.goToCart();

  // 確認
  expect(await cartPage.getItemCount()).toBe(1);
  await cartPage.proceedToCheckout();
});
💡 この演習のポイント
  • 複数のPage Objectを連携させる
  • 共通コンポーネントを各ページで再利用
  • ユーザーフローに沿ったテストを書く