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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ node_modules/
/blob-report/
/playwright/.cache/
/playwright/.auth/

# Blog screenshots — generated PNG files are not committed (too large, regenerate by running the screenshots project)
/screenshots-output/

# screenshot-catalog.json IS committed so AI tools can reference it across sessions without re-running tests
24 changes: 12 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "playwright",
"name": "angularnettutorial-playwright",
"version": "1.0.0",
"main": "index.js",
"scripts": {},
Expand All @@ -8,7 +8,7 @@
"license": "ISC",
"description": "",
"devDependencies": {
"@playwright/test": "^1.58.2",
"@playwright/test": "1.59.1",
"@types/node": "^25.2.2"
}
}
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();
}
}
16 changes: 16 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,22 @@ export default defineConfig({
},
},

// Blog Screenshots - captures key UI states for blog posts and documentation
{
name: 'screenshots',
testMatch: /tests\/screenshots\/.*\.spec\.ts/,
use: {
...devices['Desktop Chrome'],
viewport: VIEWPORTS.laptop,
video: 'off',
screenshot: 'off', // manual screenshots only — no auto-capture
launchOptions: {
slowMo: 150, // slow down interactions so UI fully settles before capture
},
},
// No setup dependency — auth is handled inline per screenshot group
},

/* Mobile viewports (optional - uncomment to enable) */
// {
// name: 'Mobile Chrome',
Expand Down
Loading
Loading