Skip to content

Commit 74c352d

Browse files
Merge pull request #1 from workcontrolgit/feature/ai-submenu-phase3-playwright
Phase 3: Add AI submenu Playwright tests
2 parents 2d7f081 + 27e726f commit 74c352d

10 files changed

Lines changed: 1135 additions & 104 deletions

page-objects/ai-assistant.page.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Page, Locator } from '@playwright/test';
2+
3+
/**
4+
* AI Assistant Page Object
5+
*
6+
* Encapsulates locators and interactions for the AI Assistant page at /ai/assistant.
7+
* Handles both enabled state (chat UI) and disabled state (info banner).
8+
*/
9+
export class AiAssistantPage {
10+
readonly page: Page;
11+
readonly url = '/ai/assistant';
12+
13+
// ── Disabled state ────────────────────────────────────────────────────────
14+
readonly disabledBanner: Locator;
15+
16+
// ── Enabled state — chat UI ───────────────────────────────────────────────
17+
readonly chatCard: Locator;
18+
readonly messageInput: Locator;
19+
readonly sendButton: Locator;
20+
readonly clearButton: Locator;
21+
readonly messages: Locator;
22+
readonly loadingRow: Locator;
23+
readonly errorRow: Locator;
24+
readonly emptyState: Locator;
25+
26+
constructor(page: Page) {
27+
this.page = page;
28+
29+
this.disabledBanner = page.locator('.ai-disabled-banner, .disabled-card').first();
30+
31+
this.chatCard = page.locator('mat-card').first();
32+
this.messageInput = page.locator('input[placeholder*="AI assistant"], input[placeholder*="anything"]').first();
33+
this.sendButton = page.locator('button').filter({ hasText: /^send$/i }).first();
34+
this.clearButton = page.locator('button[mat-icon-button]').filter({ hasText: /delete_sweep/i }).first();
35+
this.messages = page.locator('.message');
36+
this.loadingRow = page.locator('.loading-row');
37+
this.errorRow = page.locator('.error-row');
38+
this.emptyState = page.locator('.empty-state');
39+
}
40+
41+
async goto() {
42+
await this.page.goto(this.url);
43+
await this.page.waitForLoadState('networkidle');
44+
}
45+
46+
async isDisabled(): Promise<boolean> {
47+
return await this.disabledBanner.isVisible({ timeout: 3000 }).catch(() => false);
48+
}
49+
50+
async sendMessage(message: string) {
51+
await this.messageInput.fill(message);
52+
await this.page.keyboard.press('Enter');
53+
await this.page.waitForTimeout(500);
54+
}
55+
56+
async getMessageCount(): Promise<number> {
57+
return await this.messages.count();
58+
}
59+
}

page-objects/ai-hr-insight.page.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Page, Locator } from '@playwright/test';
2+
3+
/**
4+
* AI HR Insight Page Object
5+
*
6+
* Encapsulates locators and interactions for the HR Insight page at /ai/hr-insight.
7+
* Handles both enabled state (chat UI with suggestion chips) and disabled state (info banner).
8+
*/
9+
export class AiHrInsightPage {
10+
readonly page: Page;
11+
readonly url = '/ai/hr-insight';
12+
13+
// ── Disabled state ────────────────────────────────────────────────────────
14+
readonly disabledBanner: Locator;
15+
16+
// ── Enabled state — HR chat UI ────────────────────────────────────────────
17+
readonly chatCard: Locator;
18+
readonly questionInput: Locator;
19+
readonly askButton: Locator;
20+
readonly clearButton: Locator;
21+
readonly suggestionButtons: Locator;
22+
readonly messages: Locator;
23+
readonly loadingRow: Locator;
24+
readonly errorRow: Locator;
25+
26+
constructor(page: Page) {
27+
this.page = page;
28+
29+
this.disabledBanner = page.locator('.ai-disabled-banner, .disabled-card').first();
30+
31+
this.chatCard = page.locator('mat-card').first();
32+
this.questionInput = page.locator('input[placeholder*="workforce"], input[placeholder*="question"]').first();
33+
this.askButton = page.locator('button').filter({ hasText: /^ask$/i }).first();
34+
this.clearButton = page.locator('button[mat-icon-button]').filter({ hasText: /delete_sweep/i }).first();
35+
this.suggestionButtons = page.locator('.suggestion-list button, .suggestions button');
36+
this.messages = page.locator('.message');
37+
this.loadingRow = page.locator('.loading-row');
38+
this.errorRow = page.locator('.error-row');
39+
}
40+
41+
async goto() {
42+
await this.page.goto(this.url);
43+
await this.page.waitForLoadState('networkidle');
44+
}
45+
46+
async isDisabled(): Promise<boolean> {
47+
return await this.disabledBanner.isVisible({ timeout: 3000 }).catch(() => false);
48+
}
49+
50+
async getSuggestionCount(): Promise<number> {
51+
return await this.suggestionButtons.count();
52+
}
53+
54+
async clickSuggestion(index: number) {
55+
await this.suggestionButtons.nth(index).click();
56+
await this.page.waitForTimeout(500);
57+
}
58+
59+
async sendQuestion(question: string) {
60+
await this.questionInput.fill(question);
61+
await this.page.keyboard.press('Enter');
62+
await this.page.waitForTimeout(500);
63+
}
64+
}

page-objects/ai-nl-search.page.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Page, Locator } from '@playwright/test';
2+
3+
/**
4+
* AI Vector Search Page Object
5+
*
6+
* Encapsulates locators and interactions for the Vector Search page at /ai/vector-search.
7+
* Handles both enabled state (search UI with scored results) and disabled state (info banner).
8+
*/
9+
export class AiVectorSearchPage {
10+
readonly page: Page;
11+
readonly url = '/ai/vector-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 resultsTable: Locator;
20+
readonly resultRows: Locator;
21+
readonly scoreBadges: 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*="position"], input[placeholder*="describe"]').first();
33+
this.clearButton = page.locator('button').filter({ hasText: /clear/i }).first();
34+
this.resultsTable = page.locator('table.results-table, mat-table').first();
35+
this.resultRows = page.locator('table.results-table tr:not(thead tr), mat-row');
36+
this.scoreBadges = page.locator('.score-badge');
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 getScoreBadgeCount(): Promise<number> {
68+
return await this.scoreBadges.count();
69+
}
70+
}

0 commit comments

Comments
 (0)