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

205 lines
5.8 KiB
TypeScript

import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
/**
* Search Page Object Model
* Handles search functionality interactions
*
* Generated by: qe-queen-coordinator
*/
export class SearchPage extends BasePage {
// Search form
readonly searchForm: Locator;
readonly searchInput: Locator;
readonly searchButton: Locator;
// Results
readonly searchResults: Locator;
readonly resultItems: Locator;
readonly resultCount: Locator;
readonly noResultsMessage: Locator;
// Predictive search (autocomplete)
readonly predictiveResults: Locator;
readonly predictiveItems: Locator;
// Filters (if available)
readonly filterSection: Locator;
readonly sortDropdown: Locator;
constructor(page: Page) {
super(page);
// Search form selectors
this.searchForm = page.locator('form[action="/search"], [data-search-form]');
this.searchInput = page.locator('input[name="q"], [data-search-input], input[type="search"]');
this.searchButton = page.locator('button[type="submit"], [data-search-submit]');
// Results
this.searchResults = page.locator('.search-results, [data-search-results]');
this.resultItems = page.locator('.search-result, .product-card, [data-result-item]');
this.resultCount = page.locator('.results-count, [data-results-count]');
this.noResultsMessage = page.locator('.no-results, :text("No results"), :text("no search")');
// Predictive search
this.predictiveResults = page.locator('.predictive-search, [data-predictive-search]');
this.predictiveItems = page.locator('.predictive-search__item, [data-predictive-item]');
// Filters
this.filterSection = page.locator('.search-filters, [data-search-filters]');
this.sortDropdown = page.locator('[data-sort], select[name="sort_by"]');
}
/**
* Navigate to search page
*/
async navigate() {
await this.goto('/search');
await this.waitForPageLoad();
}
/**
* Navigate to search page with query
*/
async navigateWithQuery(query: string) {
await this.goto(`/search?q=${encodeURIComponent(query)}`);
await this.waitForPageLoad();
}
/**
* Perform search
*/
async search(query: string) {
await this.searchInput.fill(query);
await this.searchButton.click();
await this.waitForPageLoad();
}
/**
* Perform search and wait for results
*/
async searchAndWaitForResults(query: string) {
await this.search(query);
// Wait for either results or no results message
await Promise.race([
this.resultItems.first().waitFor({ state: 'visible', timeout: 10000 }).catch(() => {}),
this.noResultsMessage.waitFor({ state: 'visible', timeout: 10000 }).catch(() => {})
]);
}
/**
* Get search results count
*/
async getResultsCount(): Promise<number> {
if (await this.noResultsMessage.isVisible().catch(() => false)) {
return 0;
}
return await this.resultItems.count();
}
/**
* Check if search has results
*/
async hasResults(): Promise<boolean> {
const count = await this.getResultsCount();
return count > 0;
}
/**
* Get all result titles
*/
async getResultTitles(): Promise<string[]> {
if (!(await this.hasResults())) return [];
const titles = await this.resultItems.locator('.product-title, h3, a').allTextContents();
return titles.map(t => t.trim());
}
/**
* Click on result by index
*/
async clickResult(index: number) {
await this.resultItems.nth(index).click();
await this.page.waitForURL('**/products/**');
}
/**
* Click on result by name
*/
async clickResultByName(productName: string) {
const result = this.resultItems.filter({ hasText: productName }).first();
await result.click();
await this.page.waitForURL('**/products/**');
}
/**
* Type in search and wait for predictive results
*/
async typeAndWaitForPredictive(query: string) {
await this.searchInput.fill(query);
// Wait for predictive search to appear
await this.predictiveResults.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {});
}
/**
* Get predictive search suggestions
*/
async getPredictiveSuggestions(): Promise<string[]> {
if (!(await this.predictiveResults.isVisible().catch(() => false))) {
return [];
}
const suggestions = await this.predictiveItems.allTextContents();
return suggestions.map(s => s.trim());
}
/**
* Click predictive suggestion
*/
async clickPredictiveSuggestion(index: number) {
await this.predictiveItems.nth(index).click();
await this.waitForPageLoad();
}
/**
* Clear search input
*/
async clearSearch() {
await this.searchInput.clear();
}
/**
* Verify search page loaded
*/
async verifySearchPageLoaded() {
await expect(this.page).toHaveURL(/.*\/search.*/);
await expect(this.searchInput).toBeVisible();
}
/**
* Check if no results message is shown
*/
async hasNoResultsMessage(): Promise<boolean> {
return await this.noResultsMessage.isVisible().catch(() => false);
}
/**
* Test XSS in search (security test)
*/
async testXSSPayload(payload: string): Promise<boolean> {
await this.search(payload);
// Check if script executed (it shouldn't)
const alertTriggered = await this.page.evaluate(() => {
// Check if any XSS succeeded
return (window as any).__xss_triggered || false;
});
return !alertTriggered; // Return true if XSS was blocked
}
/**
* Get current search query from URL
*/
async getCurrentQuery(): Promise<string> {
const url = new URL(this.page.url());
return url.searchParams.get('q') || '';
}
}