Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions page-objects/ai-assistant.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Page, Locator } from '@playwright/test';

/**
* AI Assistant Page Object
*
* Encapsulates locators and interactions for the AI Assistant page at /ai/assistant.
* Handles both enabled state (chat UI) and disabled state (info banner).
*/
export class AiAssistantPage {
readonly page: Page;
readonly url = '/ai/assistant';

// ── Disabled state ────────────────────────────────────────────────────────
readonly disabledBanner: Locator;

// ── Enabled state — chat UI ───────────────────────────────────────────────
readonly chatCard: Locator;
readonly messageInput: Locator;
readonly sendButton: Locator;
readonly clearButton: Locator;
readonly messages: Locator;
readonly loadingRow: Locator;
readonly errorRow: Locator;
readonly emptyState: Locator;

constructor(page: Page) {
this.page = page;

this.disabledBanner = page.locator('.ai-disabled-banner, .disabled-card').first();

this.chatCard = page.locator('mat-card').first();
this.messageInput = page.locator('input[placeholder*="AI assistant"], input[placeholder*="anything"]').first();
this.sendButton = page.locator('button').filter({ hasText: /^send$/i }).first();
this.clearButton = page.locator('button[mat-icon-button]').filter({ hasText: /delete_sweep/i }).first();
this.messages = page.locator('.message');
this.loadingRow = page.locator('.loading-row');
this.errorRow = page.locator('.error-row');
this.emptyState = page.locator('.empty-state');
}

async goto() {
await this.page.goto(this.url);
await this.page.waitForLoadState('networkidle');
}

async isDisabled(): Promise<boolean> {
return await this.disabledBanner.isVisible({ timeout: 3000 }).catch(() => false);
}

async sendMessage(message: string) {
await this.messageInput.fill(message);
await this.page.keyboard.press('Enter');
await this.page.waitForTimeout(500);
}

async getMessageCount(): Promise<number> {
return await this.messages.count();
}
}
64 changes: 64 additions & 0 deletions page-objects/ai-hr-insight.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Page, Locator } from '@playwright/test';

/**
* AI HR Insight Page Object
*
* Encapsulates locators and interactions for the HR Insight page at /ai/hr-insight.
* Handles both enabled state (chat UI with suggestion chips) and disabled state (info banner).
*/
export class AiHrInsightPage {
readonly page: Page;
readonly url = '/ai/hr-insight';

// ── Disabled state ────────────────────────────────────────────────────────
readonly disabledBanner: Locator;

// ── Enabled state — HR chat UI ────────────────────────────────────────────
readonly chatCard: Locator;
readonly questionInput: Locator;
readonly askButton: Locator;
readonly clearButton: Locator;
readonly suggestionButtons: Locator;
readonly messages: Locator;
readonly loadingRow: Locator;
readonly errorRow: Locator;

constructor(page: Page) {
this.page = page;

this.disabledBanner = page.locator('.ai-disabled-banner, .disabled-card').first();

this.chatCard = page.locator('mat-card').first();
this.questionInput = page.locator('input[placeholder*="workforce"], input[placeholder*="question"]').first();
this.askButton = page.locator('button').filter({ hasText: /^ask$/i }).first();
this.clearButton = page.locator('button[mat-icon-button]').filter({ hasText: /delete_sweep/i }).first();
this.suggestionButtons = page.locator('.suggestion-list button, .suggestions button');
this.messages = page.locator('.message');
this.loadingRow = page.locator('.loading-row');
this.errorRow = page.locator('.error-row');
}

async goto() {
await this.page.goto(this.url);
await this.page.waitForLoadState('networkidle');
}

async isDisabled(): Promise<boolean> {
return await this.disabledBanner.isVisible({ timeout: 3000 }).catch(() => false);
}

async getSuggestionCount(): Promise<number> {
return await this.suggestionButtons.count();
}

async clickSuggestion(index: number) {
await this.suggestionButtons.nth(index).click();
await this.page.waitForTimeout(500);
}

async sendQuestion(question: string) {
await this.questionInput.fill(question);
await this.page.keyboard.press('Enter');
await this.page.waitForTimeout(500);
}
}
70 changes: 70 additions & 0 deletions page-objects/ai-nl-search.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Page, Locator } from '@playwright/test';

