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
package/src/factory.js ADDED
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Browser Commander - Factory Function
3
+ * This module provides the makeBrowserCommander factory function that creates
4
+ * a browser commander instance with all bound methods.
5
+ */
6
+
7
+ import { createLogger } from './core/logger.js';
8
+ import { detectEngine } from './core/engine-detection.js';
9
+ import { createNetworkTracker } from './core/network-tracker.js';
10
+ import { createNavigationManager } from './core/navigation-manager.js';
11
+ import { createPageSessionFactory } from './core/page-session.js';
12
+ import {
13
+ createPageTriggerManager,
14
+ ActionStoppedError,
15
+ isActionStoppedError,
16
+ makeUrlCondition,
17
+ allConditions,
18
+ anyCondition,
19
+ notCondition,
20
+ } from './core/page-trigger-manager.js';
21
+ import { createBoundFunctions } from './bindings.js';
22
+
23
+ /**
24
+ * Create a browser commander instance for a specific page
25
+ * @param {Object} options - Configuration options
26
+ * @param {Object} options.page - Playwright or Puppeteer page object
27
+ * @param {boolean} options.verbose - Enable verbose logging
28
+ * @param {boolean} options.enableNetworkTracking - Enable network request tracking (default: true)
29
+ * @param {boolean} options.enableNavigationManager - Enable navigation manager (default: true)
30
+ * @returns {Object} - Browser commander API
31
+ */
32
+ export function makeBrowserCommander(options = {}) {
33
+ const {
34
+ page,
35
+ verbose = false,
36
+ enableNetworkTracking = true,
37
+ enableNavigationManager = true,
38
+ } = options;
39
+
40
+ if (!page) {
41
+ throw new Error('page is required in options');
42
+ }
43
+
44
+ const engine = detectEngine(page);
45
+ const log = createLogger({ verbose });
46
+
47
+ // Create NetworkTracker if enabled
48
+ // Use 30 second idle timeout to ensure page is fully loaded
49
+ let networkTracker = null;
50
+ if (enableNetworkTracking) {
51
+ networkTracker = createNetworkTracker({
52
+ page,
53
+ engine,
54
+ log,
55
+ idleTimeout: 30000, // Wait 30 seconds without requests before considering network idle
56
+ });
57
+ networkTracker.startTracking();
58
+ }
59
+
60
+ // Create NavigationManager if enabled
61
+ let navigationManager = null;
62
+ let sessionFactory = null;
63
+
64
+ // PageTriggerManager (will be initialized after commander is created)
65
+ let pageTriggerManager = null;
66
+
67
+ if (enableNavigationManager) {
68
+ navigationManager = createNavigationManager({
69
+ page,
70
+ engine,
71
+ log,
72
+ networkTracker,
73
+ });
74
+ navigationManager.startListening();
75
+
76
+ // Create PageSession factory
77
+ sessionFactory = createPageSessionFactory({
78
+ navigationManager,
79
+ networkTracker,
80
+ log,
81
+ });
82
+
83
+ // Create PageTriggerManager
84
+ pageTriggerManager = createPageTriggerManager({
85
+ navigationManager,
86
+ log,
87
+ });
88
+ }
89
+
90
+ // Create all bound functions
91
+ const boundFunctions = createBoundFunctions({
92
+ page,
93
+ engine,
94
+ log,
95
+ verbose,
96
+ navigationManager,
97
+ networkTracker,
98
+ });
99
+
100
+ // Cleanup function
101
+ const destroy = async () => {
102
+ if (pageTriggerManager) {
103
+ await pageTriggerManager.destroy();
104
+ }
105
+ if (networkTracker) {
106
+ networkTracker.stopTracking();
107
+ }
108
+ if (navigationManager) {
109
+ navigationManager.stopListening();
110
+ }
111
+ if (sessionFactory) {
112
+ await sessionFactory.endAllSessions();
113
+ }
114
+ };
115
+
116
+ // Build commander object
117
+ const commander = {
118
+ // Core properties
119
+ engine,
120
+ page,
121
+ log,
122
+
123
+ // Navigation management components
124
+ networkTracker,
125
+ navigationManager,
126
+ sessionFactory,
127
+ pageTriggerManager,
128
+
129
+ // All bound functions
130
+ ...boundFunctions,
131
+
132
+ // Lifecycle
133
+ destroy,
134
+
135
+ // Convenience methods for page sessions (legacy API)
136
+ createSession: sessionFactory
137
+ ? (opts) => sessionFactory.createSession(opts)
138
+ : null,
139
+ getActiveSessions: sessionFactory
140
+ ? () => sessionFactory.getActiveSessions()
141
+ : () => [],
142
+
143
+ // Subscribe to navigation events (legacy API)
144
+ onNavigationStart: navigationManager
145
+ ? (fn) => navigationManager.on('onNavigationStart', fn)
146
+ : () => {},
147
+ onNavigationComplete: navigationManager
148
+ ? (fn) => navigationManager.on('onNavigationComplete', fn)
149
+ : () => {},
150
+ onUrlChange: navigationManager
151
+ ? (fn) => navigationManager.on('onUrlChange', fn)
152
+ : () => {},
153
+ onPageReady: navigationManager
154
+ ? (fn) => navigationManager.on('onPageReady', fn)
155
+ : () => {},
156
+
157
+ // Abort handling - check these to stop operations when navigation occurs
158
+ shouldAbort: navigationManager
159
+ ? () => navigationManager.shouldAbort()
160
+ : () => false,
161
+ getAbortSignal: navigationManager
162
+ ? () => navigationManager.getAbortSignal()
163
+ : () => null,
164
+
165
+ // Page Trigger API
166
+ // Register a trigger: commander.pageTrigger({ condition, action, name })
167
+ // condition receives context: { url, commander }
168
+ // action receives context: { url, commander, checkStopped, forEach, wait, onCleanup, ... }
169
+ pageTrigger: pageTriggerManager
170
+ ? (config) => pageTriggerManager.pageTrigger(config)
171
+ : () => {
172
+ throw new Error('pageTrigger requires enableNavigationManager: true');
173
+ },
174
+
175
+ // URL condition helpers
176
+ makeUrlCondition,
177
+ allConditions,
178
+ anyCondition,
179
+ notCondition,
180
+
181
+ // Error classes for action control flow
182
+ ActionStoppedError,
183
+ isActionStoppedError,
184
+ };
185
+
186
+ // Initialize PageTriggerManager with the commander
187
+ if (pageTriggerManager) {
188
+ pageTriggerManager.initialize(commander);
189
+ }
190
+
191
+ return commander;
192
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Universal high-level functions following DRY principles
3
+ * These are pure functions that work with any browser automation engine
4
+ */
5
+
6
+ import {
7
+ isNavigationError,
8
+ withNavigationSafety,
9
+ } from '../core/navigation-safety.js';
10
+
11
+ /**
12
+ * Wait indefinitely for a URL condition with custom check function
13
+ * @param {Object} options - Configuration options
14
+ * @param {Function} options.getUrl - Function to get current URL
15
+ * @param {Function} options.wait - Wait function
16
+ * @param {Function} options.evaluate - Evaluate function
17
+ * @param {string} options.targetUrl - Target URL to wait for
18
+ * @param {string} options.description - Description for logging
19
+ * @param {Function} options.customCheck - Optional custom check function (async)
20
+ * @param {Function} options.pageClosedCallback - Callback to check if page closed
21
+ * @param {number} options.pollingInterval - Polling interval in ms (default: 1000)
22
+ * @returns {Promise<any>} - Result from customCheck or true if URL matched
23
+ */
24
+ export async function waitForUrlCondition(options = {}) {
25
+ const {
26
+ getUrl,
27
+ wait,
28
+ evaluate,
29
+ targetUrl,
30
+ description,
31
+ customCheck,
32
+ pageClosedCallback = () => false,
33
+ pollingInterval = 1000,
34
+ } = options;
35
+
36
+ if (description) {
37
+ console.log(`⏳ ${description}...`);
38
+ }
39
+
40
+ while (true) {
41
+ if (pageClosedCallback()) {
42
+ return null;
43
+ }
44
+
45
+ try {
46
+ // Run custom check if provided
47
+ if (customCheck) {
48
+ const customResult = await customCheck(getUrl());
49
+ if (customResult !== undefined && customResult !== null) {
50
+ return customResult;
51
+ }
52
+ }
53
+
54
+ // Check if target URL reached
55
+ const currentUrl = getUrl();
56
+ if (currentUrl.startsWith(targetUrl)) {
57
+ return true;
58
+ }
59
+ } catch (error) {
60
+ if (pageClosedCallback()) {
61
+ return null;
62
+ }
63
+
64
+ // Handle navigation errors gracefully
65
+ if (isNavigationError(error)) {
66
+ console.log(
67
+ '⚠️ Navigation detected during URL check, continuing to wait...'
68
+ );
69
+ } else {
70
+ console.log(
71
+ `⚠️ Temporary error while checking URL: ${error.message.substring(0, 100)}... (retrying)`
72
+ );
73
+ }
74
+ }
75
+
76
+ await wait({
77
+ ms: pollingInterval,
78
+ reason: 'polling interval before next URL check',
79
+ });
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Install click detection listener on page
85
+ * @param {Object} options - Configuration options
86
+ * @param {Function} options.evaluate - Evaluate function
87
+ * @param {string} options.buttonText - Text to detect
88
+ * @param {string} options.storageKey - SessionStorage key to set
89
+ * @returns {Promise<boolean>} - True if installed, false on navigation
90
+ */
91
+ export async function installClickListener(options = {}) {
92
+ const { evaluate, buttonText, storageKey } = options;
93
+
94
+ const safeEvaluate = withNavigationSafety(evaluate, {
95
+ onNavigationError: () => {
96
+ console.log(
97
+ '⚠️ Navigation detected during installClickListener, skipping'
98
+ );
99
+ return false;
100
+ },
101
+ });
102
+
103
+ const result = await safeEvaluate({
104
+ fn: (text, key) => {
105
+ document.addEventListener(
106
+ 'click',
107
+ (event) => {
108
+ let element = event.target;
109
+ while (element && element !== document.body) {
110
+ const elementText = element.textContent?.trim() || '';
111
+ if (
112
+ elementText === text ||
113
+ ((element.tagName === 'A' || element.tagName === 'BUTTON') &&
114
+ elementText.includes(text))
115
+ ) {
116
+ console.log(`[Click Listener] Detected click on ${text} button!`);
117
+ window.sessionStorage.setItem(key, 'true');
118
+ break;
119
+ }
120
+ element = element.parentElement;
121
+ }
122
+ },
123
+ true
124
+ );
125
+ },
126
+ args: [buttonText, storageKey],
127
+ });
128
+
129
+ return result === false ? false : true;
130
+ }
131
+
132
+ /**
133
+ * Check and clear session storage flag
134
+ * @param {Object} options - Configuration options
135
+ * @param {Function} options.evaluate - Evaluate function
136
+ * @param {string} options.storageKey - SessionStorage key
137
+ * @returns {Promise<boolean>} - True if flag was set, false otherwise or on navigation
138
+ */
139
+ export async function checkAndClearFlag(options = {}) {
140
+ const { evaluate, storageKey } = options;
141
+
142
+ const safeEvaluate = withNavigationSafety(evaluate, {
143
+ onNavigationError: () => {
144
+ console.log(
145
+ '⚠️ Navigation detected during checkAndClearFlag, returning false'
146
+ );
147
+ return false;
148
+ },
149
+ });
150
+
151
+ return await safeEvaluate({
152
+ fn: (key) => {
153
+ const flag = window.sessionStorage.getItem(key);
154
+ if (flag === 'true') {
155
+ window.sessionStorage.removeItem(key);
156
+ return true;
157
+ }
158
+ return false;
159
+ },
160
+ args: [storageKey],
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Find toggle button using multiple strategies
166
+ * @param {Object} options - Configuration options
167
+ * @param {Function} options.count - Count function
168
+ * @param {Function} options.findByText - FindByText function
169
+ * @param {Array<string>} options.dataQaSelectors - Data-qa selectors to try
170
+ * @param {string} options.textToFind - Text to search for
171
+ * @param {Array<string>} options.elementTypes - Element types to search
172
+ * @returns {Promise<string|null>} - Selector or null
173
+ */
174
+ export async function findToggleButton(options = {}) {
175
+ const {
176
+ count,
177
+ findByText,
178
+ dataQaSelectors = [],
179
+ textToFind,
180
+ elementTypes = ['button', 'a', 'span'],
181
+ } = options;
182
+
183
+ // Try data-qa selectors first
184
+ for (const sel of dataQaSelectors) {
185
+ const elemCount = await count({ selector: sel });
186
+ if (elemCount > 0) {
187
+ return sel;
188
+ }
189
+ }
190
+
191
+ // Fallback to text search
192
+ if (textToFind) {
193
+ for (const elementType of elementTypes) {
194
+ const selector = await findByText({
195
+ text: textToFind,
196
+ selector: elementType,
197
+ });
198
+ const elemCount = await count({ selector });
199
+ if (elemCount > 0) {
200
+ return selector;
201
+ }
202
+ }
203
+ }
204
+
205
+ return null;
206
+ }
package/src/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Browser Commander - Universal browser automation library
3
+ * Supports both Playwright and Puppeteer with a unified API
4
+ * All functions use options objects for easy maintenance
5
+ *
6
+ * Key features:
7
+ * - Automatic network request tracking
8
+ * - Navigation-aware operations (wait for page ready after navigations)
9
+ * - Event-based page lifecycle management
10
+ * - Session management for per-page automation logic
11
+ */
12
+
13
+ // Export the factory function
14
+ export { makeBrowserCommander } from './factory.js';
15
+
16
+ // Re-export all public APIs from exports module
17
+ export * from './exports.js';