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

233 lines
6.8 KiB
TypeScript

import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
/**
* Cart Page Object Model
* Handles interactions with the shopping cart
*
* Generated by: qe-queen-coordinator
*/
export class CartPage extends BasePage {
// Cart state
readonly emptyCartMessage: Locator;
readonly cartTable: Locator;
readonly cartItems: Locator;
// Cart item details
readonly itemTitles: Locator;
readonly itemPrices: Locator;
readonly itemQuantities: Locator;
readonly itemTotals: Locator;
readonly removeButtons: Locator;
// Cart totals
readonly subtotal: Locator;
readonly discountRow: Locator;
readonly shippingRow: Locator;
readonly total: Locator;
// Actions
readonly updateCartButton: Locator;
readonly checkoutButton: Locator;
readonly continueShoppingLink: Locator;
// Promo code
readonly promoCodeInput: Locator;
readonly applyPromoButton: Locator;
readonly promoError: Locator;
readonly promoSuccess: Locator;
// Notes
readonly cartNotes: Locator;
constructor(page: Page) {
super(page);
// Cart state selectors
this.emptyCartMessage = page.locator('.cart-empty, [data-cart-empty], :text("cart is empty")');
this.cartTable = page.locator('.cart-table, table, [data-cart-items]');
this.cartItems = page.locator('.cart-item, tr[data-line-item], .line-item');
// Item details
this.itemTitles = page.locator('.cart-item__title, .line-item__title, [data-item-title]');
this.itemPrices = page.locator('.cart-item__price, .line-item__price, [data-item-price]');
this.itemQuantities = page.locator('.cart-item__quantity input, [name="updates[]"], .quantity-input');
this.itemTotals = page.locator('.cart-item__total, .line-item__total');
this.removeButtons = page.locator('.cart-item__remove, [data-remove], a:has-text("Remove")');
// Cart totals
this.subtotal = page.locator('.cart-subtotal, [data-subtotal], :text("Subtotal")');
this.discountRow = page.locator('.cart-discount, [data-discount]');
this.shippingRow = page.locator('.cart-shipping, [data-shipping]');
this.total = page.locator('.cart-total, [data-total]');
// Actions
this.updateCartButton = page.locator('[name="update"], button:has-text("Update"), .update-cart');
this.checkoutButton = page.locator('[name="checkout"], button:has-text("Check"), .checkout-button, a[href*="checkout"]');
this.continueShoppingLink = page.locator('a:has-text("Continue"), [data-continue-shopping]');
// Promo code
this.promoCodeInput = page.locator('[name="discount"], #discount-code, .promo-input');
this.applyPromoButton = page.locator('button:has-text("Apply"), .apply-discount');
this.promoError = page.locator('.discount-error, [data-discount-error]');
this.promoSuccess = page.locator('.discount-success, [data-discount-success]');
// Cart notes
this.cartNotes = page.locator('[name="note"], .cart-notes, textarea');
}
/**
* Navigate to cart page
*/
async navigate() {
await this.goto('/cart');
await this.waitForPageLoad();
}
/**
* Check if cart is empty
*/
async isCartEmpty(): Promise<boolean> {
return await this.emptyCartMessage.isVisible().catch(() => false);
}
/**
* Get number of items in cart
*/
async getItemCount(): Promise<number> {
if (await this.isCartEmpty()) return 0;
return await this.cartItems.count();
}
/**
* Get all item names in cart
*/
async getItemNames(): Promise<string[]> {
if (await this.isCartEmpty()) return [];
const names = await this.itemTitles.allTextContents();
return names.map(n => n.trim());
}
/**
* Get subtotal amount
*/
async getSubtotal(): Promise<string> {
const subtotalText = await this.subtotal.textContent();
const match = subtotalText?.match(/[\d,.]+/);
return match ? match[0] : '0';
}
/**
* Update item quantity by index
*/
async updateQuantity(itemIndex: number, newQuantity: number) {
const quantityInput = this.itemQuantities.nth(itemIndex);
await quantityInput.fill(newQuantity.toString());
if (await this.updateCartButton.isVisible()) {
await this.updateCartButton.click();
await this.waitForPageLoad();
}
}
/**
* Remove item by index
*/
async removeItem(itemIndex: number) {
const removeButton = this.removeButtons.nth(itemIndex);
await removeButton.click();
await this.waitForPageLoad();
}
/**
* Remove item by name
*/
async removeItemByName(itemName: string) {
const item = this.cartItems.filter({ hasText: itemName }).first();
const removeButton = item.locator('.cart-item__remove, [data-remove], a:has-text("Remove")');
await removeButton.click();
await this.waitForPageLoad();
}
/**
* Apply promo code
*/
async applyPromoCode(code: string) {
if (await this.promoCodeInput.isVisible()) {
await this.promoCodeInput.fill(code);
await this.applyPromoButton.click();
await this.page.waitForTimeout(1000);
}
}
/**
* Check if promo code was accepted
*/
async isPromoCodeAccepted(): Promise<boolean> {
return await this.promoSuccess.isVisible().catch(() => false);
}
/**
* Check if promo code has error
*/
async hasPromoCodeError(): Promise<boolean> {
return await this.promoError.isVisible().catch(() => false);
}
/**
* Proceed to checkout
*/
async proceedToCheckout() {
await this.checkoutButton.click();
await this.page.waitForURL('**/checkout/**', { timeout: 15000 });
}
/**
* Continue shopping
*/
async continueShopping() {
await this.continueShoppingLink.click();
await this.page.waitForURL('**/collections/**');
}
/**
* Add cart notes
*/
async addCartNotes(notes: string) {
if (await this.cartNotes.isVisible()) {
await this.cartNotes.fill(notes);
}
}
/**
* Verify cart page loaded
*/
async verifyCartPageLoaded() {
await expect(this.page).toHaveURL(/.*\/cart.*/);
// Either empty message or cart items should be visible
const isEmpty = await this.isCartEmpty();
if (!isEmpty) {
await expect(this.cartTable.or(this.cartItems.first())).toBeVisible();
}
}
/**
* Clear cart (remove all items)
*/
async clearCart() {
while (!(await this.isCartEmpty())) {
await this.removeItem(0);
await this.page.waitForTimeout(500);
}
}
/**
* Get item quantity by index
*/
async getItemQuantity(itemIndex: number): Promise<number> {
const quantityInput = this.itemQuantities.nth(itemIndex);
const value = await quantityInput.inputValue();
return parseInt(value, 10) || 0;
}
}