title: 演習: ECサイトのPOM化
演習: ECサイトのPOM化
複数のページとコンポーネントを持つECサイトをPOM化する総合演習です。
対象サイトの構成
- 商品一覧ページ - 商品リスト、検索、カテゴリフィルター
- 商品詳細ページ - 商品情報、カートに追加ボタン
- カートページ - カート内商品、数量変更、購入ボタン
- 共通コンポーネント - ヘッダー(カートアイコン、検索)
演習: 設計と実装
以下の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を連携させる
- 共通コンポーネントを各ページで再利用
- ユーザーフローに沿ったテストを書く