Files
Profa 2253d05618 feat: flatten project structure — archive v2, promote v3 to root
Remove the nested v3/ subdirectory by moving all active source code,
tests, configs, and assets directly to the project root. Archive v2/
(dead code, 847MB) to v2-archive.tar.gz.

Key changes:
- Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/
- Move v3/tsconfig.json, v3/vitest.config.ts to root
- Merge v3/package.json into root package.json (ESM, scripts, deps, exports)
- Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/
- Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes
- Fix all init installer path resolution (3-level-up → 2-level-up)
- Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe"
- Update settings.json ADR/DDD directories to docs/implementation/
- Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks)
- Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces
- Update 15 guidance shards: v3/src/ → src/ path references
- Fix sync interfaces: remove v3/ and v2/ path references
- Fix test path calculations (process.cwd() + '..' no longer needed)
- Fix flaky perf test threshold (500ms → 1000ms for CI environments)
- Fix complexity-analyzer test to match ADR-051 keyword-based eligibility

Build passes, 17,901 tests pass, CLI and MCP bundles verified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:35:41 +00:00

170 lines
5.0 KiB
TypeScript

import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
/**
* Catalog/Collection Page Object Model
* Handles interactions with the product catalog
*
* Generated by: qe-queen-coordinator
*/
export class CatalogPage extends BasePage {
// Page elements
readonly pageTitle: Locator;
readonly productGrid: Locator;
readonly productCards: Locator;
readonly productCount: Locator;
// Product card elements
readonly productImages: Locator;
readonly productTitles: Locator;
readonly productPrices: Locator;
readonly soldOutBadges: Locator;
// Sorting/Filtering (if available)
readonly sortDropdown: Locator;
readonly filterOptions: Locator;
// Pagination
readonly pagination: Locator;
readonly nextPageButton: Locator;
readonly prevPageButton: Locator;
constructor(page: Page) {
super(page);
// Page elements
this.pageTitle = page.locator('h1, .collection-title, .page-title');
this.productGrid = page.locator('.collection-products, .product-grid, [data-collection]');
this.productCards = page.locator('.product-card, .grid-item, .product-item');
this.productCount = page.locator('.collection-count, .product-count');
// Product card details
this.productImages = page.locator('.product-card img, .product-image');
this.productTitles = page.locator('.product-card h3, .product-title, .product-card__title');
this.productPrices = page.locator('.product-price, .price, [data-price]');
this.soldOutBadges = page.locator('.sold-out, .badge--sold-out, [data-sold-out]');
// Sorting/Filtering
this.sortDropdown = page.locator('[data-sort], .sort-by, select[name="sort_by"]');
this.filterOptions = page.locator('.filter-options, [data-filter]');
// Pagination
this.pagination = page.locator('.pagination, [data-pagination]');
this.nextPageButton = page.locator('.pagination-next, [rel="next"]');
this.prevPageButton = page.locator('.pagination-prev, [rel="prev"]');
}
/**
* Navigate to catalog page
*/
async navigate() {
await this.goto('/collections/all');
await this.waitForPageLoad();
}
/**
* Get total product count on page
*/
async getProductCount(): Promise<number> {
return await this.productCards.count();
}
/**
* Get all product names
*/
async getAllProductNames(): Promise<string[]> {
const titles = await this.productTitles.allTextContents();
return titles.map(t => t.trim());
}
/**
* Get all product prices
*/
async getAllProductPrices(): Promise<string[]> {
const prices = await this.productPrices.allTextContents();
return prices.map(p => p.trim());
}
/**
* Click on product by name
*/
async clickProduct(productName: string) {
const product = this.productCards.filter({ hasText: productName }).first();
await product.click();
await this.page.waitForURL('**/products/**');
}
/**
* Click on product by index
*/
async clickProductByIndex(index: number) {
await this.productCards.nth(index).click();
await this.page.waitForURL('**/products/**');
}
/**
* Check if product is sold out
*/
async isProductSoldOut(productName: string): Promise<boolean> {
const product = this.productCards.filter({ hasText: productName }).first();
const soldOutBadge = product.locator('.sold-out, .badge--sold-out, [data-sold-out]');
return await soldOutBadge.isVisible().catch(() => false);
}
/**
* Get list of sold out products
*/
async getSoldOutProducts(): Promise<string[]> {
const soldOutProducts: string[] = [];
const count = await this.productCards.count();
for (let i = 0; i < count; i++) {
const card = this.productCards.nth(i);
const soldOutBadge = card.locator('.sold-out, .badge--sold-out, [data-sold-out]');
if (await soldOutBadge.isVisible().catch(() => false)) {
const title = await card.locator('.product-title, h3').textContent();
if (title) soldOutProducts.push(title.trim());
}
}
return soldOutProducts;
}
/**
* Verify catalog page loaded
*/
async verifyCatalogLoaded() {
await expect(this.productGrid.or(this.productCards.first())).toBeVisible();
const count = await this.getProductCount();
expect(count).toBeGreaterThan(0);
}
/**
* Sort products (if sorting available)
*/
async sortBy(option: 'price-asc' | 'price-desc' | 'title-asc' | 'title-desc' | 'best-selling') {
if (await this.sortDropdown.isVisible()) {
await this.sortDropdown.selectOption(option);
await this.waitForPageLoad();
}
}
/**
* Check if pagination exists
*/
async hasPagination(): Promise<boolean> {
return await this.pagination.isVisible().catch(() => false);
}
/**
* Go to next page
*/
async goToNextPage() {
if (await this.nextPageButton.isVisible()) {
await this.nextPageButton.click();
await this.waitForPageLoad();
}
}
}