|
| 1 | +import { Page, Locator } from '@playwright/test'; |
| 2 | + |
| 3 | +/** |
| 4 | + * AI NL Search Page Object |
| 5 | + * |
| 6 | + * Encapsulates locators and interactions for the NL Search page at /ai/nl-search. |
| 7 | + * Handles both enabled state (search UI with results table) and disabled state (info banner). |
| 8 | + */ |
| 9 | +export class AiNlSearchPage { |
| 10 | + readonly page: Page; |
| 11 | + readonly url = '/ai/nl-search'; |
| 12 | + |
| 13 | + // ── Disabled state ──────────────────────────────────────────────────────── |
| 14 | + readonly disabledBanner: Locator; |
| 15 | + |
| 16 | + // ── Enabled state — search UI ───────────────────────────────────────────── |
| 17 | + readonly searchInput: Locator; |
| 18 | + readonly clearButton: Locator; |
| 19 | + readonly parsedExpression: Locator; |
| 20 | + readonly resultsTable: Locator; |
| 21 | + readonly resultRows: Locator; |
| 22 | + readonly resultCount: Locator; |
| 23 | + readonly loadingRow: Locator; |
| 24 | + readonly errorRow: Locator; |
| 25 | + readonly emptyState: Locator; |
| 26 | + |
| 27 | + constructor(page: Page) { |
| 28 | + this.page = page; |
| 29 | + |
| 30 | + this.disabledBanner = page.locator('.ai-disabled-banner, .disabled-card').first(); |
| 31 | + |
| 32 | + this.searchInput = page.locator('input[placeholder*="employee"], input[placeholder*="natural"]').first(); |
| 33 | + this.clearButton = page.locator('button').filter({ hasText: /clear/i }).first(); |
| 34 | + this.parsedExpression = page.locator('.parsed-expression'); |
| 35 | + this.resultsTable = page.locator('table.results-table, mat-table').first(); |
| 36 | + this.resultRows = page.locator('table.results-table tr:not(thead tr), mat-row'); |
| 37 | + this.resultCount = page.locator('.result-count'); |
| 38 | + this.loadingRow = page.locator('.loading-row'); |
| 39 | + this.errorRow = page.locator('.error-row'); |
| 40 | + this.emptyState = page.locator('.empty-state'); |
| 41 | + } |
| 42 | + |
| 43 | + async goto() { |
| 44 | + await this.page.goto(this.url); |
| 45 | + await this.page.waitForLoadState('networkidle'); |
| 46 | + } |
| 47 | + |
| 48 | + async isDisabled(): Promise<boolean> { |
| 49 | + return await this.disabledBanner.isVisible({ timeout: 3000 }).catch(() => false); |
| 50 | + } |
| 51 | + |
| 52 | + async search(query: string) { |
| 53 | + await this.searchInput.fill(query); |
| 54 | + // Wait for debounce (600ms) + network |
| 55 | + await this.page.waitForTimeout(1500); |
| 56 | + } |
| 57 | + |
| 58 | + async clear() { |
| 59 | + await this.clearButton.click(); |
| 60 | + await this.page.waitForTimeout(500); |
| 61 | + } |
| 62 | + |
| 63 | + async getResultCount(): Promise<number> { |
| 64 | + return await this.resultRows.count(); |
| 65 | + } |
| 66 | + |
| 67 | + async hasParsedExpression(): Promise<boolean> { |
| 68 | + return await this.parsedExpression.isVisible({ timeout: 2000 }).catch(() => false); |
| 69 | + } |
| 70 | +} |
0 commit comments