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.
Files changed (82) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/release.yml +296 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.jscpd.json +20 -0
  6. package/.prettierignore +7 -0
  7. package/.prettierrc +10 -0
  8. package/CHANGELOG.md +32 -0
  9. package/LICENSE +24 -0
  10. package/README.md +320 -0
  11. package/bunfig.toml +3 -0
  12. package/deno.json +7 -0
  13. package/eslint.config.js +125 -0
  14. package/examples/react-test-app/index.html +25 -0
  15. package/examples/react-test-app/package.json +19 -0
  16. package/examples/react-test-app/src/App.jsx +473 -0
  17. package/examples/react-test-app/src/main.jsx +10 -0
  18. package/examples/react-test-app/src/styles.css +323 -0
  19. package/examples/react-test-app/vite.config.js +9 -0
  20. package/package.json +89 -0
  21. package/scripts/changeset-version.mjs +38 -0
  22. package/scripts/create-github-release.mjs +93 -0
  23. package/scripts/create-manual-changeset.mjs +86 -0
  24. package/scripts/format-github-release.mjs +83 -0
  25. package/scripts/format-release-notes.mjs +216 -0
  26. package/scripts/instant-version-bump.mjs +121 -0
  27. package/scripts/merge-changesets.mjs +260 -0
  28. package/scripts/publish-to-npm.mjs +126 -0
  29. package/scripts/setup-npm.mjs +37 -0
  30. package/scripts/validate-changeset.mjs +262 -0
  31. package/scripts/version-and-commit.mjs +237 -0
  32. package/src/ARCHITECTURE.md +270 -0
  33. package/src/README.md +517 -0
  34. package/src/bindings.js +298 -0
  35. package/src/browser/launcher.js +93 -0
  36. package/src/browser/navigation.js +513 -0
  37. package/src/core/constants.js +24 -0
  38. package/src/core/engine-adapter.js +466 -0
  39. package/src/core/engine-detection.js +49 -0
  40. package/src/core/logger.js +21 -0
  41. package/src/core/navigation-manager.js +503 -0
  42. package/src/core/navigation-safety.js +160 -0
  43. package/src/core/network-tracker.js +373 -0
  44. package/src/core/page-session.js +299 -0
  45. package/src/core/page-trigger-manager.js +564 -0
  46. package/src/core/preferences.js +46 -0
  47. package/src/elements/content.js +197 -0
  48. package/src/elements/locators.js +243 -0
  49. package/src/elements/selectors.js +360 -0
  50. package/src/elements/visibility.js +166 -0
  51. package/src/exports.js +121 -0
  52. package/src/factory.js +192 -0
  53. package/src/high-level/universal-logic.js +206 -0
  54. package/src/index.js +17 -0
  55. package/src/interactions/click.js +684 -0
  56. package/src/interactions/fill.js +383 -0
  57. package/src/interactions/scroll.js +341 -0
  58. package/src/utilities/url.js +33 -0
  59. package/src/utilities/wait.js +135 -0
  60. package/tests/e2e/playwright.e2e.test.js +442 -0
  61. package/tests/e2e/puppeteer.e2e.test.js +408 -0
  62. package/tests/helpers/mocks.js +542 -0
  63. package/tests/unit/bindings.test.js +218 -0
  64. package/tests/unit/browser/navigation.test.js +345 -0
  65. package/tests/unit/core/constants.test.js +72 -0
  66. package/tests/unit/core/engine-adapter.test.js +170 -0
  67. package/tests/unit/core/engine-detection.test.js +81 -0
  68. package/tests/unit/core/logger.test.js +80 -0
  69. package/tests/unit/core/navigation-safety.test.js +202 -0
  70. package/tests/unit/core/network-tracker.test.js +198 -0
  71. package/tests/unit/core/page-trigger-manager.test.js +358 -0
  72. package/tests/unit/elements/content.test.js +318 -0
  73. package/tests/unit/elements/locators.test.js +236 -0
  74. package/tests/unit/elements/selectors.test.js +302 -0
  75. package/tests/unit/elements/visibility.test.js +234 -0
  76. package/tests/unit/factory.test.js +174 -0
  77. package/tests/unit/high-level/universal-logic.test.js +299 -0
  78. package/tests/unit/interactions/click.test.js +340 -0
  79. package/tests/unit/interactions/fill.test.js +378 -0
  80. package/tests/unit/interactions/scroll.test.js +330 -0
  81. package/tests/unit/utilities/url.test.js +63 -0
  82. package/tests/unit/utilities/wait.test.js +207 -0
@@ -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
+ }