mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
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>
205 lines
5.8 KiB
TypeScript
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') || '';
|
|
}
|
|
}
|