/**
* AI NL Search Page Object
*
* Encapsulates locators and interactions for the NL Search page at /ai/nl-search.
* Handles both enabled state (search UI with results table) and disabled state (info banner).
*/
export class AiNlSearchPage {
readonly page: Page;
readonly url = '/ai/nl-search';

// ── Disabled state ────────────────────────────────────────────────────────
readonly disabledBanner: Locator;

// ── Enabled state — search UI ─────────────────────────────────────────────
readonly searchInput: Locator;
readonly clearButton: Locator;
readonly parsedExpression: Locator;
readonly resultsTable: Locator;
readonly resultRows: Locator;
readonly resultCount: Locator;
readonly loadingRow: Locator;
readonly errorRow: Locator;
readonly emptyState: Locator;

constructor(page: Page) {
this.page = page;

this.disabledBanner = page.locator('.ai-disabled-banner, .disabled-card').first();

this.searchInput = page.locator('input[placeholder*="employee"], input[placeholder*="natural"]').first();
this.clearButton = page.locator('button').filter({ hasText: /clear/i }).first();
this.parsedExpression = page.locator('.parsed-expression');
this.resultsTable = page.locator('table.results-table, mat-table').first();
this.resultRows = page.locator('table.results-table tr:not(thead tr), mat-row');
this.resultCount = page.locator('.result-count');
this.loadingRow = page.locator('.loading-row');
this.errorRow = page.locator('.error-row');
this.emptyState = page.locator('.empty-state');
}

async goto() {
await this.page.goto(this.url);
await this.page.waitForLoadState('networkidle');
}

async isDisabled(): Promise<boolean> {
return await this.disabledBanner.isVisible({ timeout: 3000 }).catch(() => false);
}

async search(query: string) {
await this.searchInput.fill(query);
// Wait for debounce (600ms) + network
await this.page.waitForTimeout(1500);
}

async clear() {
await this.clearButton.click();
await this.page.waitForTimeout(500);
}

async getResultCount(): Promise<number> {
return await this.resultRows.count();
}

async hasParsedExpression(): Promise<boolean> {
return await this.parsedExpression.isVisible({ timeout: 2000 }).catch(() => false);
}
}
70 changes: 70 additions & 0 deletions page-objects/ai-vector-search.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Page, Locator } from '@playwright/test';

/**
* AI Vector Search Page Object
*
* Encapsulates locators and interactions for the Vector Search page at /ai/vector-search.
* Handles both enabled state (search UI with scored results) and disabled state (info banner).
*/
export class AiVectorSearchPage {
readonly page: Page;
readonly url = '/ai/vector-search';

// ── Disabled state ────────────────────────────────────────────────────────
readonly disabledBanner: Locator;

// ── Enabled state — search UI ─────────────────────────────────────────────
readonly searchInput: Locator;
readonly clearButton: Locator;
readonly resultsTable: Locator;
readonly resultRows: Locator;
readonly scoreBadges: Locator;
readonly resultCount: Locator;
readonly loadingRow: Locator;
readonly errorRow: Locator;
readonly emptyState: Locator;

constructor(page: Page) {
this.page = page;

this.disabledBanner = page.locator('.ai-disabled-banner, .disabled-card').first();

this.searchInput = page.locator('input[placeholder*="position"], input[placeholder*="describe"]').first();
this.clearButton = page.locator('button').filter({ hasText: /clear/i }).first();
this.resultsTable = page.locator('table.results-table, mat-table').first();
this.resultRows = page.locator('table.results-table tr:not(thead tr), mat-row');
this.scoreBadges = page.locator('.score-badge');
this.resultCount = page.locator('.result-count');
this.loadingRow = page.locator('.loading-row');
this.errorRow = page.locator('.error-row');
this.emptyState = page.locator('.empty-state');
}

async goto() {
await this.page.goto(this.url);
await this.page.waitForLoadState('networkidle');
}

async isDisabled(): Promise<boolean> {
return await this.disabledBanner.isVisible({ timeout: 3000 }).catch(() => false);
}

async search(query: string) {
await this.searchInput.fill(query);
// Wait for debounce (600ms) + network
await this.page.waitForTimeout(1500);
}

async clear() {
await this.clearButton.click();
await this.page.waitForTimeout(500);
}

async getResultCount(): Promise<number> {
return await this.resultRows.count();
}

async getScoreBadgeCount(): Promise<number> {
return await this.scoreBadges.count();
}
}
Loading
Loading