@sridharkikkeri/playwright-common 1.0.19 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/create-healthedge-tests.js +64 -292
- package/package.json +2 -1
- package/src/core/api/ApiClient.ts +289 -0
- package/src/core/api/auth/AuthStrategy.ts +11 -0
- package/src/core/api/auth/CookieAuth.ts +36 -0
- package/src/core/api/auth/OAuth2Auth.ts +46 -0
- package/src/core/config/ConfigManager.ts +72 -0
- package/src/core/i18n/Localization.ts +34 -0
- package/src/core/i18n/en.json +5 -0
- package/src/core/pages/BasePage.ts +47 -0
- package/src/core/reporting/AllureUtil.ts +35 -0
- package/src/core/selfhealing/ActionOrchestrator.ts +117 -0
- package/src/core/selfhealing/ElementProfileStore.ts +76 -0
- package/src/core/selfhealing/LocatorHealing.ts +84 -0
- package/src/core/utils/ErrorUtils.ts +21 -0
- package/src/core/utils/Logger.ts +14 -0
- package/src/core/visual/VisualTesting.ts +95 -0
- package/src/core/wrappers/ElementWrapper.ts +211 -0
- package/src/fixtures/fixtures.ts +90 -0
- package/src/index.ts +17 -0
- package/src/quality/pages/LoginPage.ts +36 -0
- package/src/tests/visual.spec.ts +38 -0
- package/src/tests/visual.spec.ts-snapshots/header-element-darwin.png +0 -0
- package/src/tests/visual.spec.ts-snapshots/playwright-homepage-darwin.png +0 -0
- package/src/tests/visual.spec.ts-snapshots/viewport-masked-darwin.png +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Locator } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
export interface ElementProfile {
|
|
4
|
+
selector: string;
|
|
5
|
+
intent: string;
|
|
6
|
+
attributes: Record<string, string>;
|
|
7
|
+
lastHealedSelector?: string;
|
|
8
|
+
healingFailed?: boolean;
|
|
9
|
+
failureType?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Stores and manages element profiles for self-healing.
|
|
14
|
+
* Captured durante successful runs to be used as a baseline for healing.
|
|
15
|
+
*/
|
|
16
|
+
export class ElementProfileStore {
|
|
17
|
+
private profiles: Map<string, ElementProfile> = new Map();
|
|
18
|
+
|
|
19
|
+
private getKey(pageName: string, intent: string): string {
|
|
20
|
+
return `${pageName}:${intent}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Captures the profile of a successful locator.
|
|
25
|
+
*/
|
|
26
|
+
async captureProfile(locator: Locator, pageName: string, intent: string): Promise<void> {
|
|
27
|
+
const key = this.getKey(pageName, intent);
|
|
28
|
+
try {
|
|
29
|
+
const attributes = await locator.evaluate((el) => {
|
|
30
|
+
const attrs: Record<string, string> = {};
|
|
31
|
+
Array.from(el.attributes).forEach(attr => {
|
|
32
|
+
attrs[attr.name] = attr.value;
|
|
33
|
+
});
|
|
34
|
+
attrs['text'] = el.textContent?.trim() || '';
|
|
35
|
+
attrs['tag'] = el.tagName.toLowerCase();
|
|
36
|
+
return attrs;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
this.profiles.set(key, {
|
|
40
|
+
selector: locator.toString(),
|
|
41
|
+
intent,
|
|
42
|
+
attributes
|
|
43
|
+
});
|
|
44
|
+
} catch {
|
|
45
|
+
// Ignore if element is gone or evaluation fails during capture
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getProfile(pageName: string, intent: string): ElementProfile | undefined {
|
|
50
|
+
return this.profiles.get(this.getKey(pageName, intent));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getHealedSelector(pageName: string, intent: string): string | undefined {
|
|
54
|
+
return this.profiles.get(this.getKey(pageName, intent))?.lastHealedSelector;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
updateHealedSelector(pageName: string, intent: string, selector: string): void {
|
|
58
|
+
const profile = this.getProfile(pageName, intent);
|
|
59
|
+
if (profile) {
|
|
60
|
+
profile.lastHealedSelector = selector;
|
|
61
|
+
profile.healingFailed = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
markHealingFailed(pageName: string, intent: string, failureType: string): void {
|
|
66
|
+
const profile = this.getProfile(pageName, intent);
|
|
67
|
+
if (profile) {
|
|
68
|
+
profile.healingFailed = true;
|
|
69
|
+
profile.failureType = failureType;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
hasHealingFailed(pageName: string, intent: string): boolean {
|
|
74
|
+
return this.profiles.get(this.getKey(pageName, intent))?.healingFailed || false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Page, Locator } from '@playwright/test';
|
|
2
|
+
import { ElementProfile } from './ElementProfileStore';
|
|
3
|
+
import { Logger } from '../utils/Logger';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AI-powered logic to heal failing locators using element profiles.
|
|
7
|
+
*/
|
|
8
|
+
export class LocatorHealing {
|
|
9
|
+
public lastFailureType: string | null = null;
|
|
10
|
+
|
|
11
|
+
constructor(private page: Page) { }
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Evaluates a selector string into a Playwright Locator.
|
|
15
|
+
*/
|
|
16
|
+
evaluateLocatorPublic(selector: string): Locator {
|
|
17
|
+
return this.page.locator(selector);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Orchestrates the AI healing process.
|
|
22
|
+
* @param originalSelector The broken selector.
|
|
23
|
+
* @param intent The intended action (for context).
|
|
24
|
+
* @param error The original error encountered.
|
|
25
|
+
* @param profile The historical fingerprint of the element.
|
|
26
|
+
*/
|
|
27
|
+
async healWithAI(
|
|
28
|
+
originalSelector: string,
|
|
29
|
+
intent: string,
|
|
30
|
+
error: Error | null,
|
|
31
|
+
profile?: ElementProfile
|
|
32
|
+
): Promise<string | null> {
|
|
33
|
+
Logger.info(`🤖 Starting AI healing for: ${intent}`);
|
|
34
|
+
|
|
35
|
+
if (!profile) {
|
|
36
|
+
Logger.error(`❌ No profile found for ${intent}. AI cannot heal without a baseline.`);
|
|
37
|
+
this.lastFailureType = 'NO_PROFILE';
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// In a real implementation, this would call an LLM with the profile and current DOM.
|
|
42
|
+
// For now, we simulate a smart recovery logic based on the profile attributes.
|
|
43
|
+
try {
|
|
44
|
+
const attributes = profile.attributes;
|
|
45
|
+
|
|
46
|
+
// Strategy 1: Try healing via Text content if available
|
|
47
|
+
if (attributes['text']) {
|
|
48
|
+
const textSelector = `text="${attributes['text']}"`;
|
|
49
|
+
const count = await this.page.locator(textSelector).count();
|
|
50
|
+
if (count === 1) {
|
|
51
|
+
Logger.info(`✅ Healed using Text content: ${textSelector}`);
|
|
52
|
+
return textSelector;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Strategy 2: Try healing via specific attributes (id, data-testid, etc.)
|
|
57
|
+
const criticalAttrs = ['data-testid', 'id', 'name', 'aria-label'];
|
|
58
|
+
for (const attr of criticalAttrs) {
|
|
59
|
+
if (attributes[attr]) {
|
|
60
|
+
const attrSelector = `[${attr}="${attributes[attr]}"]`;
|
|
61
|
+
const count = await this.page.locator(attrSelector).count();
|
|
62
|
+
if (count === 1) {
|
|
63
|
+
Logger.info(`✅ Healed using Attribute: ${attrSelector}`);
|
|
64
|
+
return attrSelector;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.lastFailureType = 'AI_RECOVERY_FAILED';
|
|
70
|
+
return null;
|
|
71
|
+
} catch {
|
|
72
|
+
this.lastFailureType = 'HEALING_ERROR';
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Legacy heal method (kept for compatibility or simple fixes).
|
|
79
|
+
*/
|
|
80
|
+
static heal(page: Page, locator: Locator): Locator {
|
|
81
|
+
Logger.error(`Locator failed: ${locator.toString()}`);
|
|
82
|
+
return locator;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility class for handling and parsing Playwright errors.
|
|
3
|
+
*/
|
|
4
|
+
export class ErrorUtils {
|
|
5
|
+
/**
|
|
6
|
+
* Converts a value to a standard Error object.
|
|
7
|
+
*/
|
|
8
|
+
static toError(error: unknown): Error {
|
|
9
|
+
if (error instanceof Error) return error;
|
|
10
|
+
return new Error(String(error));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extracts a clean message from a Playwright error.
|
|
15
|
+
*/
|
|
16
|
+
static getErrorMessage(error: unknown): string {
|
|
17
|
+
const err = this.toError(error);
|
|
18
|
+
// Remove stack trace and other noise if necessary
|
|
19
|
+
return err.message.split('\n')[0];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Simple logger utility for framework-level logging.
|
|
4
|
+
*/
|
|
5
|
+
export class Logger {
|
|
6
|
+
/**
|
|
7
|
+
* Logs an informational message.
|
|
8
|
+
*/
|
|
9
|
+
static info(msg: string) { console.log(`[INFO] ${msg}`); }
|
|
10
|
+
/**
|
|
11
|
+
* Logs an error message.
|
|
12
|
+
*/
|
|
13
|
+
static error(msg: string) { console.error(`[ERROR] ${msg}`); }
|
|
14
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Page, Locator, expect } from '@playwright/test';
|
|
2
|
+
import { AllureUtil } from '../reporting/AllureUtil';
|
|
3
|
+
|
|
4
|
+
export interface VisualCompareOptions {
|
|
5
|
+
/** Maximum allowed pixel difference ratio (0-1). Default: 0.01 (1%) */
|
|
6
|
+
threshold?: number;
|
|
7
|
+
/** Mask dynamic elements before comparison */
|
|
8
|
+
mask?: Locator[];
|
|
9
|
+
/** Full page screenshot vs viewport only */
|
|
10
|
+
fullPage?: boolean;
|
|
11
|
+
/** Custom snapshot name */
|
|
12
|
+
name?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Visual regression testing utility using Playwright's built-in snapshot comparison.
|
|
17
|
+
* Integrates with Allure reporting for diff visualization.
|
|
18
|
+
*/
|
|
19
|
+
export class VisualTesting {
|
|
20
|
+
constructor(private page: Page) {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Compare full page screenshot against baseline.
|
|
24
|
+
*/
|
|
25
|
+
async compareFullPage(options: VisualCompareOptions = {}): Promise<void> {
|
|
26
|
+
const name = options.name || 'full-page';
|
|
27
|
+
|
|
28
|
+
await AllureUtil.step(`Visual Check: ${name}`, async () => {
|
|
29
|
+
try {
|
|
30
|
+
await expect(this.page).toHaveScreenshot(`${name}.png`, {
|
|
31
|
+
fullPage: options.fullPage ?? true,
|
|
32
|
+
mask: options.mask,
|
|
33
|
+
maxDiffPixelRatio: options.threshold ?? 0.01,
|
|
34
|
+
});
|
|
35
|
+
AllureUtil.attachText('Visual Check', `✅ Passed: ${name}`);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
AllureUtil.attachText('Visual Check', `❌ Failed: ${name}`);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compare specific element screenshot against baseline.
|
|
45
|
+
*/
|
|
46
|
+
async compareElement(
|
|
47
|
+
locator: Locator,
|
|
48
|
+
options: VisualCompareOptions = {}
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const name = options.name || 'element';
|
|
51
|
+
|
|
52
|
+
await AllureUtil.step(`Visual Check: ${name}`, async () => {
|
|
53
|
+
try {
|
|
54
|
+
await expect(locator).toHaveScreenshot(`${name}.png`, {
|
|
55
|
+
maxDiffPixelRatio: options.threshold ?? 0.01,
|
|
56
|
+
});
|
|
57
|
+
AllureUtil.attachText('Visual Check', `✅ Passed: ${name}`);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
AllureUtil.attachText('Visual Check', `❌ Failed: ${name}`);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compare viewport screenshot (visible area only).
|
|
67
|
+
*/
|
|
68
|
+
async compareViewport(options: VisualCompareOptions = {}): Promise<void> {
|
|
69
|
+
const name = options.name || 'viewport';
|
|
70
|
+
|
|
71
|
+
await AllureUtil.step(`Visual Check: ${name}`, async () => {
|
|
72
|
+
try {
|
|
73
|
+
await expect(this.page).toHaveScreenshot(`${name}.png`, {
|
|
74
|
+
fullPage: false,
|
|
75
|
+
mask: options.mask,
|
|
76
|
+
maxDiffPixelRatio: options.threshold ?? 0.01,
|
|
77
|
+
});
|
|
78
|
+
AllureUtil.attachText('Visual Check', `✅ Passed: ${name}`);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
AllureUtil.attachText('Visual Check', `❌ Failed: ${name}`);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Take baseline screenshot (use --update-snapshots flag to update).
|
|
88
|
+
*/
|
|
89
|
+
async captureBaseline(name: string, fullPage = true): Promise<void> {
|
|
90
|
+
await AllureUtil.step(`Capture Baseline: ${name}`, async () => {
|
|
91
|
+
const screenshot = await this.page.screenshot({ fullPage });
|
|
92
|
+
AllureUtil.attachImage('Baseline', screenshot);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { Locator } from '@playwright/test';
|
|
2
|
+
import { ActionOrchestrator } from '../selfhealing/ActionOrchestrator';
|
|
3
|
+
import { ConfigManager } from '../config/ConfigManager';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resilient wrapper around Playwright Locator.
|
|
7
|
+
* Provides all standard interaction methods with integrated self-healing.
|
|
8
|
+
*/
|
|
9
|
+
export class ElementWrapper {
|
|
10
|
+
private healingEnabled: boolean;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private locator: Locator,
|
|
14
|
+
private orchestrator: ActionOrchestrator,
|
|
15
|
+
healingEnabled?: boolean
|
|
16
|
+
) {
|
|
17
|
+
// Default to the value in framework.config.json if not specified
|
|
18
|
+
this.healingEnabled = healingEnabled !== undefined
|
|
19
|
+
? healingEnabled
|
|
20
|
+
: ConfigManager.getConfig().healingEnabled;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Enables or disables self-healing for this element instance or action chain.
|
|
25
|
+
* @param enabled Whether healing should be enabled
|
|
26
|
+
*/
|
|
27
|
+
withHealing(enabled: boolean): ElementWrapper {
|
|
28
|
+
this.healingEnabled = enabled;
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Internal executor that delegates to the healing orchestrator.
|
|
34
|
+
*/
|
|
35
|
+
private async exec(
|
|
36
|
+
action: (l: Locator) => Promise<any>,
|
|
37
|
+
intent: string
|
|
38
|
+
) {
|
|
39
|
+
await this.orchestrator.runWithHealing(this.locator, intent, action, this.healingEnabled);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ===============================
|
|
43
|
+
* Interaction API
|
|
44
|
+
* =============================== */
|
|
45
|
+
|
|
46
|
+
async click(intent = 'click') {
|
|
47
|
+
await this.exec(l => l.click(), intent);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async dblClick(intent = 'double click') {
|
|
51
|
+
await this.exec(l => l.dblclick(), intent);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async rightClick(intent = 'right click') {
|
|
55
|
+
await this.exec(l => l.click({ button: 'right' }), intent);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async fill(value: string, intent?: string) {
|
|
59
|
+
await this.exec(l => l.fill(value), intent || `fill: ${value}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async type(text: string, intent?: string) {
|
|
63
|
+
await this.exec(
|
|
64
|
+
l => l.pressSequentially(text),
|
|
65
|
+
intent || `type: ${text}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async press(key: string, intent?: string) {
|
|
70
|
+
await this.exec(
|
|
71
|
+
l => l.press(key),
|
|
72
|
+
intent || `press: ${key}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async check(intent = 'check') {
|
|
77
|
+
await this.exec(l => l.check(), intent);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async uncheck(intent = 'uncheck') {
|
|
81
|
+
await this.exec(l => l.uncheck(), intent);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async hover(intent = 'hover') {
|
|
85
|
+
await this.exec(l => l.hover(), intent);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async selectOption(value: string | string[], intent?: string) {
|
|
89
|
+
await this.exec(
|
|
90
|
+
l => l.selectOption(value),
|
|
91
|
+
intent || `selectOption: ${value}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ===============================
|
|
96
|
+
* Retrieval API
|
|
97
|
+
* =============================== */
|
|
98
|
+
|
|
99
|
+
async getText(intent = 'getText'): Promise<string> {
|
|
100
|
+
let text = '';
|
|
101
|
+
await this.exec(async l => {
|
|
102
|
+
text = (await l.textContent()) ?? '';
|
|
103
|
+
}, intent);
|
|
104
|
+
return text;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async getInnerText(intent = 'getInnerText'): Promise<string> {
|
|
108
|
+
let text = '';
|
|
109
|
+
await this.exec(async l => {
|
|
110
|
+
text = await l.innerText();
|
|
111
|
+
}, intent);
|
|
112
|
+
return text;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async getAllTextContents(): Promise<string[]> {
|
|
116
|
+
return await this.locator.allTextContents();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async getAttribute(name: string): Promise<string | null> {
|
|
120
|
+
return await this.locator.getAttribute(name);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getValue(): Promise<string> {
|
|
124
|
+
return await this.locator.inputValue();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ===============================
|
|
128
|
+
* State API
|
|
129
|
+
* =============================== */
|
|
130
|
+
|
|
131
|
+
async isVisible(): Promise<boolean> {
|
|
132
|
+
return await this.locator.isVisible();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async isHidden(): Promise<boolean> {
|
|
136
|
+
return await this.locator.isHidden();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async isEnabled(): Promise<boolean> {
|
|
140
|
+
return await this.locator.isEnabled();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async isDisabled(): Promise<boolean> {
|
|
144
|
+
return await this.locator.isDisabled();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async isChecked(): Promise<boolean> {
|
|
148
|
+
return await this.locator.isChecked();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async isEditable(): Promise<boolean> {
|
|
152
|
+
return await this.locator.isEditable();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ===============================
|
|
156
|
+
* Wait API
|
|
157
|
+
* =============================== */
|
|
158
|
+
|
|
159
|
+
async waitForVisible(timeout = 5000) {
|
|
160
|
+
await this.exec(
|
|
161
|
+
l => l.waitFor({ state: 'visible', timeout }),
|
|
162
|
+
'waitForVisible'
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async waitForHidden(timeout = 5000) {
|
|
167
|
+
await this.exec(
|
|
168
|
+
l => l.waitFor({ state: 'hidden', timeout }),
|
|
169
|
+
'waitForHidden'
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async waitForEnabled(timeout = 5000) {
|
|
174
|
+
await this.exec(
|
|
175
|
+
async l => {
|
|
176
|
+
await l.waitFor({ state: 'attached', timeout });
|
|
177
|
+
await l.isEnabled();
|
|
178
|
+
},
|
|
179
|
+
'waitForEnabled'
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async waitForDetached(timeout = 5000) {
|
|
184
|
+
await this.exec(
|
|
185
|
+
l => l.waitFor({ state: 'detached', timeout }),
|
|
186
|
+
'waitForDetached'
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* ===============================
|
|
191
|
+
* Misc
|
|
192
|
+
* =============================== */
|
|
193
|
+
|
|
194
|
+
async scrollIntoView(intent = 'scrollIntoView') {
|
|
195
|
+
await this.exec(
|
|
196
|
+
l => l.scrollIntoViewIfNeeded(),
|
|
197
|
+
intent
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async focus(intent = 'focus') {
|
|
202
|
+
await this.exec(l => l.focus(), intent);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async blur(intent = 'blur') {
|
|
206
|
+
await this.exec(
|
|
207
|
+
l => l.evaluate(e => (e as HTMLElement).blur()),
|
|
208
|
+
intent
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { test as base } from '@playwright/test';
|
|
2
|
+
import { ActionOrchestrator } from '../core/selfhealing/ActionOrchestrator';
|
|
3
|
+
import { ApiClient } from '../core/api/ApiClient';
|
|
4
|
+
import { AllureUtil } from '../core/reporting/AllureUtil';
|
|
5
|
+
import { LoginPage } from '../quality/pages/LoginPage';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Defines the custom fixtures available in the framework.
|
|
9
|
+
*/
|
|
10
|
+
export type FrameworkFixtures = {
|
|
11
|
+
/** High-fidelity UI action orchestrator with AI healing */
|
|
12
|
+
orchestrator: ActionOrchestrator;
|
|
13
|
+
/** Robust API client with reporting and auth support */
|
|
14
|
+
apiClient: ApiClient;
|
|
15
|
+
/** Pre-instantiated LoginPage */
|
|
16
|
+
loginPage: LoginPage;
|
|
17
|
+
/** The current test locale */
|
|
18
|
+
locale: string;
|
|
19
|
+
/** Information about the current test retry state */
|
|
20
|
+
retryInfo: { current: number; max: number };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extended Playwright test object with enterprise fixtures.
|
|
25
|
+
*/
|
|
26
|
+
export const test = base.extend<FrameworkFixtures>({
|
|
27
|
+
/* ----------------------------------------
|
|
28
|
+
* UI Orchestrator
|
|
29
|
+
* ---------------------------------------- */
|
|
30
|
+
orchestrator: async ({ page }, use, testInfo) => {
|
|
31
|
+
const orchestrator = new ActionOrchestrator(page, {
|
|
32
|
+
pageName: testInfo.title // Initially use test title as context
|
|
33
|
+
});
|
|
34
|
+
await use(orchestrator);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/* ----------------------------------------
|
|
38
|
+
* API Client
|
|
39
|
+
* ---------------------------------------- */
|
|
40
|
+
apiClient: async ({ }, use) => {
|
|
41
|
+
const client = new ApiClient(process.env.API_BASE_URL || '');
|
|
42
|
+
await client.init();
|
|
43
|
+
await use(client);
|
|
44
|
+
await client.dispose();
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/* ----------------------------------------
|
|
48
|
+
* Page Objects
|
|
49
|
+
* ---------------------------------------- */
|
|
50
|
+
loginPage: async ({ page, orchestrator }, use) => {
|
|
51
|
+
await use(new LoginPage(page, orchestrator));
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/* ----------------------------------------
|
|
55
|
+
* Metadata & State
|
|
56
|
+
* ---------------------------------------- */
|
|
57
|
+
locale: async ({ }, use) => {
|
|
58
|
+
const locale = process.env.LOCALE || 'en';
|
|
59
|
+
await use(locale);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
retryInfo: async ({ }, use, testInfo) => {
|
|
63
|
+
await use({
|
|
64
|
+
current: testInfo.retry,
|
|
65
|
+
max: testInfo.project.retries
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Automatic global hooks for reporting and telemetry.
|
|
72
|
+
*/
|
|
73
|
+
test.afterEach(({ retryInfo }, testInfo) => {
|
|
74
|
+
// 📊 Auto-attach failure info to Allure
|
|
75
|
+
if (testInfo.status !== testInfo.expectedStatus) {
|
|
76
|
+
AllureUtil.attachText(
|
|
77
|
+
'Test Failure Details',
|
|
78
|
+
`Error: ${testInfo.error?.message || 'Unknown'}\nStack: ${testInfo.error?.stack || 'N/A'}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 🔄 Log flaky test telemetry
|
|
83
|
+
if (retryInfo.current > 0) {
|
|
84
|
+
AllureUtil.attachText(
|
|
85
|
+
'Retry Telemetry',
|
|
86
|
+
`This test is being retried. Attempt: ${retryInfo.current} / ${retryInfo.max}`
|
|
87
|
+
);
|
|
88
|
+
console.warn(`⚠️ Flaky test detected: "${testInfo.title}" (Retry ${retryInfo.current})`);
|
|
89
|
+
}
|
|
90
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from './core/api/auth/CookieAuth';
|
|
2
|
+
export * from './core/api/auth/AuthStrategy';
|
|
3
|
+
export * from './core/api/auth/OAuth2Auth';
|
|
4
|
+
export * from './core/api/ApiClient';
|
|
5
|
+
export * from './core/reporting/AllureUtil';
|
|
6
|
+
export * from './core/i18n/Localization';
|
|
7
|
+
export * from './core/pages/BasePage';
|
|
8
|
+
export * from './core/wrappers/ElementWrapper';
|
|
9
|
+
export * from './core/selfhealing/LocatorHealing';
|
|
10
|
+
export * from './core/selfhealing/ElementProfileStore';
|
|
11
|
+
export * from './core/selfhealing/ActionOrchestrator';
|
|
12
|
+
export * from './core/config/ConfigManager';
|
|
13
|
+
export * from './core/utils/ErrorUtils';
|
|
14
|
+
export * from './core/utils/Logger';
|
|
15
|
+
export * from './core/visual/VisualTesting';
|
|
16
|
+
export * from './quality/pages/LoginPage';
|
|
17
|
+
export * from './fixtures/fixtures';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Page } from '@playwright/test';
|
|
2
|
+
import { BasePage } from '../../core/pages/BasePage';
|
|
3
|
+
import { ActionOrchestrator } from '../../core/selfhealing/ActionOrchestrator';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Example Page Object (LoginPage) using the Resilient ElementWrapper architecture.
|
|
7
|
+
* Elements are self-healing objects created via this.element().
|
|
8
|
+
* Supports Dependency Injection for use in Fixtures.
|
|
9
|
+
*/
|
|
10
|
+
export class LoginPage extends BasePage {
|
|
11
|
+
constructor(page: Page, orchestrator?: ActionOrchestrator) {
|
|
12
|
+
super(page, { pageName: 'LoginPage', orchestrator });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Define elements as Resilient ElementWrappers
|
|
16
|
+
private readonly usernameInput = this.element('#username');
|
|
17
|
+
private readonly passwordInput = this.element('#password');
|
|
18
|
+
private readonly loginButton = this.element('button[type="submit"]');
|
|
19
|
+
private readonly errorMessage = this.element('.error-message');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Performs login. Actions are naturally resilient.
|
|
23
|
+
*/
|
|
24
|
+
async login(user: string, pass: string) {
|
|
25
|
+
await this.usernameInput.fill(user, 'Enter Username');
|
|
26
|
+
await this.passwordInput.fill(pass, 'Enter Password');
|
|
27
|
+
await this.loginButton.click('Click Login Button');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns error message.
|
|
32
|
+
*/
|
|
33
|
+
async getErrorMessage(): Promise<string> {
|
|
34
|
+
return await this.errorMessage.getText('Get Login Error Message');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { test } from '@playwright/test';
|
|
2
|
+
import { VisualTesting } from '../core/visual/VisualTesting';
|
|
3
|
+
|
|
4
|
+
test.describe('Visual Regression Examples', () => {
|
|
5
|
+
test('Full page comparison', async ({ page }) => {
|
|
6
|
+
const visual = new VisualTesting(page);
|
|
7
|
+
|
|
8
|
+
await page.goto('https://playwright.dev');
|
|
9
|
+
await page.waitForLoadState('networkidle');
|
|
10
|
+
|
|
11
|
+
await visual.compareFullPage({
|
|
12
|
+
name: 'playwright-homepage',
|
|
13
|
+
threshold: 0.01
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('Element comparison', async ({ page }) => {
|
|
18
|
+
const visual = new VisualTesting(page);
|
|
19
|
+
|
|
20
|
+
await page.goto('https://playwright.dev');
|
|
21
|
+
const header = page.locator('header');
|
|
22
|
+
|
|
23
|
+
await visual.compareElement(header, {
|
|
24
|
+
name: 'header-element'
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('Viewport comparison with masking', async ({ page }) => {
|
|
29
|
+
const visual = new VisualTesting(page);
|
|
30
|
+
|
|
31
|
+
await page.goto('https://playwright.dev');
|
|
32
|
+
|
|
33
|
+
await visual.compareViewport({
|
|
34
|
+
name: 'viewport-masked',
|
|
35
|
+
mask: [page.locator('.theme-toggle')]
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
Binary file
|
|
Binary file
|