browser-commander 0.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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/release.yml +296 -0
- package/.husky/pre-commit +1 -0
- package/.jscpd.json +20 -0
- package/.prettierignore +7 -0
- package/.prettierrc +10 -0
- package/CHANGELOG.md +32 -0
- package/LICENSE +24 -0
- package/README.md +320 -0
- package/bunfig.toml +3 -0
- package/deno.json +7 -0
- package/eslint.config.js +125 -0
- package/examples/react-test-app/index.html +25 -0
- package/examples/react-test-app/package.json +19 -0
- package/examples/react-test-app/src/App.jsx +473 -0
- package/examples/react-test-app/src/main.jsx +10 -0
- package/examples/react-test-app/src/styles.css +323 -0
- package/examples/react-test-app/vite.config.js +9 -0
- package/package.json +89 -0
- package/scripts/changeset-version.mjs +38 -0
- package/scripts/create-github-release.mjs +93 -0
- package/scripts/create-manual-changeset.mjs +86 -0
- package/scripts/format-github-release.mjs +83 -0
- package/scripts/format-release-notes.mjs +216 -0
- package/scripts/instant-version-bump.mjs +121 -0
- package/scripts/merge-changesets.mjs +260 -0
- package/scripts/publish-to-npm.mjs +126 -0
- package/scripts/setup-npm.mjs +37 -0
- package/scripts/validate-changeset.mjs +262 -0
- package/scripts/version-and-commit.mjs +237 -0
- package/src/ARCHITECTURE.md +270 -0
- package/src/README.md +517 -0
- package/src/bindings.js +298 -0
- package/src/browser/launcher.js +93 -0
- package/src/browser/navigation.js +513 -0
- package/src/core/constants.js +24 -0
- package/src/core/engine-adapter.js +466 -0
- package/src/core/engine-detection.js +49 -0
- package/src/core/logger.js +21 -0
- package/src/core/navigation-manager.js +503 -0
- package/src/core/navigation-safety.js +160 -0
- package/src/core/network-tracker.js +373 -0
- package/src/core/page-session.js +299 -0
- package/src/core/page-trigger-manager.js +564 -0
- package/src/core/preferences.js +46 -0
- package/src/elements/content.js +197 -0
- package/src/elements/locators.js +243 -0
- package/src/elements/selectors.js +360 -0
- package/src/elements/visibility.js +166 -0
- package/src/exports.js +121 -0
- package/src/factory.js +192 -0
- package/src/high-level/universal-logic.js +206 -0
- package/src/index.js +17 -0
- package/src/interactions/click.js +684 -0
- package/src/interactions/fill.js +383 -0
- package/src/interactions/scroll.js +341 -0
- package/src/utilities/url.js +33 -0
- package/src/utilities/wait.js +135 -0
- package/tests/e2e/playwright.e2e.test.js +442 -0
- package/tests/e2e/puppeteer.e2e.test.js +408 -0
- package/tests/helpers/mocks.js +542 -0
- package/tests/unit/bindings.test.js +218 -0
- package/tests/unit/browser/navigation.test.js +345 -0
- package/tests/unit/core/constants.test.js +72 -0
- package/tests/unit/core/engine-adapter.test.js +170 -0
- package/tests/unit/core/engine-detection.test.js +81 -0
- package/tests/unit/core/logger.test.js +80 -0
- package/tests/unit/core/navigation-safety.test.js +202 -0
- package/tests/unit/core/network-tracker.test.js +198 -0
- package/tests/unit/core/page-trigger-manager.test.js +358 -0
- package/tests/unit/elements/content.test.js +318 -0
- package/tests/unit/elements/locators.test.js +236 -0
- package/tests/unit/elements/selectors.test.js +302 -0
- package/tests/unit/elements/visibility.test.js +234 -0
- package/tests/unit/factory.test.js +174 -0
- package/tests/unit/high-level/universal-logic.test.js +299 -0
- package/tests/unit/interactions/click.test.js +340 -0
- package/tests/unit/interactions/fill.test.js +378 -0
- package/tests/unit/interactions/scroll.test.js +330 -0
- package/tests/unit/utilities/url.test.js +63 -0
- package/tests/unit/utilities/wait.test.js +207 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { isNavigationError } from '../core/navigation-safety.js';
|
|
2
|
+
import { getLocatorOrElement } from './locators.js';
|
|
3
|
+
import { createEngineAdapter } from '../core/engine-adapter.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get text content
|
|
7
|
+
* @param {Object} options - Configuration options
|
|
8
|
+
* @param {Object} options.page - Browser page object
|
|
9
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
10
|
+
* @param {string|Object} options.selector - CSS selector or element
|
|
11
|
+
* @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
|
|
12
|
+
* @returns {Promise<string|null>} - Text content or null
|
|
13
|
+
*/
|
|
14
|
+
export async function textContent(options = {}) {
|
|
15
|
+
const { page, engine, selector, adapter: providedAdapter } = options;
|
|
16
|
+
|
|
17
|
+
if (!selector) {
|
|
18
|
+
throw new Error('selector is required in options');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const adapter = providedAdapter || createEngineAdapter(page, engine);
|
|
23
|
+
const locatorOrElement = await getLocatorOrElement({
|
|
24
|
+
page,
|
|
25
|
+
engine,
|
|
26
|
+
selector,
|
|
27
|
+
});
|
|
28
|
+
if (!locatorOrElement) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return await adapter.getTextContent(locatorOrElement);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (isNavigationError(error)) {
|
|
34
|
+
console.log('⚠️ Navigation detected during textContent, returning null');
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get input value
|
|
43
|
+
* @param {Object} options - Configuration options
|
|
44
|
+
* @param {Object} options.page - Browser page object
|
|
45
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
46
|
+
* @param {string|Object} options.selector - CSS selector or element
|
|
47
|
+
* @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
|
|
48
|
+
* @returns {Promise<string>} - Input value
|
|
49
|
+
*/
|
|
50
|
+
export async function inputValue(options = {}) {
|
|
51
|
+
const { page, engine, selector, adapter: providedAdapter } = options;
|
|
52
|
+
|
|
53
|
+
if (!selector) {
|
|
54
|
+
throw new Error('selector is required in options');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const adapter = providedAdapter || createEngineAdapter(page, engine);
|
|
59
|
+
const locatorOrElement = await getLocatorOrElement({
|
|
60
|
+
page,
|
|
61
|
+
engine,
|
|
62
|
+
selector,
|
|
63
|
+
});
|
|
64
|
+
if (!locatorOrElement) {
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
return await adapter.getInputValue(locatorOrElement);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (isNavigationError(error)) {
|
|
70
|
+
console.log(
|
|
71
|
+
'⚠️ Navigation detected during inputValue, returning empty string'
|
|
72
|
+
);
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get element attribute
|
|
81
|
+
* @param {Object} options - Configuration options
|
|
82
|
+
* @param {Object} options.page - Browser page object
|
|
83
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
84
|
+
* @param {string|Object} options.selector - CSS selector or element
|
|
85
|
+
* @param {string} options.attribute - Attribute name
|
|
86
|
+
* @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
|
|
87
|
+
* @returns {Promise<string|null>} - Attribute value or null
|
|
88
|
+
*/
|
|
89
|
+
export async function getAttribute(options = {}) {
|
|
90
|
+
const {
|
|
91
|
+
page,
|
|
92
|
+
engine,
|
|
93
|
+
selector,
|
|
94
|
+
attribute,
|
|
95
|
+
adapter: providedAdapter,
|
|
96
|
+
} = options;
|
|
97
|
+
|
|
98
|
+
if (!selector || !attribute) {
|
|
99
|
+
throw new Error('selector and attribute are required in options');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const adapter = providedAdapter || createEngineAdapter(page, engine);
|
|
104
|
+
const locatorOrElement = await getLocatorOrElement({
|
|
105
|
+
page,
|
|
106
|
+
engine,
|
|
107
|
+
selector,
|
|
108
|
+
});
|
|
109
|
+
if (!locatorOrElement) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return await adapter.getAttribute(locatorOrElement, attribute);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (isNavigationError(error)) {
|
|
115
|
+
console.log(
|
|
116
|
+
'⚠️ Navigation detected during getAttribute, returning null'
|
|
117
|
+
);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get input value from element (helper)
|
|
126
|
+
* @param {Object} options - Configuration options
|
|
127
|
+
* @param {Object} options.page - Browser page object
|
|
128
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
129
|
+
* @param {Object} options.locatorOrElement - Element or locator
|
|
130
|
+
* @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
|
|
131
|
+
* @returns {Promise<string>}
|
|
132
|
+
*/
|
|
133
|
+
export async function getInputValue(options = {}) {
|
|
134
|
+
const { page, engine, locatorOrElement, adapter: providedAdapter } = options;
|
|
135
|
+
|
|
136
|
+
if (!locatorOrElement) {
|
|
137
|
+
throw new Error('locatorOrElement is required in options');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const adapter = providedAdapter || createEngineAdapter(page, engine);
|
|
142
|
+
return await adapter.getInputValue(locatorOrElement);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (isNavigationError(error)) {
|
|
145
|
+
console.log(
|
|
146
|
+
'⚠️ Navigation detected during getInputValue, returning empty string'
|
|
147
|
+
);
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Log element information for verbose debugging
|
|
156
|
+
* @param {Object} options - Configuration options
|
|
157
|
+
* @param {Object} options.page - Browser page object
|
|
158
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
159
|
+
* @param {Function} options.log - Logger instance
|
|
160
|
+
* @param {Object} options.locatorOrElement - Element or locator to log
|
|
161
|
+
* @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
|
|
162
|
+
* @returns {Promise<void>}
|
|
163
|
+
*/
|
|
164
|
+
export async function logElementInfo(options = {}) {
|
|
165
|
+
const {
|
|
166
|
+
page,
|
|
167
|
+
engine,
|
|
168
|
+
log,
|
|
169
|
+
locatorOrElement,
|
|
170
|
+
adapter: providedAdapter,
|
|
171
|
+
} = options;
|
|
172
|
+
|
|
173
|
+
if (!locatorOrElement) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const adapter = providedAdapter || createEngineAdapter(page, engine);
|
|
179
|
+
const tagName = await adapter.evaluateOnElement(
|
|
180
|
+
locatorOrElement,
|
|
181
|
+
(el) => el.tagName
|
|
182
|
+
);
|
|
183
|
+
const text = await adapter.getTextContent(locatorOrElement);
|
|
184
|
+
log.debug(
|
|
185
|
+
() =>
|
|
186
|
+
`🔍 [VERBOSE] Target element: ${tagName}: "${text?.trim().substring(0, 30)}..."`
|
|
187
|
+
);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (isNavigationError(error)) {
|
|
190
|
+
log.debug(
|
|
191
|
+
() => '⚠️ Navigation detected during logElementInfo, skipping'
|
|
192
|
+
);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { TIMING } from '../core/constants.js';
|
|
2
|
+
import { isNavigationError } from '../core/navigation-safety.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Helper to create Playwright locator from selector string
|
|
6
|
+
* Handles :nth-of-type() pseudo-selectors which don't work in Playwright locators
|
|
7
|
+
* @param {Object} options - Configuration options
|
|
8
|
+
* @param {Object} options.page - Browser page object
|
|
9
|
+
* @param {string} options.selector - CSS selector
|
|
10
|
+
* @returns {Object} - Playwright locator
|
|
11
|
+
*/
|
|
12
|
+
export function createPlaywrightLocator(options = {}) {
|
|
13
|
+
const { page, selector } = options;
|
|
14
|
+
|
|
15
|
+
if (!selector) {
|
|
16
|
+
throw new Error('selector is required in options');
|
|
17
|
+
}
|
|
18
|
+
// Check if selector has :nth-of-type(n) pattern
|
|
19
|
+
const nthOfTypeMatch = selector.match(/^(.+):nth-of-type\((\d+)\)$/);
|
|
20
|
+
|
|
21
|
+
if (nthOfTypeMatch) {
|
|
22
|
+
const baseSelector = nthOfTypeMatch[1];
|
|
23
|
+
const index = parseInt(nthOfTypeMatch[2], 10) - 1; // Convert to 0-based index
|
|
24
|
+
return page.locator(baseSelector).nth(index);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return page.locator(selector);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get locator/element from selector (unified helper for both engines)
|
|
32
|
+
* Does NOT wait - use waitForLocatorOrElement() if you need to wait
|
|
33
|
+
* @param {Object} options - Configuration options
|
|
34
|
+
* @param {Object} options.page - Browser page object
|
|
35
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
36
|
+
* @param {string|Object} options.selector - CSS selector or element/locator
|
|
37
|
+
* @returns {Promise<Object|null>} - Locator for Playwright, Element for Puppeteer (can be null)
|
|
38
|
+
*/
|
|
39
|
+
export async function getLocatorOrElement(options = {}) {
|
|
40
|
+
const { page, engine, selector } = options;
|
|
41
|
+
|
|
42
|
+
if (!selector) {
|
|
43
|
+
throw new Error('selector is required in options');
|
|
44
|
+
}
|
|
45
|
+
if (typeof selector !== 'string') {
|
|
46
|
+
return selector; // Already a locator/element
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (engine === 'playwright') {
|
|
50
|
+
return createPlaywrightLocator({ page, selector });
|
|
51
|
+
} else {
|
|
52
|
+
// For Puppeteer, return element (can be null if doesn't exist)
|
|
53
|
+
return await page.$(selector);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get locator/element and wait for it to be visible
|
|
59
|
+
* Unified waiting behavior for both engines
|
|
60
|
+
* @param {Object} options - Configuration options
|
|
61
|
+
* @param {Object} options.page - Browser page object
|
|
62
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
63
|
+
* @param {string|Object} options.selector - CSS selector or existing locator/element
|
|
64
|
+
* @param {number} options.timeout - Timeout in ms (default: TIMING.DEFAULT_TIMEOUT)
|
|
65
|
+
* @param {boolean} options.throwOnNavigation - Whether to throw on navigation error (default: true)
|
|
66
|
+
* @returns {Promise<Object|null>} - Locator for Playwright (first match), Element for Puppeteer, or null on navigation
|
|
67
|
+
* @throws {Error} - If element not found or not visible within timeout (unless navigation error and throwOnNavigation is false)
|
|
68
|
+
*/
|
|
69
|
+
export async function waitForLocatorOrElement(options = {}) {
|
|
70
|
+
const {
|
|
71
|
+
page,
|
|
72
|
+
engine,
|
|
73
|
+
selector,
|
|
74
|
+
timeout = TIMING.DEFAULT_TIMEOUT,
|
|
75
|
+
throwOnNavigation = true,
|
|
76
|
+
} = options;
|
|
77
|
+
|
|
78
|
+
if (!selector) {
|
|
79
|
+
throw new Error('selector is required in options');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
if (engine === 'playwright') {
|
|
84
|
+
const locator = await getLocatorOrElement({ page, engine, selector });
|
|
85
|
+
// Use .first() to handle multiple matches (Playwright strict mode)
|
|
86
|
+
const firstLocator = locator.first();
|
|
87
|
+
await firstLocator.waitFor({ state: 'visible', timeout });
|
|
88
|
+
return firstLocator;
|
|
89
|
+
} else {
|
|
90
|
+
// Puppeteer: wait for selector to be visible (returns first match by default)
|
|
91
|
+
await page.waitForSelector(selector, { visible: true, timeout });
|
|
92
|
+
const element = await page.$(selector);
|
|
93
|
+
if (!element) {
|
|
94
|
+
throw new Error(`Element not found after waiting: ${selector}`);
|
|
95
|
+
}
|
|
96
|
+
return element;
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (isNavigationError(error)) {
|
|
100
|
+
console.log(
|
|
101
|
+
'⚠️ Navigation detected during waitForLocatorOrElement, recovering gracefully'
|
|
102
|
+
);
|
|
103
|
+
if (throwOnNavigation) {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Wait for element to be visible (works with existing locatorOrElement)
|
|
114
|
+
* @param {Object} options - Configuration options
|
|
115
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
116
|
+
* @param {Object} options.locatorOrElement - Element or locator to wait for
|
|
117
|
+
* @param {number} options.timeout - Timeout in ms (default: TIMING.DEFAULT_TIMEOUT)
|
|
118
|
+
* @returns {Promise<void>}
|
|
119
|
+
*/
|
|
120
|
+
export async function waitForVisible(options = {}) {
|
|
121
|
+
const {
|
|
122
|
+
engine,
|
|
123
|
+
locatorOrElement,
|
|
124
|
+
timeout = TIMING.DEFAULT_TIMEOUT,
|
|
125
|
+
} = options;
|
|
126
|
+
|
|
127
|
+
if (!locatorOrElement) {
|
|
128
|
+
throw new Error('locatorOrElement is required in options');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (engine === 'playwright') {
|
|
132
|
+
await locatorOrElement.waitFor({ state: 'visible', timeout });
|
|
133
|
+
} else {
|
|
134
|
+
// For Puppeteer, element is already fetched, just verify it exists
|
|
135
|
+
if (!locatorOrElement) {
|
|
136
|
+
throw new Error('Element not found');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create locator (Playwright-style fluent API)
|
|
143
|
+
* @param {Object} options - Configuration options
|
|
144
|
+
* @param {Object} options.page - Browser page object
|
|
145
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
146
|
+
* @param {string} options.selector - CSS selector
|
|
147
|
+
* @returns {Object} - Locator object (Playwright) or wrapper (Puppeteer)
|
|
148
|
+
*/
|
|
149
|
+
export function locator(options = {}) {
|
|
150
|
+
const { page, engine, selector } = options;
|
|
151
|
+
|
|
152
|
+
if (!selector) {
|
|
153
|
+
throw new Error('selector is required in options');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (engine === 'playwright') {
|
|
157
|
+
return createPlaywrightLocator({ page, selector });
|
|
158
|
+
} else {
|
|
159
|
+
// Return a wrapper that mimics Playwright locator API for Puppeteer
|
|
160
|
+
const createLocatorWrapper = (sel) => ({
|
|
161
|
+
selector: sel,
|
|
162
|
+
async count() {
|
|
163
|
+
const elements = await page.$$(sel);
|
|
164
|
+
return elements.length;
|
|
165
|
+
},
|
|
166
|
+
async click(options = {}) {
|
|
167
|
+
await page.click(sel, options);
|
|
168
|
+
},
|
|
169
|
+
async fill(text) {
|
|
170
|
+
await page.$eval(
|
|
171
|
+
sel,
|
|
172
|
+
(el, value) => {
|
|
173
|
+
el.value = value;
|
|
174
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
175
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
176
|
+
},
|
|
177
|
+
text
|
|
178
|
+
);
|
|
179
|
+
},
|
|
180
|
+
async type(text, options = {}) {
|
|
181
|
+
await page.type(sel, text, options);
|
|
182
|
+
},
|
|
183
|
+
async textContent() {
|
|
184
|
+
const element = await page.$(sel);
|
|
185
|
+
if (!element) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
return await page.evaluate((el) => el.textContent, element);
|
|
189
|
+
},
|
|
190
|
+
async inputValue() {
|
|
191
|
+
const element = await page.$(sel);
|
|
192
|
+
if (!element) {
|
|
193
|
+
return '';
|
|
194
|
+
}
|
|
195
|
+
return await page.evaluate((el) => el.value, element);
|
|
196
|
+
},
|
|
197
|
+
async getAttribute(name) {
|
|
198
|
+
const element = await page.$(sel);
|
|
199
|
+
if (!element) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
return await page.evaluate(
|
|
203
|
+
(el, attr) => el.getAttribute(attr),
|
|
204
|
+
element,
|
|
205
|
+
name
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
async isVisible() {
|
|
209
|
+
const element = await page.$(sel);
|
|
210
|
+
if (!element) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
return await page.evaluate(
|
|
214
|
+
(el) => el.offsetWidth > 0 && el.offsetHeight > 0,
|
|
215
|
+
element
|
|
216
|
+
);
|
|
217
|
+
},
|
|
218
|
+
async waitFor(options = {}) {
|
|
219
|
+
const { state = 'visible', timeout = TIMING.DEFAULT_TIMEOUT } = options;
|
|
220
|
+
const visible = state === 'visible';
|
|
221
|
+
await page.waitForSelector(sel, { visible, timeout });
|
|
222
|
+
},
|
|
223
|
+
nth(index) {
|
|
224
|
+
return createLocatorWrapper(`${sel}:nth-of-type(${index + 1})`);
|
|
225
|
+
},
|
|
226
|
+
first() {
|
|
227
|
+
return createLocatorWrapper(`${sel}:nth-of-type(1)`);
|
|
228
|
+
},
|
|
229
|
+
last() {
|
|
230
|
+
return createLocatorWrapper(`${sel}:last-of-type`);
|
|
231
|
+
},
|
|
232
|
+
async evaluate(fn, arg) {
|
|
233
|
+
const element = await page.$(sel);
|
|
234
|
+
if (!element) {
|
|
235
|
+
throw new Error(`Element not found: ${sel}`);
|
|
236
|
+
}
|
|
237
|
+
return await page.evaluate(fn, element, arg);
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return createLocatorWrapper(selector);
|
|
242
|
+
}
|
|
243
|
+
}
|