pw-core 1.1.0 → 1.2.0
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/dist/component/table.js +5 -4
- package/dist/helpers.d.ts +1 -1
- package/dist/helpers.js +21 -9
- package/dist/page/actions/locator-actions.js +34 -1
- package/dist/page/actions/page-actions.d.ts +11 -6
- package/dist/page/actions/page-actions.js +12 -12
- package/dist/page/assertions/verify-chain.js +3 -2
- package/dist/page/assertions/verify-helpers.d.ts +9 -3
- package/dist/page/assertions/verify-helpers.js +6 -6
- package/dist/page/config.d.ts +59 -9
- package/dist/page/index.d.ts +1 -0
- package/dist/page/index.js +1 -0
- package/dist/page/locators/dynamic-locator-resolver.d.ts +49 -0
- package/dist/page/locators/dynamic-locator-resolver.js +167 -0
- package/dist/page/locators/resolver.d.ts +8 -7
- package/dist/page/locators/resolver.js +47 -9
- package/dist/page/registry.d.ts +8 -6
- package/dist/page/typed-page.d.ts +10 -9
- package/dist/page/typed-page.js +57 -9
- package/dist/page/types/validation.d.ts +22 -0
- package/dist/page/types/validation.js +2 -0
- package/dist/page/utils/caller-location.d.ts +5 -0
- package/dist/page/utils/caller-location.js +78 -0
- package/dist/page/utils/formatter.js +8 -1
- package/package.json +1 -1
package/dist/component/table.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Table = exports.TableRows = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
+
const caller_location_1 = require("../page/utils/caller-location");
|
|
5
6
|
/**
|
|
6
7
|
* A custom Array subclass representing a list of table rows.
|
|
7
8
|
* Inherits all standard Array methods while adding type-safe get and getAll capabilities.
|
|
@@ -43,7 +44,7 @@ class Table {
|
|
|
43
44
|
return test_1.test.step(`Get headers of table`, async () => {
|
|
44
45
|
const headers = await this.root.locator('th').evaluateAll(ths => ths.map(th => th.textContent?.trim().toLowerCase() || ''));
|
|
45
46
|
return headers;
|
|
46
|
-
});
|
|
47
|
+
}, { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
49
50
|
* Retrieves all the rows of the table as typed objects.
|
|
@@ -67,7 +68,7 @@ class Table {
|
|
|
67
68
|
});
|
|
68
69
|
}, headers);
|
|
69
70
|
return rowsData.filter((r) => r !== null);
|
|
70
|
-
});
|
|
71
|
+
}, { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
74
|
* Retrieves all rows from the table, returned as a custom TableRows collection
|
|
@@ -85,7 +86,7 @@ class Table {
|
|
|
85
86
|
return await this.root.locator('tbody tr, tr[data-testid="transaction-row"]').evaluateAll(trs => {
|
|
86
87
|
return trs.filter(tr => tr.querySelectorAll('td').length > 0).length;
|
|
87
88
|
});
|
|
88
|
-
});
|
|
89
|
+
}, { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
89
90
|
}
|
|
90
91
|
/**
|
|
91
92
|
* Retrieves the value of a specific cell by row index and column key.
|
|
@@ -106,7 +107,7 @@ class Table {
|
|
|
106
107
|
const cell = dataRows[rowIndex].querySelectorAll('td')[columnIndex];
|
|
107
108
|
return cell?.textContent?.trim() ?? '';
|
|
108
109
|
}, { rowIndex, columnIndex });
|
|
109
|
-
});
|
|
110
|
+
}, { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
exports.Table = Table;
|
package/dist/helpers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Page } from '@playwright/test';
|
|
2
2
|
/** Read a value from the browser's localStorage. Returns null if the key is absent. */
|
|
3
3
|
export declare function getLocalStorage(page: Page, key: string): Promise<string | null>;
|
|
4
4
|
/** Write a value to the browser's localStorage. */
|
package/dist/helpers.js
CHANGED
|
@@ -5,21 +5,31 @@ exports.setLocalStorage = setLocalStorage;
|
|
|
5
5
|
exports.getSessionStorage = getSessionStorage;
|
|
6
6
|
exports.setSessionStorage = setSessionStorage;
|
|
7
7
|
exports.seedSessionStorage = seedSessionStorage;
|
|
8
|
+
const test_1 = require("@playwright/test");
|
|
9
|
+
const caller_location_1 = require("./page/utils/caller-location");
|
|
8
10
|
/** Read a value from the browser's localStorage. Returns null if the key is absent. */
|
|
9
11
|
async function getLocalStorage(page, key) {
|
|
10
|
-
return
|
|
12
|
+
return test_1.test.step(`Get localStorage "${key}"`, async () => {
|
|
13
|
+
return page.evaluate((k) => localStorage.getItem(k), key);
|
|
14
|
+
}, { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
11
15
|
}
|
|
12
16
|
/** Write a value to the browser's localStorage. */
|
|
13
17
|
async function setLocalStorage(page, key, value) {
|
|
14
|
-
await
|
|
18
|
+
await test_1.test.step(`Set localStorage "${key}"`, async () => {
|
|
19
|
+
await page.evaluate(([k, v]) => localStorage.setItem(k, v), [key, value]);
|
|
20
|
+
}, { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
15
21
|
}
|
|
16
22
|
/** Read a value from the browser's sessionStorage. Returns null if the key is absent. */
|
|
17
23
|
async function getSessionStorage(page, key) {
|
|
18
|
-
return
|
|
24
|
+
return test_1.test.step(`Get sessionStorage "${key}"`, async () => {
|
|
25
|
+
return page.evaluate((k) => sessionStorage.getItem(k), key);
|
|
26
|
+
}, { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
19
27
|
}
|
|
20
28
|
/** Write a value to the browser's sessionStorage. */
|
|
21
29
|
async function setSessionStorage(page, key, value) {
|
|
22
|
-
await
|
|
30
|
+
await test_1.test.step(`Set sessionStorage "${key}"`, async () => {
|
|
31
|
+
await page.evaluate(([k, v]) => sessionStorage.setItem(k, v), [key, value]);
|
|
32
|
+
}, { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
23
33
|
}
|
|
24
34
|
/**
|
|
25
35
|
* Seeds sessionStorage entries via addInitScript before any navigation.
|
|
@@ -30,10 +40,12 @@ async function seedSessionStorage(page, entries) {
|
|
|
30
40
|
const cookies = await page.context().cookies();
|
|
31
41
|
if (cookies.length > 0) {
|
|
32
42
|
const pairs = Object.entries(entries);
|
|
33
|
-
await
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
await test_1.test.step('Seed sessionStorage', async () => {
|
|
44
|
+
await page.addInitScript((items) => {
|
|
45
|
+
for (const [key, value] of items) {
|
|
46
|
+
sessionStorage.setItem(key, value);
|
|
47
|
+
}
|
|
48
|
+
}, pairs);
|
|
49
|
+
}, { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
38
50
|
}
|
|
39
51
|
}
|
|
@@ -5,6 +5,7 @@ exports.defineActionMethods = defineActionMethods;
|
|
|
5
5
|
const test_1 = require("@playwright/test");
|
|
6
6
|
const config_1 = require("../config");
|
|
7
7
|
const formatter_1 = require("../utils/formatter");
|
|
8
|
+
const caller_location_1 = require("../utils/caller-location");
|
|
8
9
|
function executeAction(prop, resolveLocatorFn, timeout, args) {
|
|
9
10
|
const [locatorKey, ...methodArgs] = args;
|
|
10
11
|
let optNth = undefined;
|
|
@@ -47,9 +48,41 @@ function executeAction(prop, resolveLocatorFn, timeout, args) {
|
|
|
47
48
|
methodArgs[1] = playwrightOpts;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
let shouldMask = false;
|
|
52
|
+
if (prop === 'fill' && methodArgs.length > 0) {
|
|
53
|
+
const opts = methodArgs[1];
|
|
54
|
+
if (opts && typeof opts === 'object' && opts.mask !== undefined) {
|
|
55
|
+
shouldMask = opts.mask === true;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const targetStr = (0, formatter_1.formatTarget)(locatorKey);
|
|
59
|
+
const targetStrLower = targetStr.toLowerCase();
|
|
60
|
+
shouldMask = targetStrLower.includes('pass') || targetStrLower.includes('pw');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (prop === 'fill' && shouldMask) {
|
|
64
|
+
return test_1.test.step(stepName, async () => {
|
|
65
|
+
await locator.focus();
|
|
66
|
+
await locator.evaluate((el, val) => {
|
|
67
|
+
const inputEl = el;
|
|
68
|
+
const prototype = el.tagName === 'TEXTAREA'
|
|
69
|
+
? window.HTMLTextAreaElement.prototype
|
|
70
|
+
: window.HTMLInputElement.prototype;
|
|
71
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
|
|
72
|
+
if (descriptor && descriptor.set) {
|
|
73
|
+
descriptor.set.call(inputEl, val);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
inputEl.value = val;
|
|
77
|
+
}
|
|
78
|
+
inputEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
79
|
+
inputEl.dispatchEvent(new Event('change', { bubbles: true }));
|
|
80
|
+
}, methodArgs[0]);
|
|
81
|
+
}, { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
82
|
+
}
|
|
50
83
|
return test_1.test.step(stepName, () => {
|
|
51
84
|
return method.apply(locator, methodArgs);
|
|
52
|
-
});
|
|
85
|
+
}, { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
53
86
|
}
|
|
54
87
|
function defineActionMethods(instance, resolveLocatorFn, timeout) {
|
|
55
88
|
const locatorMethods = [...config_1.zeroArgMethodsList, ...config_1.oneArgMethodsList];
|
|
@@ -6,34 +6,39 @@ type GotoOptions = {
|
|
|
6
6
|
};
|
|
7
7
|
type ToHaveURLOptions = Parameters<ReturnType<typeof playwrightExpect<Page>>['toHaveURL']>[1];
|
|
8
8
|
type ToHaveTitleOptions = Parameters<ReturnType<typeof playwrightExpect<Page>>['toHaveTitle']>[1];
|
|
9
|
+
type StepLocation = {
|
|
10
|
+
file: string;
|
|
11
|
+
line: number;
|
|
12
|
+
column: number;
|
|
13
|
+
};
|
|
9
14
|
/**
|
|
10
15
|
* Navigate to the page URL defined in the page config.
|
|
11
16
|
* Wraps {@link https://playwright.dev/docs/api/class-page#page-goto Page.goto} in a test step.
|
|
12
17
|
*/
|
|
13
|
-
export declare function goto(page: Page, url: string | undefined, constructorName: string, options?: GotoOptions): Promise<void>;
|
|
18
|
+
export declare function goto(page: Page, url: string | undefined, constructorName: string, options?: GotoOptions, location?: StepLocation): Promise<void>;
|
|
14
19
|
/**
|
|
15
20
|
* Assert the current URL matches the page config URL or a custom pattern.
|
|
16
21
|
* Uses {@link https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-url expect(page).toHaveURL}.
|
|
17
22
|
*/
|
|
18
|
-
export declare function verifyURL(page: Page, url: string | undefined, constructorName: string, urlOrOptions?: string | RegExp | ToHaveURLOptions, options?: ToHaveURLOptions): Promise<void>;
|
|
23
|
+
export declare function verifyURL(page: Page, url: string | undefined, constructorName: string, urlOrOptions?: string | RegExp | ToHaveURLOptions, options?: ToHaveURLOptions, location?: StepLocation): Promise<void>;
|
|
19
24
|
/**
|
|
20
25
|
* Assert the page title matches the expected value.
|
|
21
26
|
* Uses {@link https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-title expect(page).toHaveTitle}.
|
|
22
27
|
*/
|
|
23
|
-
export declare function verifyTitle(page: Page, title: string | RegExp, options?: ToHaveTitleOptions): Promise<void>;
|
|
28
|
+
export declare function verifyTitle(page: Page, title: string | RegExp, options?: ToHaveTitleOptions, location?: StepLocation): Promise<void>;
|
|
24
29
|
/**
|
|
25
30
|
* Reload the page.
|
|
26
31
|
* Wraps {@link https://playwright.dev/docs/api/class-page#page-reload Page.reload} in a test step.
|
|
27
32
|
*/
|
|
28
|
-
export declare function reload(page: Page, options?: Parameters<Page['reload']>[0]): Promise<void>;
|
|
33
|
+
export declare function reload(page: Page, options?: Parameters<Page['reload']>[0], location?: StepLocation): Promise<void>;
|
|
29
34
|
/**
|
|
30
35
|
* Wait for the page to reach a load state.
|
|
31
36
|
* Wraps {@link https://playwright.dev/docs/api/class-page#page-wait-for-load-state Page.waitForLoadState} in a test step.
|
|
32
37
|
*/
|
|
33
|
-
export declare function waitForLoadState(page: Page, state?: Parameters<Page['waitForLoadState']>[0], options?: Parameters<Page['waitForLoadState']>[1]): Promise<void>;
|
|
38
|
+
export declare function waitForLoadState(page: Page, state?: Parameters<Page['waitForLoadState']>[0], options?: Parameters<Page['waitForLoadState']>[1], location?: StepLocation): Promise<void>;
|
|
34
39
|
/**
|
|
35
40
|
* Wait for navigation to a URL. Defaults to the page config URL when no pattern is given.
|
|
36
41
|
* Wraps {@link https://playwright.dev/docs/api/class-page#page-wait-for-url Page.waitForURL} in a test step.
|
|
37
42
|
*/
|
|
38
|
-
export declare function waitForURL(page: Page, url: string | undefined, constructorName: string, urlOrOptions?: string | RegExp | Parameters<Page['waitForURL']>[1], options?: Parameters<Page['waitForURL']>[1]): Promise<void>;
|
|
43
|
+
export declare function waitForURL(page: Page, url: string | undefined, constructorName: string, urlOrOptions?: string | RegExp | Parameters<Page['waitForURL']>[1], options?: Parameters<Page['waitForURL']>[1], location?: StepLocation): Promise<void>;
|
|
39
44
|
export {};
|
|
@@ -33,59 +33,59 @@ function resolveUrlPattern(url, constructorName, urlOrOptions, options) {
|
|
|
33
33
|
* Navigate to the page URL defined in the page config.
|
|
34
34
|
* Wraps {@link https://playwright.dev/docs/api/class-page#page-goto Page.goto} in a test step.
|
|
35
35
|
*/
|
|
36
|
-
async function goto(page, url, constructorName, options) {
|
|
36
|
+
async function goto(page, url, constructorName, options, location) {
|
|
37
37
|
await test_1.test.step(`Goto "${url || ''}"`, async () => {
|
|
38
38
|
if (!url) {
|
|
39
39
|
throw new Error(`URL is not defined on ${constructorName}`);
|
|
40
40
|
}
|
|
41
41
|
await page.goto(url, options);
|
|
42
|
-
});
|
|
42
|
+
}, { box: true, location });
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
45
|
* Assert the current URL matches the page config URL or a custom pattern.
|
|
46
46
|
* Uses {@link https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-url expect(page).toHaveURL}.
|
|
47
47
|
*/
|
|
48
|
-
async function verifyURL(page, url, constructorName, urlOrOptions, options) {
|
|
48
|
+
async function verifyURL(page, url, constructorName, urlOrOptions, options, location) {
|
|
49
49
|
const { targetUrl, actualOptions } = resolveUrlPattern(url, constructorName, urlOrOptions, options);
|
|
50
50
|
const stepName = `Verify URL matches "${targetUrl.toString()}"`;
|
|
51
51
|
await test_1.test.step(stepName, async () => {
|
|
52
52
|
await (0, test_1.expect)(page).toHaveURL(targetUrl, actualOptions);
|
|
53
|
-
});
|
|
53
|
+
}, { box: true, location });
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* Assert the page title matches the expected value.
|
|
57
57
|
* Uses {@link https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-title expect(page).toHaveTitle}.
|
|
58
58
|
*/
|
|
59
|
-
async function verifyTitle(page, title, options) {
|
|
59
|
+
async function verifyTitle(page, title, options, location) {
|
|
60
60
|
const stepName = `Verify title matches "${title}"`;
|
|
61
61
|
await test_1.test.step(stepName, async () => {
|
|
62
62
|
await (0, test_1.expect)(page).toHaveTitle(title, options);
|
|
63
|
-
});
|
|
63
|
+
}, { box: true, location });
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* Reload the page.
|
|
67
67
|
* Wraps {@link https://playwright.dev/docs/api/class-page#page-reload Page.reload} in a test step.
|
|
68
68
|
*/
|
|
69
|
-
async function reload(page, options) {
|
|
69
|
+
async function reload(page, options, location) {
|
|
70
70
|
await test_1.test.step('Reload page', async () => {
|
|
71
71
|
await page.reload(options);
|
|
72
|
-
});
|
|
72
|
+
}, { box: true, location });
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
75
|
* Wait for the page to reach a load state.
|
|
76
76
|
* Wraps {@link https://playwright.dev/docs/api/class-page#page-wait-for-load-state Page.waitForLoadState} in a test step.
|
|
77
77
|
*/
|
|
78
|
-
async function waitForLoadState(page, state, options) {
|
|
78
|
+
async function waitForLoadState(page, state, options, location) {
|
|
79
79
|
const stepName = `Wait for load state "${state ?? 'load'}"`;
|
|
80
80
|
await test_1.test.step(stepName, async () => {
|
|
81
81
|
await page.waitForLoadState(state, options);
|
|
82
|
-
});
|
|
82
|
+
}, { box: true, location });
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
* Wait for navigation to a URL. Defaults to the page config URL when no pattern is given.
|
|
86
86
|
* Wraps {@link https://playwright.dev/docs/api/class-page#page-wait-for-url Page.waitForURL} in a test step.
|
|
87
87
|
*/
|
|
88
|
-
async function waitForURL(page, url, constructorName, urlOrOptions, options) {
|
|
88
|
+
async function waitForURL(page, url, constructorName, urlOrOptions, options, location) {
|
|
89
89
|
let targetUrl;
|
|
90
90
|
let actualOptions = options;
|
|
91
91
|
if (urlOrOptions !== undefined && (typeof urlOrOptions === 'string' || urlOrOptions instanceof RegExp)) {
|
|
@@ -103,5 +103,5 @@ async function waitForURL(page, url, constructorName, urlOrOptions, options) {
|
|
|
103
103
|
const stepName = `Wait for URL "${targetUrl.toString()}"`;
|
|
104
104
|
await test_1.test.step(stepName, async () => {
|
|
105
105
|
await page.waitForURL(targetUrl, actualOptions);
|
|
106
|
-
});
|
|
106
|
+
}, { box: true, location });
|
|
107
107
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createVerifyChain = createVerifyChain;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const formatter_1 = require("../utils/formatter");
|
|
6
|
+
const caller_location_1 = require("../utils/caller-location");
|
|
6
7
|
function createVerifyChain(resolveLocator, target, verifyOptions, isSoft) {
|
|
7
8
|
const defaultNth = verifyOptions?.nth;
|
|
8
9
|
const defaultHasText = verifyOptions?.hasText;
|
|
@@ -18,7 +19,7 @@ function createVerifyChain(resolveLocator, target, verifyOptions, isSoft) {
|
|
|
18
19
|
const expectation = expectFn(locator, stepName);
|
|
19
20
|
const match = isNegated ? expectation.not : expectation;
|
|
20
21
|
await match.toBeVisible(options);
|
|
21
|
-
});
|
|
22
|
+
}, { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
22
23
|
};
|
|
23
24
|
return new Proxy(baseFn, {
|
|
24
25
|
get(targetObj, prop) {
|
|
@@ -53,7 +54,7 @@ function createVerifyChain(resolveLocator, target, verifyOptions, isSoft) {
|
|
|
53
54
|
const expectation = expectFn(locator, stepName);
|
|
54
55
|
const match = isNegated ? expectation.not : expectation;
|
|
55
56
|
await match[prop](...args);
|
|
56
|
-
});
|
|
57
|
+
}, { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
});
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { Locator, expect as playwrightExpect } from '@playwright/test';
|
|
2
|
+
type StepLocation = {
|
|
3
|
+
file: string;
|
|
4
|
+
line: number;
|
|
5
|
+
column: number;
|
|
6
|
+
};
|
|
2
7
|
export declare function verifyHidden(resolveLocator: (target: any, options?: {
|
|
3
8
|
nth?: number;
|
|
4
9
|
hasText?: string | RegExp;
|
|
@@ -7,7 +12,7 @@ export declare function verifyHidden(resolveLocator: (target: any, options?: {
|
|
|
7
12
|
nth?: number;
|
|
8
13
|
hasText?: string | RegExp;
|
|
9
14
|
message?: string;
|
|
10
|
-
}): Promise<void>;
|
|
15
|
+
}, location?: StepLocation): Promise<void>;
|
|
11
16
|
export declare function verifyEnabled(resolveLocator: (target: any, options?: {
|
|
12
17
|
nth?: number;
|
|
13
18
|
hasText?: string | RegExp;
|
|
@@ -16,7 +21,7 @@ export declare function verifyEnabled(resolveLocator: (target: any, options?: {
|
|
|
16
21
|
nth?: number;
|
|
17
22
|
hasText?: string | RegExp;
|
|
18
23
|
message?: string;
|
|
19
|
-
}): Promise<void>;
|
|
24
|
+
}, location?: StepLocation): Promise<void>;
|
|
20
25
|
export declare function verifyDisabled(resolveLocator: (target: any, options?: {
|
|
21
26
|
nth?: number;
|
|
22
27
|
hasText?: string | RegExp;
|
|
@@ -25,4 +30,5 @@ export declare function verifyDisabled(resolveLocator: (target: any, options?: {
|
|
|
25
30
|
nth?: number;
|
|
26
31
|
hasText?: string | RegExp;
|
|
27
32
|
message?: string;
|
|
28
|
-
}): Promise<void>;
|
|
33
|
+
}, location?: StepLocation): Promise<void>;
|
|
34
|
+
export {};
|
|
@@ -5,24 +5,24 @@ exports.verifyEnabled = verifyEnabled;
|
|
|
5
5
|
exports.verifyDisabled = verifyDisabled;
|
|
6
6
|
const test_1 = require("@playwright/test");
|
|
7
7
|
const formatter_1 = require("../utils/formatter");
|
|
8
|
-
async function verifyHidden(resolveLocator, target, options) {
|
|
8
|
+
async function verifyHidden(resolveLocator, target, options, location) {
|
|
9
9
|
const stepName = options?.message ?? (0, formatter_1.formatAssertionDescription)(target, 'toBeHidden', false, [options]);
|
|
10
10
|
await test_1.test.step(stepName, async () => {
|
|
11
11
|
const locator = resolveLocator(target, { nth: options?.nth, hasText: options?.hasText });
|
|
12
12
|
await (0, test_1.expect)(locator, stepName).toBeHidden(options);
|
|
13
|
-
});
|
|
13
|
+
}, { box: true, location });
|
|
14
14
|
}
|
|
15
|
-
async function verifyEnabled(resolveLocator, target, options) {
|
|
15
|
+
async function verifyEnabled(resolveLocator, target, options, location) {
|
|
16
16
|
const stepName = options?.message ?? (0, formatter_1.formatAssertionDescription)(target, 'toBeEnabled', false, [options]);
|
|
17
17
|
await test_1.test.step(stepName, async () => {
|
|
18
18
|
const locator = resolveLocator(target, { nth: options?.nth, hasText: options?.hasText });
|
|
19
19
|
await (0, test_1.expect)(locator, stepName).toBeEnabled(options);
|
|
20
|
-
});
|
|
20
|
+
}, { box: true, location });
|
|
21
21
|
}
|
|
22
|
-
async function verifyDisabled(resolveLocator, target, options) {
|
|
22
|
+
async function verifyDisabled(resolveLocator, target, options, location) {
|
|
23
23
|
const stepName = options?.message ?? (0, formatter_1.formatAssertionDescription)(target, 'toBeDisabled', false, [options]);
|
|
24
24
|
await test_1.test.step(stepName, async () => {
|
|
25
25
|
const locator = resolveLocator(target, { nth: options?.nth, hasText: options?.hasText });
|
|
26
26
|
await (0, test_1.expect)(locator, stepName).toBeDisabled(options);
|
|
27
|
-
});
|
|
27
|
+
}, { box: true, location });
|
|
28
28
|
}
|
package/dist/page/config.d.ts
CHANGED
|
@@ -1,15 +1,52 @@
|
|
|
1
1
|
import { Locator, Page } from '@playwright/test';
|
|
2
|
-
|
|
2
|
+
import type { DynamicTestIdEntry, DynamicSelectorEntry } from './locators/dynamic-locator-resolver.js';
|
|
3
|
+
export { ProxyLocatorMethods } from './types/proxy-methods.js';
|
|
4
|
+
export type { DynamicLocatorEntry, DynamicTestIdEntry, DynamicSelectorEntry } from './locators/dynamic-locator-resolver.js';
|
|
3
5
|
export declare const zeroArgMethodsList: readonly ["click", "dblclick", "hover", "focus", "blur", "check", "uncheck", "clear", "waitFor", "isChecked", "isDisabled", "isVisible", "textContent", "innerText", "allInnerTexts", "allTextContents", "count", "scrollIntoViewIfNeeded", "boundingBox"];
|
|
4
6
|
export declare const oneArgMethodsList: readonly ["fill", "press", "pressSequentially", "selectOption", "setInputFiles", "getAttribute", "dragTo"];
|
|
5
7
|
export type AllowedZeroArgMethods = typeof zeroArgMethodsList[number];
|
|
6
8
|
export type AllowedOneArgMethods = typeof oneArgMethodsList[number];
|
|
7
9
|
export type AllowedMethodKeys = AllowedZeroArgMethods | AllowedOneArgMethods;
|
|
10
|
+
import { ValidateDynamicEntryProperties, HasDuplicatePlaceholders } from './types/validation.js';
|
|
11
|
+
/**
|
|
12
|
+
* Replaces every `{Name}` placeholder in `Pattern` with `Capitalize<Value>`
|
|
13
|
+
* where `Value` is drawn from the corresponding array in `Entry`.
|
|
14
|
+
* Template-literal unions distribute automatically, producing the full cartesian product.
|
|
15
|
+
*/
|
|
16
|
+
type ReplacePattern<Pattern extends string, Entry> = Pattern extends `${infer Before}{${infer Name}}${infer After}` ? Name extends keyof Entry ? Entry[Name] extends readonly string[] ? `${Before}${Capitalize<Entry[Name][number]>}${ReplacePattern<After, Entry> & string}` : never : never : Pattern;
|
|
17
|
+
/**
|
|
18
|
+
* Expands a dynamic key pattern into the union of all concrete (camelCased) keys.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* type Keys = ExpandDynamicKey<
|
|
23
|
+
* "{status}{id}Chart",
|
|
24
|
+
* { status: readonly ['active', 'inactive']; id: readonly ['line', 'bar']; testId: "status-id-chart" }
|
|
25
|
+
* >;
|
|
26
|
+
* // => "activeLineChart" | "activeBarChart" | "inactiveLineChart" | "inactiveBarChart"
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
type ExpandDynamicKey<Pattern extends string, Entry> = Uncapitalize<ReplacePattern<Pattern, Entry>>;
|
|
30
|
+
/**
|
|
31
|
+
* Resolves every key in a testIds record:
|
|
32
|
+
* - string values → the key itself (static locator)
|
|
33
|
+
* - object values → the expanded dynamic key union
|
|
34
|
+
*/
|
|
35
|
+
type ResolvedTestIdKeys<I> = {
|
|
36
|
+
[K in keyof I & string]: I[K] extends string ? K : ExpandDynamicKey<K, I[K]>;
|
|
37
|
+
}[keyof I & string];
|
|
38
|
+
/**
|
|
39
|
+
* Validates all entries in a testIds or selectors record.
|
|
40
|
+
* Static (string) entries pass through. Dynamic entries are checked via ValidateDynamicEntryProperties.
|
|
41
|
+
*/
|
|
42
|
+
type ValidateTestIds<I, TargetKey extends 'testId' | 'selector'> = {
|
|
43
|
+
[K in keyof I & string]: I[K] extends string ? string : K extends `${string}{${string}}${string}` ? HasDuplicatePlaceholders<K> extends true ? "Error: Pattern contains duplicate placeholders" : ValidateDynamicEntryProperties<K, I[K], TargetKey> : I[K];
|
|
44
|
+
};
|
|
8
45
|
export type PageKeys<T> = (T extends {
|
|
9
46
|
testIds?: infer I;
|
|
10
|
-
} ?
|
|
47
|
+
} ? I extends Record<string, any> ? ResolvedTestIdKeys<I> : never : never) | (T extends {
|
|
11
48
|
selectors?: infer S;
|
|
12
|
-
} ?
|
|
49
|
+
} ? S extends Record<string, any> ? ResolvedTestIdKeys<S> : never : never);
|
|
13
50
|
export type TypedLocators<T> = {
|
|
14
51
|
[K in PageKeys<T>]: Locator;
|
|
15
52
|
};
|
|
@@ -17,16 +54,29 @@ export type ChainedKeys<T> = `${PageKeys<T>}.${PageKeys<T>}`;
|
|
|
17
54
|
export type TargetKey<T> = PageKeys<T> | ChainedKeys<T> | Locator;
|
|
18
55
|
export type ValidateTarget<K, T> = K extends Locator ? Locator : K extends (PageKeys<T> | ChainedKeys<T>) ? K : never;
|
|
19
56
|
export declare function getOptionsArgumentIndex(methodName: AllowedMethodKeys): number;
|
|
20
|
-
|
|
57
|
+
/** Map type accepted by the `testIds` property in a page config. */
|
|
58
|
+
export type TestIdMap = Record<string, string | DynamicTestIdEntry>;
|
|
59
|
+
export type SelectorMap = Record<string, string | DynamicSelectorEntry>;
|
|
60
|
+
export type PageConfig = {
|
|
21
61
|
url?: string;
|
|
22
|
-
testIds?:
|
|
62
|
+
testIds?: TestIdMap;
|
|
63
|
+
selectors?: SelectorMap;
|
|
64
|
+
Class?: new (page: Page, config?: any) => any;
|
|
65
|
+
};
|
|
66
|
+
export type ValidatePageConfig<T> = {
|
|
67
|
+
url?: string;
|
|
68
|
+
testIds?: T extends {
|
|
69
|
+
testIds: infer I;
|
|
70
|
+
} ? I extends Record<string, any> ? ValidateTestIds<I, 'testId'> : TestIdMap : TestIdMap;
|
|
23
71
|
selectors?: T extends {
|
|
24
72
|
testIds: infer I;
|
|
25
73
|
selectors: infer S;
|
|
26
|
-
} ? {
|
|
27
|
-
[K in keyof S]: K extends keyof I ? `Duplicate key: ${K & string} already exists in testIds` : S[K];
|
|
28
|
-
} :
|
|
74
|
+
} ? S extends Record<string, any> ? {
|
|
75
|
+
[K in keyof S]: K extends keyof I ? `Duplicate key: ${K & string} already exists in testIds` : ValidateTestIds<S, 'selector'>[K & keyof S & string];
|
|
76
|
+
} : SelectorMap : SelectorMap;
|
|
29
77
|
Class?: new (page: Page, config?: any) => any;
|
|
78
|
+
} & {
|
|
79
|
+
[K in Exclude<keyof T, keyof PageConfig>]: never;
|
|
30
80
|
};
|
|
31
81
|
/**
|
|
32
82
|
* Creates a strongly-typed page configuration object containing URLs, testIds, and CSS selectors.
|
|
@@ -48,4 +98,4 @@ export type PageConfig<T = any> = {
|
|
|
48
98
|
* });
|
|
49
99
|
* ```
|
|
50
100
|
*/
|
|
51
|
-
export declare function createPageConfig<T extends PageConfig
|
|
101
|
+
export declare function createPageConfig<const T extends PageConfig>(config: [T] extends [ValidatePageConfig<T>] ? T : ValidatePageConfig<T>): T;
|
package/dist/page/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './assertions/verify-helpers';
|
|
|
5
5
|
export * from './assertions/expect';
|
|
6
6
|
export * from './actions/page-actions';
|
|
7
7
|
export * from './actions/locator-actions';
|
|
8
|
+
export * from './locators/dynamic-locator-resolver';
|
|
8
9
|
export * from './locators/resolver';
|
|
9
10
|
export * from './typed-page';
|
|
10
11
|
export * from './registry';
|
package/dist/page/index.js
CHANGED
|
@@ -21,6 +21,7 @@ __exportStar(require("./assertions/verify-helpers"), exports);
|
|
|
21
21
|
__exportStar(require("./assertions/expect"), exports);
|
|
22
22
|
__exportStar(require("./actions/page-actions"), exports);
|
|
23
23
|
__exportStar(require("./actions/locator-actions"), exports);
|
|
24
|
+
__exportStar(require("./locators/dynamic-locator-resolver"), exports);
|
|
24
25
|
__exportStar(require("./locators/resolver"), exports);
|
|
25
26
|
__exportStar(require("./typed-page"), exports);
|
|
26
27
|
__exportStar(require("./registry"), exports);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic locator resolver — expands `{placeholder}` patterns in testId keys
|
|
3
|
+
* into all value combinations at runtime.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const testIds = {
|
|
8
|
+
* staticKey: 'static-test-id',
|
|
9
|
+
* "{status}{id}Chart": {
|
|
10
|
+
* id: ['line', 'bar'],
|
|
11
|
+
* status: ['active', 'inactive'],
|
|
12
|
+
* testId: "status-id-chart"
|
|
13
|
+
* }
|
|
14
|
+
* };
|
|
15
|
+
*
|
|
16
|
+
* expandDynamicLocators(testIds);
|
|
17
|
+
* // => {
|
|
18
|
+
* // staticKey: 'static-test-id',
|
|
19
|
+
* // activeLineChart: 'active-line-chart',
|
|
20
|
+
* // activeBarChart: 'active-bar-chart',
|
|
21
|
+
* // inactiveLineChart: 'inactive-line-chart',
|
|
22
|
+
* // inactiveBarChart: 'inactive-bar-chart'
|
|
23
|
+
* // }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Shape of a dynamic testId entry.
|
|
28
|
+
*
|
|
29
|
+
* `testId` — the test-id pattern with placeholder *names* as literal substrings.
|
|
30
|
+
* Every other property is a placeholder name mapped to its list of allowed values.
|
|
31
|
+
*/
|
|
32
|
+
export type DynamicTestIdEntry = {
|
|
33
|
+
testId?: string;
|
|
34
|
+
[placeholder: string]: string | readonly string[] | undefined;
|
|
35
|
+
};
|
|
36
|
+
export type DynamicSelectorEntry = {
|
|
37
|
+
selector?: string;
|
|
38
|
+
[placeholder: string]: string | readonly string[] | undefined;
|
|
39
|
+
};
|
|
40
|
+
export type DynamicLocatorEntry = DynamicTestIdEntry | DynamicSelectorEntry;
|
|
41
|
+
/** Returns true when `key` contains at least one `{…}` placeholder. */
|
|
42
|
+
export declare function isDynamicKey(key: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Expands all dynamic entries in a `testIds` map into a flat `key → testId` map.
|
|
45
|
+
* Static (string-valued) entries pass through unchanged.
|
|
46
|
+
*/
|
|
47
|
+
export declare function expandDynamicLocators(testIds: Record<string, string | DynamicLocatorEntry>): Record<string, string>;
|
|
48
|
+
/** Return the expanded (flat) testIds map, caching the result per config object. */
|
|
49
|
+
export declare function getExpandedTestIds(testIds: Record<string, string | DynamicLocatorEntry>): Record<string, string>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dynamic locator resolver — expands `{placeholder}` patterns in testId keys
|
|
4
|
+
* into all value combinations at runtime.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const testIds = {
|
|
9
|
+
* staticKey: 'static-test-id',
|
|
10
|
+
* "{status}{id}Chart": {
|
|
11
|
+
* id: ['line', 'bar'],
|
|
12
|
+
* status: ['active', 'inactive'],
|
|
13
|
+
* testId: "status-id-chart"
|
|
14
|
+
* }
|
|
15
|
+
* };
|
|
16
|
+
*
|
|
17
|
+
* expandDynamicLocators(testIds);
|
|
18
|
+
* // => {
|
|
19
|
+
* // staticKey: 'static-test-id',
|
|
20
|
+
* // activeLineChart: 'active-line-chart',
|
|
21
|
+
* // activeBarChart: 'active-bar-chart',
|
|
22
|
+
* // inactiveLineChart: 'inactive-line-chart',
|
|
23
|
+
* // inactiveBarChart: 'inactive-bar-chart'
|
|
24
|
+
* // }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.isDynamicKey = isDynamicKey;
|
|
29
|
+
exports.expandDynamicLocators = expandDynamicLocators;
|
|
30
|
+
exports.getExpandedTestIds = getExpandedTestIds;
|
|
31
|
+
/** Returns true when `key` contains at least one `{…}` placeholder. */
|
|
32
|
+
function isDynamicKey(key) {
|
|
33
|
+
return /\{[^}]+\}/.test(key);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Capitalize the first character of a string.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
function capitalize(value) {
|
|
40
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Compute the cartesian product of an array-of-arrays.
|
|
44
|
+
*
|
|
45
|
+
* @example cartesianProduct([['a','b'],['1','2']]) // [['a','1'],['a','2'],['b','1'],['b','2']]
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
function cartesianProduct(arrays) {
|
|
49
|
+
if (arrays.length === 0)
|
|
50
|
+
return [[]];
|
|
51
|
+
return arrays.reduce((acc, arr) => acc.flatMap(combo => arr.map(val => [...combo, val])), [[]]);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Extract placeholder names from a pattern like `"{status}{id}Chart"`.
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
function extractPlaceholders(pattern) {
|
|
58
|
+
const names = [];
|
|
59
|
+
const re = /\{([^}]+)\}/g;
|
|
60
|
+
let m;
|
|
61
|
+
while ((m = re.exec(pattern)) !== null) {
|
|
62
|
+
names.push(m[1]);
|
|
63
|
+
}
|
|
64
|
+
return names;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Expand a single dynamic key pattern + entry into flat key → locator-value pairs.
|
|
68
|
+
* Validates the entry at runtime before expansion.
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
function expandSingleDynamic(pattern, entry) {
|
|
72
|
+
const testIdPattern = entry.testId;
|
|
73
|
+
const selectorPattern = entry.selector;
|
|
74
|
+
const targetPattern = (testIdPattern !== undefined ? testIdPattern : selectorPattern);
|
|
75
|
+
const targetKey = testIdPattern !== undefined ? 'testId' : 'selector';
|
|
76
|
+
if (targetPattern === undefined) {
|
|
77
|
+
throw new Error(`Dynamic locator "${pattern}": entry must contain either 'testId' or 'selector' property.`);
|
|
78
|
+
}
|
|
79
|
+
const placeholders = { ...entry };
|
|
80
|
+
delete placeholders.testId;
|
|
81
|
+
delete placeholders.selector;
|
|
82
|
+
// ─── Validation ──────────────────────────────────────────────────────
|
|
83
|
+
const expectedNames = extractPlaceholders(pattern);
|
|
84
|
+
// 0.5. No duplicate placeholders in the pattern
|
|
85
|
+
const uniqueNames = Array.from(new Set(expectedNames));
|
|
86
|
+
if (uniqueNames.length !== expectedNames.length) {
|
|
87
|
+
throw new Error(`Dynamic locator "${pattern}": pattern has duplicate placeholders.`);
|
|
88
|
+
}
|
|
89
|
+
// 1. No unknown keys (only placeholder names + targetKey)
|
|
90
|
+
const actualKeys = Object.keys(placeholders);
|
|
91
|
+
const unknownKeys = actualKeys.filter(k => !expectedNames.includes(k));
|
|
92
|
+
if (unknownKeys.length > 0) {
|
|
93
|
+
throw new Error(`Dynamic locator "${pattern}": unknown key(s) [${unknownKeys.join(', ')}]. ` +
|
|
94
|
+
`Only '${targetKey}' and placeholder keys [${expectedNames.join(', ')}] are allowed.`);
|
|
95
|
+
}
|
|
96
|
+
// 2. All placeholders from the pattern must have a key in the entry
|
|
97
|
+
const missingKeys = expectedNames.filter(name => !(name in placeholders));
|
|
98
|
+
if (missingKeys.length > 0) {
|
|
99
|
+
throw new Error(`Dynamic locator "${pattern}": missing placeholder key(s) [${missingKeys.join(', ')}]. ` +
|
|
100
|
+
`You must provide values for all placeholders defined in the pattern.`);
|
|
101
|
+
}
|
|
102
|
+
// 2.5. Placeholder values must be arrays
|
|
103
|
+
const nonArrayKeys = expectedNames.filter(name => !Array.isArray(placeholders[name]));
|
|
104
|
+
if (nonArrayKeys.length > 0) {
|
|
105
|
+
throw new Error(`Dynamic locator "${pattern}": placeholder key(s) [${nonArrayKeys.join(', ')}] must be arrays.`);
|
|
106
|
+
}
|
|
107
|
+
// 3. All placeholder names must appear as substrings in the target pattern value
|
|
108
|
+
const missingInPattern = expectedNames.filter(name => !targetPattern.includes(name));
|
|
109
|
+
if (missingInPattern.length > 0) {
|
|
110
|
+
throw new Error(`Dynamic locator "${pattern}": ${targetKey} "${targetPattern}" does not contain ` +
|
|
111
|
+
`placeholder name(s) [${missingInPattern.join(', ')}]. ` +
|
|
112
|
+
`All placeholder names must appear in the ${targetKey} so they can be replaced at runtime.`);
|
|
113
|
+
}
|
|
114
|
+
// ─── Expansion ───────────────────────────────────────────────────────
|
|
115
|
+
// Sort placeholder names longest-first to avoid substring replacement issues
|
|
116
|
+
const names = Object.keys(placeholders).sort((a, b) => b.length - a.length);
|
|
117
|
+
const values = names.map(name => {
|
|
118
|
+
const v = placeholders[name];
|
|
119
|
+
return v;
|
|
120
|
+
});
|
|
121
|
+
const result = {};
|
|
122
|
+
for (const combo of cartesianProduct(values)) {
|
|
123
|
+
// Build the *key* — replace {name} with Capitalize(value), then uncapitalize the first char
|
|
124
|
+
let expandedKey = pattern;
|
|
125
|
+
let expandedValue = targetPattern;
|
|
126
|
+
for (let i = 0; i < names.length; i++) {
|
|
127
|
+
const name = names[i];
|
|
128
|
+
const val = combo[i];
|
|
129
|
+
expandedKey = expandedKey.replace(`{${name}}`, capitalize(val));
|
|
130
|
+
expandedValue = expandedValue.replaceAll(name, targetKey === 'selector' ? val : val.toLowerCase());
|
|
131
|
+
}
|
|
132
|
+
// camelCase: uncapitalize the first character
|
|
133
|
+
expandedKey = expandedKey.charAt(0).toLowerCase() + expandedKey.slice(1);
|
|
134
|
+
result[expandedKey] = expandedValue;
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Expands all dynamic entries in a `testIds` map into a flat `key → testId` map.
|
|
140
|
+
* Static (string-valued) entries pass through unchanged.
|
|
141
|
+
*/
|
|
142
|
+
function expandDynamicLocators(testIds) {
|
|
143
|
+
const result = {};
|
|
144
|
+
for (const [key, value] of Object.entries(testIds)) {
|
|
145
|
+
if (typeof value === 'string') {
|
|
146
|
+
result[key] = value;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
Object.assign(result, expandSingleDynamic(key, value));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Cached expansion — avoids re-expanding the same config on every action call.
|
|
156
|
+
* @internal
|
|
157
|
+
*/
|
|
158
|
+
const expandedCache = new WeakMap();
|
|
159
|
+
/** Return the expanded (flat) testIds map, caching the result per config object. */
|
|
160
|
+
function getExpandedTestIds(testIds) {
|
|
161
|
+
let expanded = expandedCache.get(testIds);
|
|
162
|
+
if (!expanded) {
|
|
163
|
+
expanded = expandDynamicLocators(testIds);
|
|
164
|
+
expandedCache.set(testIds, expanded);
|
|
165
|
+
}
|
|
166
|
+
return expanded;
|
|
167
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Page, Locator } from '@playwright/test';
|
|
2
|
-
import { ChainedKeys, PageKeys } from '../config';
|
|
2
|
+
import { ChainedKeys, PageKeys, DynamicSelectorEntry } from '../config';
|
|
3
|
+
import type { DynamicLocatorEntry } from './dynamic-locator-resolver';
|
|
3
4
|
export declare function defineLocators<T extends {
|
|
4
|
-
testIds?: Record<string, string>;
|
|
5
|
-
selectors?: Record<string, string>;
|
|
5
|
+
testIds?: Record<string, string | DynamicLocatorEntry>;
|
|
6
|
+
selectors?: Record<string, string | DynamicSelectorEntry>;
|
|
6
7
|
}>(instance: any, context: Page | Locator, config: T): void;
|
|
7
8
|
export declare function resolveLocator<T extends {
|
|
8
|
-
testIds?: Record<string, string>;
|
|
9
|
-
selectors?: Record<string, string>;
|
|
9
|
+
testIds?: Record<string, string | DynamicLocatorEntry>;
|
|
10
|
+
selectors?: Record<string, string | DynamicSelectorEntry>;
|
|
10
11
|
}>(context: Page | Locator, config: T, target: PageKeys<T> | ChainedKeys<T> | Locator, options?: {
|
|
11
12
|
nth?: number;
|
|
12
13
|
raw?: boolean;
|
|
@@ -16,8 +17,8 @@ export declare function resolveLocator<T extends {
|
|
|
16
17
|
* Resolve a config key to a proxied Locator with step-wrapped Playwright methods.
|
|
17
18
|
*/
|
|
18
19
|
export declare function locator<T extends {
|
|
19
|
-
testIds?: Record<string, string>;
|
|
20
|
-
selectors?: Record<string, string>;
|
|
20
|
+
testIds?: Record<string, string | DynamicLocatorEntry>;
|
|
21
|
+
selectors?: Record<string, string | DynamicSelectorEntry>;
|
|
21
22
|
}>(context: Page | Locator, config: T, target: PageKeys<T> | ChainedKeys<T> | Locator, options?: Parameters<Locator['filter']>[0] & {
|
|
22
23
|
nth?: number;
|
|
23
24
|
}): Locator;
|
|
@@ -7,12 +7,15 @@ exports.wrapLocatorWithProxy = wrapLocatorWithProxy;
|
|
|
7
7
|
const test_1 = require("@playwright/test");
|
|
8
8
|
const config_1 = require("../config");
|
|
9
9
|
const formatter_1 = require("../utils/formatter");
|
|
10
|
+
const dynamic_locator_resolver_1 = require("./dynamic-locator-resolver");
|
|
11
|
+
const caller_location_1 = require("../utils/caller-location");
|
|
10
12
|
function defineLocators(instance, context, config) {
|
|
11
13
|
if (config.testIds) {
|
|
12
|
-
|
|
14
|
+
const expanded = (0, dynamic_locator_resolver_1.getExpandedTestIds)(config.testIds);
|
|
15
|
+
for (const key of Object.keys(expanded)) {
|
|
13
16
|
Object.defineProperty(instance, key, {
|
|
14
17
|
get: () => {
|
|
15
|
-
return context.getByTestId(
|
|
18
|
+
return context.getByTestId(expanded[key]);
|
|
16
19
|
},
|
|
17
20
|
enumerable: true,
|
|
18
21
|
configurable: true,
|
|
@@ -20,10 +23,11 @@ function defineLocators(instance, context, config) {
|
|
|
20
23
|
}
|
|
21
24
|
}
|
|
22
25
|
if (config.selectors) {
|
|
23
|
-
|
|
26
|
+
const expanded = (0, dynamic_locator_resolver_1.getExpandedTestIds)(config.selectors);
|
|
27
|
+
for (const key of Object.keys(expanded)) {
|
|
24
28
|
Object.defineProperty(instance, key, {
|
|
25
29
|
get: () => {
|
|
26
|
-
return context.locator(
|
|
30
|
+
return context.locator(expanded[key]);
|
|
27
31
|
},
|
|
28
32
|
enumerable: true,
|
|
29
33
|
configurable: true,
|
|
@@ -36,12 +40,14 @@ function resolveLocator(context, config, target, options) {
|
|
|
36
40
|
return target;
|
|
37
41
|
const targetStr = target;
|
|
38
42
|
const parts = targetStr.split('.');
|
|
43
|
+
const expandedTestIds = config.testIds ? (0, dynamic_locator_resolver_1.getExpandedTestIds)(config.testIds) : undefined;
|
|
44
|
+
const expandedSelectors = config.selectors ? (0, dynamic_locator_resolver_1.getExpandedTestIds)(config.selectors) : undefined;
|
|
39
45
|
const resolveSingle = (key, ctx) => {
|
|
40
|
-
if (
|
|
41
|
-
return ctx.getByTestId(
|
|
46
|
+
if (expandedTestIds && key in expandedTestIds) {
|
|
47
|
+
return ctx.getByTestId(expandedTestIds[key]);
|
|
42
48
|
}
|
|
43
|
-
if (
|
|
44
|
-
return ctx.locator(
|
|
49
|
+
if (expandedSelectors && key in expandedSelectors) {
|
|
50
|
+
return ctx.locator(expandedSelectors[key]);
|
|
45
51
|
}
|
|
46
52
|
throw new Error(`Locator key '${key}' is not defined in testIds or selectors.`);
|
|
47
53
|
};
|
|
@@ -78,7 +84,39 @@ function wrapLocatorWithProxy(loc) {
|
|
|
78
84
|
if (typeof val === 'function' && actionMethods.includes(propKey)) {
|
|
79
85
|
return (...args) => {
|
|
80
86
|
const stepName = (0, formatter_1.formatStepDescription)(propKey, targetLoc, args);
|
|
81
|
-
|
|
87
|
+
if (propKey === 'fill') {
|
|
88
|
+
const options = args[1];
|
|
89
|
+
let shouldMask = false;
|
|
90
|
+
if (options && typeof options === 'object' && options.mask !== undefined) {
|
|
91
|
+
shouldMask = options.mask === true;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const targetStr = (0, formatter_1.formatTarget)(targetLoc);
|
|
95
|
+
const targetStrLower = targetStr.toLowerCase();
|
|
96
|
+
shouldMask = targetStrLower.includes('pass') || targetStrLower.includes('pw');
|
|
97
|
+
}
|
|
98
|
+
if (shouldMask) {
|
|
99
|
+
return test_1.test.step(stepName, async () => {
|
|
100
|
+
await targetLoc.focus();
|
|
101
|
+
await targetLoc.evaluate((el, val) => {
|
|
102
|
+
const inputEl = el;
|
|
103
|
+
const prototype = el.tagName === 'TEXTAREA'
|
|
104
|
+
? window.HTMLTextAreaElement.prototype
|
|
105
|
+
: window.HTMLInputElement.prototype;
|
|
106
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
|
|
107
|
+
if (descriptor && descriptor.set) {
|
|
108
|
+
descriptor.set.call(inputEl, val);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
inputEl.value = val;
|
|
112
|
+
}
|
|
113
|
+
inputEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
114
|
+
inputEl.dispatchEvent(new Event('change', { bubbles: true }));
|
|
115
|
+
}, args[0]);
|
|
116
|
+
}, { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return test_1.test.step(stepName, () => val.apply(targetLoc, args), { box: true, location: (0, caller_location_1.getCallerLocation)() });
|
|
82
120
|
};
|
|
83
121
|
}
|
|
84
122
|
return val;
|
package/dist/page/registry.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import test, { Page, TestType } from '@playwright/test';
|
|
2
|
-
import { PageConfig } from './config';
|
|
2
|
+
import { PageConfig, ValidatePageConfig } from './config';
|
|
3
3
|
import { TypedPage } from './typed-page';
|
|
4
4
|
export type PageRegistry = Record<string, PageConfig>;
|
|
5
|
-
export interface PageConstructor<C extends PageConfig
|
|
5
|
+
export interface PageConstructor<C extends PageConfig> {
|
|
6
6
|
new (page: Page): TypedPage<C>;
|
|
7
7
|
}
|
|
8
|
-
export type PageRegistryTest<T extends Record<string, PageConfig
|
|
8
|
+
export type PageRegistryTest<T extends Record<string, PageConfig>, P, W, O = {}> = TestType<P & {
|
|
9
9
|
[K in keyof T]: K extends keyof O ? O[K] extends new (page: Page, ...args: any[]) => infer R ? R : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => infer R2 ? R2 : TypedPage<T[K]> : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => infer R ? R : TypedPage<T[K]>;
|
|
10
10
|
}, W & {
|
|
11
11
|
[K in keyof T as `worker${Capitalize<K & string>}`]: K extends keyof O ? O[K] extends new (page: Page, ...args: any[]) => infer R ? R : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => infer R2 ? R2 : TypedPage<T[K]> : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => infer R ? R : TypedPage<T[K]>;
|
|
@@ -51,6 +51,8 @@ export type PageRegistryTest<T extends Record<string, PageConfig<any>>, P, W, O
|
|
|
51
51
|
* });
|
|
52
52
|
* ```
|
|
53
53
|
*/
|
|
54
|
-
export declare function createPageRegistry<T extends {
|
|
55
|
-
[K in keyof T]:
|
|
56
|
-
}
|
|
54
|
+
export declare function createPageRegistry<const T extends Record<string, PageConfig>>(registry: [T] extends [{
|
|
55
|
+
[K in keyof T]: ValidatePageConfig<T[K]>;
|
|
56
|
+
}] ? T : {
|
|
57
|
+
[K in keyof T]: ValidatePageConfig<T[K]>;
|
|
58
|
+
}): typeof test extends TestType<infer P, infer W> ? PageRegistryTest<T, P, W> : never;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Page, Locator, expect as playwrightExpect } from '@playwright/test';
|
|
2
|
-
import { TypedLocators, ProxyLocatorMethods, PageKeys, ChainedKeys } from './config';
|
|
2
|
+
import { TypedLocators, ProxyLocatorMethods, PageKeys, ChainedKeys, DynamicSelectorEntry } from './config';
|
|
3
|
+
import type { DynamicLocatorEntry } from './locators/dynamic-locator-resolver';
|
|
3
4
|
import { VerifyFn, AssertionsMethod } from './assertions/verify-chain';
|
|
4
5
|
declare class TypedPageClass<T extends {
|
|
5
|
-
testIds?: Record<string, string>;
|
|
6
|
-
selectors?: Record<string, string>;
|
|
6
|
+
testIds?: Record<string, string | DynamicLocatorEntry>;
|
|
7
|
+
selectors?: Record<string, string | DynamicSelectorEntry>;
|
|
7
8
|
url?: string;
|
|
8
9
|
}> {
|
|
9
10
|
readonly page: Page;
|
|
@@ -58,22 +59,22 @@ declare class TypedPageClass<T extends {
|
|
|
58
59
|
expect(target: PageKeys<T> | ChainedKeys<T> | Locator, message?: string): ReturnType<typeof playwrightExpect<Locator>>;
|
|
59
60
|
}
|
|
60
61
|
export type TypedPageType<T extends {
|
|
61
|
-
testIds?: Record<string, string>;
|
|
62
|
-
selectors?: Record<string, string>;
|
|
62
|
+
testIds?: Record<string, string | DynamicLocatorEntry>;
|
|
63
|
+
selectors?: Record<string, string | DynamicSelectorEntry>;
|
|
63
64
|
url?: string;
|
|
64
65
|
}> = TypedPageClass<T> & TypedLocators<T> & ProxyLocatorMethods<T> & AssertionsMethod<T>;
|
|
65
66
|
export declare const TypedPage: {
|
|
66
67
|
new <T extends {
|
|
67
|
-
testIds?: Record<string, string>;
|
|
68
|
-
selectors?: Record<string, string>;
|
|
68
|
+
testIds?: Record<string, string | DynamicLocatorEntry>;
|
|
69
|
+
selectors?: Record<string, string | DynamicSelectorEntry>;
|
|
69
70
|
url?: string;
|
|
70
71
|
}>(context: Page | Locator, config: T, options?: {
|
|
71
72
|
timeout?: number;
|
|
72
73
|
}): TypedPageType<T>;
|
|
73
74
|
};
|
|
74
75
|
export type TypedPage<T extends {
|
|
75
|
-
testIds?: Record<string, string>;
|
|
76
|
-
selectors?: Record<string, string>;
|
|
76
|
+
testIds?: Record<string, string | DynamicLocatorEntry>;
|
|
77
|
+
selectors?: Record<string, string | DynamicSelectorEntry>;
|
|
77
78
|
url?: string;
|
|
78
79
|
}> = TypedPageType<T>;
|
|
79
80
|
export {};
|
package/dist/page/typed-page.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TypedPage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
4
5
|
const verify_chain_1 = require("./assertions/verify-chain");
|
|
5
6
|
const verify_helpers_1 = require("./assertions/verify-helpers");
|
|
6
7
|
const expect_1 = require("./assertions/expect");
|
|
7
8
|
const page_actions_1 = require("./actions/page-actions");
|
|
8
9
|
const locator_actions_1 = require("./actions/locator-actions");
|
|
9
10
|
const resolver_1 = require("./locators/resolver");
|
|
11
|
+
const caller_location_1 = require("./utils/caller-location");
|
|
10
12
|
class TypedPageClass {
|
|
11
13
|
page;
|
|
12
14
|
context;
|
|
@@ -23,30 +25,76 @@ class TypedPageClass {
|
|
|
23
25
|
this.timeout = options?.timeout;
|
|
24
26
|
(0, resolver_1.defineLocators)(this, this.context, this.config);
|
|
25
27
|
(0, locator_actions_1.defineActionMethods)(this, this.resolveLocator.bind(this), this.timeout);
|
|
28
|
+
// Auto-wrap subclass methods with Playwright test.step
|
|
29
|
+
const builtInMethods = new Set([
|
|
30
|
+
'constructor', 'goto', 'verifyURL', 'verifyTitle', 'reload',
|
|
31
|
+
'waitForLoadState', 'waitForURL', 'resolveLocator', 'locator',
|
|
32
|
+
'verify', 'verifyHidden', 'verifyEnabled', 'verifyDisabled', 'expect'
|
|
33
|
+
]);
|
|
34
|
+
let proto = Object.getPrototypeOf(this);
|
|
35
|
+
while (proto && proto !== Object.prototype && proto !== TypedPageClass.prototype) {
|
|
36
|
+
const propNames = Object.getOwnPropertyNames(proto);
|
|
37
|
+
for (const prop of propNames) {
|
|
38
|
+
const desc = Object.getOwnPropertyDescriptor(proto, prop);
|
|
39
|
+
if (desc && typeof desc.value === 'function' && !builtInMethods.has(prop) && !prop.startsWith('_')) {
|
|
40
|
+
const originalFn = desc.value;
|
|
41
|
+
Object.defineProperty(this, prop, {
|
|
42
|
+
value: function (...args) {
|
|
43
|
+
const words = prop.split(/(?=[A-Z])/).map(w => w.toLowerCase());
|
|
44
|
+
if (words.length > 0) {
|
|
45
|
+
words[0] = words[0].charAt(0).toUpperCase() + words[0].slice(1);
|
|
46
|
+
}
|
|
47
|
+
const sentence = words.join(' ');
|
|
48
|
+
const formattedArgs = args.map(arg => {
|
|
49
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.stringify(arg);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return String(arg);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return String(arg);
|
|
58
|
+
});
|
|
59
|
+
const stepName = formattedArgs.length > 0
|
|
60
|
+
? `${sentence} - ${formattedArgs.join(' - ')}`
|
|
61
|
+
: sentence;
|
|
62
|
+
return test_1.test.step(stepName, () => {
|
|
63
|
+
return originalFn.apply(this, args);
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
writable: true,
|
|
67
|
+
configurable: true,
|
|
68
|
+
enumerable: true
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
proto = Object.getPrototypeOf(proto);
|
|
73
|
+
}
|
|
26
74
|
}
|
|
27
75
|
/** Navigate to the URL from page config. */
|
|
28
76
|
async goto(options) {
|
|
29
|
-
return (0, page_actions_1.goto)(this.page, this.url, this.constructor.name, options);
|
|
77
|
+
return (0, page_actions_1.goto)(this.page, this.url, this.constructor.name, options, (0, caller_location_1.getCallerLocation)());
|
|
30
78
|
}
|
|
31
79
|
/** Assert the current URL matches the page config URL or a custom pattern. */
|
|
32
80
|
async verifyURL(urlOrOptions, options) {
|
|
33
|
-
return (0, page_actions_1.verifyURL)(this.page, this.url, this.constructor.name, urlOrOptions, options);
|
|
81
|
+
return (0, page_actions_1.verifyURL)(this.page, this.url, this.constructor.name, urlOrOptions, options, (0, caller_location_1.getCallerLocation)());
|
|
34
82
|
}
|
|
35
83
|
/** Assert the page title matches the expected value. */
|
|
36
84
|
async verifyTitle(title, options) {
|
|
37
|
-
return (0, page_actions_1.verifyTitle)(this.page, title, options);
|
|
85
|
+
return (0, page_actions_1.verifyTitle)(this.page, title, options, (0, caller_location_1.getCallerLocation)());
|
|
38
86
|
}
|
|
39
87
|
/** Reload the current page. */
|
|
40
88
|
async reload(options) {
|
|
41
|
-
return (0, page_actions_1.reload)(this.page, options);
|
|
89
|
+
return (0, page_actions_1.reload)(this.page, options, (0, caller_location_1.getCallerLocation)());
|
|
42
90
|
}
|
|
43
91
|
/** Wait for the page to reach a load state. */
|
|
44
92
|
async waitForLoadState(state, options) {
|
|
45
|
-
return (0, page_actions_1.waitForLoadState)(this.page, state, options);
|
|
93
|
+
return (0, page_actions_1.waitForLoadState)(this.page, state, options, (0, caller_location_1.getCallerLocation)());
|
|
46
94
|
}
|
|
47
95
|
/** Wait for navigation to a URL. Defaults to the page config URL. */
|
|
48
96
|
async waitForURL(urlOrOptions, options) {
|
|
49
|
-
return (0, page_actions_1.waitForURL)(this.page, this.url, this.constructor.name, urlOrOptions, options);
|
|
97
|
+
return (0, page_actions_1.waitForURL)(this.page, this.url, this.constructor.name, urlOrOptions, options, (0, caller_location_1.getCallerLocation)());
|
|
50
98
|
}
|
|
51
99
|
resolveLocator(target, options) {
|
|
52
100
|
return (0, resolver_1.resolveLocator)(this.context, this.config, target, options);
|
|
@@ -62,13 +110,13 @@ class TypedPageClass {
|
|
|
62
110
|
return fn;
|
|
63
111
|
}
|
|
64
112
|
async verifyHidden(target, options) {
|
|
65
|
-
return (0, verify_helpers_1.verifyHidden)(this.resolveLocator.bind(this), target, options);
|
|
113
|
+
return (0, verify_helpers_1.verifyHidden)(this.resolveLocator.bind(this), target, options, (0, caller_location_1.getCallerLocation)());
|
|
66
114
|
}
|
|
67
115
|
async verifyEnabled(target, options) {
|
|
68
|
-
return (0, verify_helpers_1.verifyEnabled)(this.resolveLocator.bind(this), target, options);
|
|
116
|
+
return (0, verify_helpers_1.verifyEnabled)(this.resolveLocator.bind(this), target, options, (0, caller_location_1.getCallerLocation)());
|
|
69
117
|
}
|
|
70
118
|
async verifyDisabled(target, options) {
|
|
71
|
-
return (0, verify_helpers_1.verifyDisabled)(this.resolveLocator.bind(this), target, options);
|
|
119
|
+
return (0, verify_helpers_1.verifyDisabled)(this.resolveLocator.bind(this), target, options, (0, caller_location_1.getCallerLocation)());
|
|
72
120
|
}
|
|
73
121
|
expect(target, message) {
|
|
74
122
|
const resolved = typeof target !== 'string' ? target : this.resolveLocator(target);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts placeholder names from a `{name}` pattern string.
|
|
3
|
+
*/
|
|
4
|
+
export type ExtractPlaceholders<S extends string> = S extends `${string}{${infer Name}}${infer Rest}` ? Name | ExtractPlaceholders<Rest> : never;
|
|
5
|
+
/**
|
|
6
|
+
* Checks recursively if a pattern string contains duplicate placeholders.
|
|
7
|
+
*/
|
|
8
|
+
export type HasDuplicatePlaceholders<S extends string, Seen extends string = never> = S extends `${string}{${infer Name}}${infer Rest}` ? Name extends Seen ? true : HasDuplicatePlaceholders<Rest, Seen | Name> : false;
|
|
9
|
+
/**
|
|
10
|
+
* Returns the subset of `Placeholders` whose names are NOT found as
|
|
11
|
+
* substrings of `TestId`. Evaluates to `never` when all are present.
|
|
12
|
+
*/
|
|
13
|
+
export type MissingInTestId<TestId extends string, Placeholders extends string> = Placeholders extends any ? TestId extends `${string}${Placeholders}${string}` ? never : Placeholders : never;
|
|
14
|
+
/**
|
|
15
|
+
* Validates a dynamic entry by mapping each of its properties to their expected types.
|
|
16
|
+
*
|
|
17
|
+
* By checking properties individually, TypeScript reports the error exactly on the offending line
|
|
18
|
+
* (e.g. on the wrong value or unknown key) instead of marking the entire object as invalid.
|
|
19
|
+
*/
|
|
20
|
+
export type ValidateDynamicEntryProperties<Pattern extends string, Entry, TargetKey extends 'testId' | 'selector'> = {
|
|
21
|
+
[Key in (keyof Entry | ExtractPlaceholders<Pattern> | TargetKey)]: Key extends TargetKey ? Entry extends Record<TargetKey, infer TPat> ? TPat extends string ? [MissingInTestId<TPat, ExtractPlaceholders<Pattern>>] extends [never] ? string : `Error: ${TargetKey} must contain all placeholder names` : `Error: ${TargetKey} must be a string` : `Error: Dynamic entry must include a ${TargetKey} property` : Key extends ExtractPlaceholders<Pattern> ? Key extends keyof Entry ? Entry[Key] extends readonly string[] ? readonly string[] : `Error: Placeholder key must be an array: ${Key & string}` : `Error: Missing placeholder key: ${Key & string}` : `Error: Only placeholder keys and '${TargetKey}' are allowed`;
|
|
22
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCallerLocation = getCallerLocation;
|
|
4
|
+
// Global stack trace filter to remove pw-core internal frames from all error stacks.
|
|
5
|
+
// This ensures that Playwright's auto-generated steps and expectations are attributed
|
|
6
|
+
// directly to the user's project files instead of showing internal pw-core library paths.
|
|
7
|
+
const originalPrepare = Error.prepareStackTrace;
|
|
8
|
+
Error.prepareStackTrace = (err, stack) => {
|
|
9
|
+
const filteredStack = stack.filter(frame => {
|
|
10
|
+
const file = frame.getFileName();
|
|
11
|
+
if (!file)
|
|
12
|
+
return true;
|
|
13
|
+
const normalized = file.replace(/\\/g, '/');
|
|
14
|
+
if (normalized.includes('pw-core/src') ||
|
|
15
|
+
normalized.includes('pw-core/dist') ||
|
|
16
|
+
normalized.includes('node_modules/pw-core')) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
});
|
|
21
|
+
if (originalPrepare) {
|
|
22
|
+
try {
|
|
23
|
+
return originalPrepare(err, filteredStack);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Fallback if originalPrepare fails on filtered stack
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const errString = err.toString ? err.toString() : String(err);
|
|
30
|
+
return errString + '\n' + filteredStack.map(frame => {
|
|
31
|
+
try {
|
|
32
|
+
return ' at ' + frame.toString();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return ' at <unknown>';
|
|
36
|
+
}
|
|
37
|
+
}).join('\n');
|
|
38
|
+
};
|
|
39
|
+
function getCallerLocation() {
|
|
40
|
+
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
41
|
+
try {
|
|
42
|
+
const err = {};
|
|
43
|
+
Error.prepareStackTrace = (_err, stack) => stack;
|
|
44
|
+
Error.captureStackTrace(err);
|
|
45
|
+
const stack = err.stack;
|
|
46
|
+
if (!stack)
|
|
47
|
+
return undefined;
|
|
48
|
+
for (let i = 1; i < stack.length; i++) {
|
|
49
|
+
const frame = stack[i];
|
|
50
|
+
const file = frame.getFileName();
|
|
51
|
+
if (!file)
|
|
52
|
+
continue;
|
|
53
|
+
const normalizedFile = file.replace(/\\/g, '/');
|
|
54
|
+
// Skip internal node_modules, node built-ins, and pw-core source/dist files
|
|
55
|
+
if (normalizedFile.includes('node_modules') ||
|
|
56
|
+
normalizedFile.includes('pw-core/src') ||
|
|
57
|
+
normalizedFile.includes('pw-core/dist') ||
|
|
58
|
+
normalizedFile.includes('playwright/lib') ||
|
|
59
|
+
normalizedFile.includes('@playwright') ||
|
|
60
|
+
normalizedFile.startsWith('node:') ||
|
|
61
|
+
!normalizedFile.includes('/')) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const line = frame.getLineNumber();
|
|
65
|
+
const column = frame.getColumnNumber();
|
|
66
|
+
if (!line)
|
|
67
|
+
continue;
|
|
68
|
+
return { file, line, column: column ?? 1 };
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -15,7 +15,14 @@ function formatStepDescription(methodName, target, args) {
|
|
|
15
15
|
const targetStr = formatTarget(target);
|
|
16
16
|
if (methodName === 'fill' && args[0] !== undefined) {
|
|
17
17
|
const options = args[1];
|
|
18
|
-
|
|
18
|
+
let shouldMask = false;
|
|
19
|
+
if (options && typeof options === 'object' && options.mask !== undefined) {
|
|
20
|
+
shouldMask = options.mask === true;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const targetStrLower = targetStr.toLowerCase();
|
|
24
|
+
shouldMask = targetStrLower.includes('pass') || targetStrLower.includes('pw');
|
|
25
|
+
}
|
|
19
26
|
const displayValue = shouldMask ? '*'.repeat(String(args[0]).length) : args[0];
|
|
20
27
|
return `Fill "${targetStr}" with "${displayValue}"`;
|
|
21
28
|
}
|