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
package/src/bindings.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Commander - Function Binding Helpers
|
|
3
|
+
* This module provides helper functions for binding page, engine, and log to library functions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { wait, evaluate, safeEvaluate } from './utilities/wait.js';
|
|
7
|
+
import { getUrl, unfocusAddressBar } from './utilities/url.js';
|
|
8
|
+
import {
|
|
9
|
+
waitForUrlStabilization,
|
|
10
|
+
goto,
|
|
11
|
+
waitForNavigation,
|
|
12
|
+
waitForPageReady,
|
|
13
|
+
waitAfterAction,
|
|
14
|
+
} from './browser/navigation.js';
|
|
15
|
+
import {
|
|
16
|
+
createPlaywrightLocator,
|
|
17
|
+
getLocatorOrElement,
|
|
18
|
+
waitForLocatorOrElement,
|
|
19
|
+
waitForVisible,
|
|
20
|
+
locator,
|
|
21
|
+
} from './elements/locators.js';
|
|
22
|
+
import {
|
|
23
|
+
querySelector,
|
|
24
|
+
querySelectorAll,
|
|
25
|
+
findByText,
|
|
26
|
+
normalizeSelector,
|
|
27
|
+
waitForSelector,
|
|
28
|
+
withTextSelectorSupport,
|
|
29
|
+
} from './elements/selectors.js';
|
|
30
|
+
import { isVisible, isEnabled, count } from './elements/visibility.js';
|
|
31
|
+
import {
|
|
32
|
+
textContent,
|
|
33
|
+
inputValue,
|
|
34
|
+
getAttribute,
|
|
35
|
+
getInputValue,
|
|
36
|
+
logElementInfo,
|
|
37
|
+
} from './elements/content.js';
|
|
38
|
+
import {
|
|
39
|
+
scrollIntoView,
|
|
40
|
+
needsScrolling,
|
|
41
|
+
scrollIntoViewIfNeeded,
|
|
42
|
+
} from './interactions/scroll.js';
|
|
43
|
+
import { clickElement, clickButton } from './interactions/click.js';
|
|
44
|
+
import {
|
|
45
|
+
checkIfElementEmpty,
|
|
46
|
+
performFill,
|
|
47
|
+
fillTextArea,
|
|
48
|
+
} from './interactions/fill.js';
|
|
49
|
+
import {
|
|
50
|
+
waitForUrlCondition,
|
|
51
|
+
installClickListener,
|
|
52
|
+
checkAndClearFlag,
|
|
53
|
+
findToggleButton,
|
|
54
|
+
} from './high-level/universal-logic.js';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create bound functions for a browser commander instance
|
|
58
|
+
* @param {Object} options - Configuration
|
|
59
|
+
* @param {Object} options.page - Browser page object
|
|
60
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
61
|
+
* @param {Function} options.log - Logger instance
|
|
62
|
+
* @param {boolean} options.verbose - Enable verbose logging
|
|
63
|
+
* @param {Object} options.navigationManager - NavigationManager instance (optional)
|
|
64
|
+
* @param {Object} options.networkTracker - NetworkTracker instance (optional)
|
|
65
|
+
* @returns {Object} - Object containing all bound functions
|
|
66
|
+
*/
|
|
67
|
+
export function createBoundFunctions(options = {}) {
|
|
68
|
+
const {
|
|
69
|
+
page,
|
|
70
|
+
engine,
|
|
71
|
+
log,
|
|
72
|
+
verbose = false,
|
|
73
|
+
navigationManager,
|
|
74
|
+
networkTracker,
|
|
75
|
+
} = options;
|
|
76
|
+
|
|
77
|
+
// Create bound helper functions that inject page, engine, log
|
|
78
|
+
// Wait function now automatically gets abort signal from navigation manager
|
|
79
|
+
const waitBound = (opts) => {
|
|
80
|
+
const abortSignal = navigationManager
|
|
81
|
+
? navigationManager.getAbortSignal()
|
|
82
|
+
: null;
|
|
83
|
+
return wait({ ...opts, log, abortSignal: opts.abortSignal || abortSignal });
|
|
84
|
+
};
|
|
85
|
+
const evaluateBound = (opts) => evaluate({ ...opts, page, engine });
|
|
86
|
+
const safeEvaluateBound = (opts) => safeEvaluate({ ...opts, page, engine });
|
|
87
|
+
const getUrlBound = () => getUrl({ page });
|
|
88
|
+
const unfocusAddressBarBound = (opts = {}) =>
|
|
89
|
+
unfocusAddressBar({ ...opts, page });
|
|
90
|
+
|
|
91
|
+
// Bound navigation - with NavigationManager integration
|
|
92
|
+
const waitForUrlStabilizationBound = (opts) =>
|
|
93
|
+
waitForUrlStabilization({
|
|
94
|
+
...opts,
|
|
95
|
+
page,
|
|
96
|
+
log,
|
|
97
|
+
wait: waitBound,
|
|
98
|
+
navigationManager,
|
|
99
|
+
});
|
|
100
|
+
const gotoBound = (opts) =>
|
|
101
|
+
goto({
|
|
102
|
+
...opts,
|
|
103
|
+
page,
|
|
104
|
+
waitForUrlStabilization: waitForUrlStabilizationBound,
|
|
105
|
+
navigationManager,
|
|
106
|
+
});
|
|
107
|
+
const waitForNavigationBound = (opts) =>
|
|
108
|
+
waitForNavigation({
|
|
109
|
+
...opts,
|
|
110
|
+
page,
|
|
111
|
+
navigationManager,
|
|
112
|
+
});
|
|
113
|
+
const waitForPageReadyBound = (opts) =>
|
|
114
|
+
waitForPageReady({
|
|
115
|
+
...opts,
|
|
116
|
+
page,
|
|
117
|
+
navigationManager,
|
|
118
|
+
networkTracker,
|
|
119
|
+
log,
|
|
120
|
+
wait: waitBound,
|
|
121
|
+
});
|
|
122
|
+
const waitAfterActionBound = (opts) =>
|
|
123
|
+
waitAfterAction({
|
|
124
|
+
...opts,
|
|
125
|
+
page,
|
|
126
|
+
navigationManager,
|
|
127
|
+
networkTracker,
|
|
128
|
+
log,
|
|
129
|
+
wait: waitBound,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Bound locators
|
|
133
|
+
const createPlaywrightLocatorBound = (opts) =>
|
|
134
|
+
createPlaywrightLocator({ ...opts, page });
|
|
135
|
+
const getLocatorOrElementBound = (opts) =>
|
|
136
|
+
getLocatorOrElement({ ...opts, page, engine });
|
|
137
|
+
const waitForLocatorOrElementBound = (opts) =>
|
|
138
|
+
waitForLocatorOrElement({ ...opts, page, engine });
|
|
139
|
+
const waitForVisibleBound = (opts) => waitForVisible({ ...opts, engine });
|
|
140
|
+
const locatorBound = (opts) => locator({ ...opts, page, engine });
|
|
141
|
+
|
|
142
|
+
// Bound selectors
|
|
143
|
+
const querySelectorBound = (opts) => querySelector({ ...opts, page, engine });
|
|
144
|
+
const querySelectorAllBound = (opts) =>
|
|
145
|
+
querySelectorAll({ ...opts, page, engine });
|
|
146
|
+
const findByTextBound = (opts) => findByText({ ...opts, engine });
|
|
147
|
+
const normalizeSelectorBound = (opts) =>
|
|
148
|
+
normalizeSelector({ ...opts, page, engine });
|
|
149
|
+
const waitForSelectorBound = (opts) =>
|
|
150
|
+
waitForSelector({ ...opts, page, engine });
|
|
151
|
+
|
|
152
|
+
// Bound visibility
|
|
153
|
+
const isVisibleBound = (opts) => isVisible({ ...opts, page, engine });
|
|
154
|
+
const isEnabledBound = (opts) => isEnabled({ ...opts, page, engine });
|
|
155
|
+
const countBound = (opts) => count({ ...opts, page, engine });
|
|
156
|
+
|
|
157
|
+
// Bound content
|
|
158
|
+
const textContentBound = (opts) => textContent({ ...opts, page, engine });
|
|
159
|
+
const inputValueBound = (opts) => inputValue({ ...opts, page, engine });
|
|
160
|
+
const getAttributeBound = (opts) => getAttribute({ ...opts, page, engine });
|
|
161
|
+
const getInputValueBound = (opts) => getInputValue({ ...opts, page, engine });
|
|
162
|
+
const logElementInfoBound = (opts) =>
|
|
163
|
+
logElementInfo({ ...opts, page, engine, log });
|
|
164
|
+
|
|
165
|
+
// Bound scroll
|
|
166
|
+
const scrollIntoViewBound = (opts) =>
|
|
167
|
+
scrollIntoView({ ...opts, page, engine });
|
|
168
|
+
const needsScrollingBound = (opts) =>
|
|
169
|
+
needsScrolling({ ...opts, page, engine });
|
|
170
|
+
const scrollIntoViewIfNeededBound = (opts) =>
|
|
171
|
+
scrollIntoViewIfNeeded({ ...opts, page, engine, wait: waitBound, log });
|
|
172
|
+
|
|
173
|
+
// Bound click - now navigation-aware
|
|
174
|
+
const clickElementBound = (opts) => clickElement({ ...opts, engine, log });
|
|
175
|
+
const clickButtonBound = (opts) =>
|
|
176
|
+
clickButton({
|
|
177
|
+
...opts,
|
|
178
|
+
page,
|
|
179
|
+
engine,
|
|
180
|
+
wait: waitBound,
|
|
181
|
+
log,
|
|
182
|
+
verbose,
|
|
183
|
+
navigationManager,
|
|
184
|
+
networkTracker,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Bound fill
|
|
188
|
+
const checkIfElementEmptyBound = (opts) =>
|
|
189
|
+
checkIfElementEmpty({ ...opts, page, engine });
|
|
190
|
+
const performFillBound = (opts) => performFill({ ...opts, page, engine });
|
|
191
|
+
const fillTextAreaBound = (opts) =>
|
|
192
|
+
fillTextArea({ ...opts, page, engine, wait: waitBound, log });
|
|
193
|
+
|
|
194
|
+
// Bound high-level
|
|
195
|
+
const waitForUrlConditionBound = (opts) =>
|
|
196
|
+
waitForUrlCondition({
|
|
197
|
+
...opts,
|
|
198
|
+
getUrl: getUrlBound,
|
|
199
|
+
wait: waitBound,
|
|
200
|
+
evaluate: evaluateBound,
|
|
201
|
+
});
|
|
202
|
+
const installClickListenerBound = (opts) =>
|
|
203
|
+
installClickListener({ ...opts, evaluate: evaluateBound });
|
|
204
|
+
const checkAndClearFlagBound = (opts) =>
|
|
205
|
+
checkAndClearFlag({ ...opts, evaluate: evaluateBound });
|
|
206
|
+
const findToggleButtonBound = (opts) =>
|
|
207
|
+
findToggleButton({
|
|
208
|
+
...opts,
|
|
209
|
+
count: countBound,
|
|
210
|
+
findByText: findByTextBound,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Wrap functions with text selector support
|
|
214
|
+
const fillTextAreaWrapped = withTextSelectorSupport(
|
|
215
|
+
fillTextAreaBound,
|
|
216
|
+
engine,
|
|
217
|
+
page
|
|
218
|
+
);
|
|
219
|
+
const clickButtonWrapped = withTextSelectorSupport(
|
|
220
|
+
clickButtonBound,
|
|
221
|
+
engine,
|
|
222
|
+
page
|
|
223
|
+
);
|
|
224
|
+
const getAttributeWrapped = withTextSelectorSupport(
|
|
225
|
+
getAttributeBound,
|
|
226
|
+
engine,
|
|
227
|
+
page
|
|
228
|
+
);
|
|
229
|
+
const isVisibleWrapped = withTextSelectorSupport(
|
|
230
|
+
isVisibleBound,
|
|
231
|
+
engine,
|
|
232
|
+
page
|
|
233
|
+
);
|
|
234
|
+
const isEnabledWrapped = withTextSelectorSupport(
|
|
235
|
+
isEnabledBound,
|
|
236
|
+
engine,
|
|
237
|
+
page
|
|
238
|
+
);
|
|
239
|
+
const textContentWrapped = withTextSelectorSupport(
|
|
240
|
+
textContentBound,
|
|
241
|
+
engine,
|
|
242
|
+
page
|
|
243
|
+
);
|
|
244
|
+
const inputValueWrapped = withTextSelectorSupport(
|
|
245
|
+
inputValueBound,
|
|
246
|
+
engine,
|
|
247
|
+
page
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
// Helper functions (now public)
|
|
252
|
+
createPlaywrightLocator: createPlaywrightLocatorBound,
|
|
253
|
+
getLocatorOrElement: getLocatorOrElementBound,
|
|
254
|
+
waitForLocatorOrElement: waitForLocatorOrElementBound,
|
|
255
|
+
scrollIntoView: scrollIntoViewBound,
|
|
256
|
+
scrollIntoViewIfNeeded: scrollIntoViewIfNeededBound,
|
|
257
|
+
needsScrolling: needsScrollingBound,
|
|
258
|
+
checkIfElementEmpty: checkIfElementEmptyBound,
|
|
259
|
+
performFill: performFillBound,
|
|
260
|
+
logElementInfo: logElementInfoBound,
|
|
261
|
+
normalizeSelector: normalizeSelectorBound,
|
|
262
|
+
withTextSelectorSupport: (fn) => withTextSelectorSupport(fn, engine, page),
|
|
263
|
+
waitForVisible: waitForVisibleBound,
|
|
264
|
+
clickElement: clickElementBound,
|
|
265
|
+
getInputValue: getInputValueBound,
|
|
266
|
+
unfocusAddressBar: unfocusAddressBarBound,
|
|
267
|
+
|
|
268
|
+
// Main API functions
|
|
269
|
+
wait: waitBound,
|
|
270
|
+
fillTextArea: fillTextAreaWrapped,
|
|
271
|
+
clickButton: clickButtonWrapped,
|
|
272
|
+
evaluate: evaluateBound,
|
|
273
|
+
safeEvaluate: safeEvaluateBound,
|
|
274
|
+
waitForSelector: waitForSelectorBound,
|
|
275
|
+
querySelector: querySelectorBound,
|
|
276
|
+
querySelectorAll: querySelectorAllBound,
|
|
277
|
+
waitForUrlStabilization: waitForUrlStabilizationBound,
|
|
278
|
+
goto: gotoBound,
|
|
279
|
+
getUrl: getUrlBound,
|
|
280
|
+
waitForNavigation: waitForNavigationBound,
|
|
281
|
+
waitForPageReady: waitForPageReadyBound,
|
|
282
|
+
waitAfterAction: waitAfterActionBound,
|
|
283
|
+
getAttribute: getAttributeWrapped,
|
|
284
|
+
isVisible: isVisibleWrapped,
|
|
285
|
+
isEnabled: isEnabledWrapped,
|
|
286
|
+
count: countBound,
|
|
287
|
+
textContent: textContentWrapped,
|
|
288
|
+
inputValue: inputValueWrapped,
|
|
289
|
+
locator: locatorBound,
|
|
290
|
+
findByText: findByTextBound,
|
|
291
|
+
|
|
292
|
+
// Universal High-Level Functions (DRY Principle)
|
|
293
|
+
waitForUrlCondition: waitForUrlConditionBound,
|
|
294
|
+
installClickListener: installClickListenerBound,
|
|
295
|
+
checkAndClearFlag: checkAndClearFlagBound,
|
|
296
|
+
findToggleButton: findToggleButtonBound,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import { CHROME_ARGS } from '../core/constants.js';
|
|
4
|
+
import { disableTranslateInPreferences } from '../core/preferences.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Launch browser with default configuration
|
|
8
|
+
* @param {Object} options - Configuration options
|
|
9
|
+
* @param {string} options.engine - Browser automation engine: 'playwright' or 'puppeteer'
|
|
10
|
+
* @param {string} options.userDataDir - Path to user data directory
|
|
11
|
+
* @param {boolean} options.headless - Run in headless mode (default: false)
|
|
12
|
+
* @param {number} options.slowMo - Slow down operations by ms (default: 150 for Playwright, 0 for Puppeteer)
|
|
13
|
+
* @param {boolean} options.verbose - Enable verbose logging (default: false)
|
|
14
|
+
* @returns {Promise<Object>} - Object with browser and page
|
|
15
|
+
*/
|
|
16
|
+
export async function launchBrowser(options = {}) {
|
|
17
|
+
const {
|
|
18
|
+
engine = 'playwright',
|
|
19
|
+
userDataDir = path.join(os.homedir(), '.hh-apply', `${engine}-data`),
|
|
20
|
+
headless = false,
|
|
21
|
+
slowMo = engine === 'playwright' ? 150 : 0,
|
|
22
|
+
verbose = false,
|
|
23
|
+
} = options;
|
|
24
|
+
|
|
25
|
+
if (!['playwright', 'puppeteer'].includes(engine)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Invalid engine: ${engine}. Expected 'playwright' or 'puppeteer'`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Set environment variables to suppress warnings
|
|
32
|
+
process.env.GOOGLE_API_KEY = 'no';
|
|
33
|
+
process.env.GOOGLE_DEFAULT_CLIENT_ID = 'no';
|
|
34
|
+
process.env.GOOGLE_DEFAULT_CLIENT_SECRET = 'no';
|
|
35
|
+
|
|
36
|
+
// Disable translate in Preferences
|
|
37
|
+
await disableTranslateInPreferences({ userDataDir });
|
|
38
|
+
|
|
39
|
+
if (verbose) {
|
|
40
|
+
console.log(`🚀 Launching browser with ${engine} engine...`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let browser;
|
|
44
|
+
let page;
|
|
45
|
+
|
|
46
|
+
if (engine === 'playwright') {
|
|
47
|
+
const { chromium } = await import('playwright');
|
|
48
|
+
browser = await chromium.launchPersistentContext(userDataDir, {
|
|
49
|
+
headless,
|
|
50
|
+
slowMo,
|
|
51
|
+
chromiumSandbox: true,
|
|
52
|
+
viewport: null,
|
|
53
|
+
args: CHROME_ARGS,
|
|
54
|
+
ignoreDefaultArgs: ['--enable-automation'],
|
|
55
|
+
});
|
|
56
|
+
page = browser.pages()[0];
|
|
57
|
+
} else {
|
|
58
|
+
const puppeteer = await import('puppeteer');
|
|
59
|
+
browser = await puppeteer.default.launch({
|
|
60
|
+
headless,
|
|
61
|
+
defaultViewport: null,
|
|
62
|
+
args: ['--start-maximized', ...CHROME_ARGS],
|
|
63
|
+
userDataDir,
|
|
64
|
+
});
|
|
65
|
+
const pages = await browser.pages();
|
|
66
|
+
page = pages[0];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (verbose) {
|
|
70
|
+
console.log(`✅ Browser launched with ${engine} engine`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Unfocus address bar automatically after browser launch
|
|
74
|
+
// Using page.bringToFront() - confirmed working solution
|
|
75
|
+
try {
|
|
76
|
+
// Wait for the browser to fully initialize
|
|
77
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
78
|
+
|
|
79
|
+
// Bring page to front - this removes focus from address bar
|
|
80
|
+
await page.bringToFront();
|
|
81
|
+
|
|
82
|
+
if (verbose) {
|
|
83
|
+
console.log('✅ Address bar unfocused automatically');
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
// Ignore errors - this is just a UX improvement
|
|
87
|
+
if (verbose) {
|
|
88
|
+
console.log('⚠️ Could not unfocus address bar:', error.message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { browser, page };
|
|
93
|
+
}
|