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/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';
|