browser-commander 0.23.0 → 0.25.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 (77) hide show
  1. package/.jscpd-baseline.json +48 -30
  2. package/CHANGELOG.md +32 -0
  3. package/README.md +19 -7
  4. package/eslint.config.js +1 -1
  5. package/examples/react-test-app/package-lock.json +890 -0
  6. package/examples/react-test-app/package.json +4 -4
  7. package/package.json +20 -17
  8. package/src/browser/attach/snapshot.js +4 -28
  9. package/src/browser/browser-cookie-credentials.js +15 -8
  10. package/src/browser/browser-cookies.js +36 -30
  11. package/src/browser/browser-profile-files.js +31 -0
  12. package/src/browser/browser-profiles.js +11 -13
  13. package/src/browser/browser-sources.js +39 -9
  14. package/src/browser/browser-sources.json +600 -23
  15. package/src/browser/connector.js +46 -4
  16. package/src/browser/launcher.js +26 -1
  17. package/src/browser/migration/capabilities.json +117 -0
  18. package/src/browser/migration/chromium-history.sql +22 -0
  19. package/src/browser/migration/chromium-writers.js +85 -0
  20. package/src/browser/migration/cookies.js +4 -1
  21. package/src/browser/migration/data-classes.js +68 -0
  22. package/src/browser/migration/domains.js +32 -0
  23. package/src/browser/migration/firefox-history.js +106 -0
  24. package/src/browser/migration/firefox.js +39 -25
  25. package/src/browser/migration/history.js +161 -2
  26. package/src/browser/migration/index.js +229 -83
  27. package/src/browser/migration/os-crypt-keys.js +1 -5
  28. package/src/browser/migration/password-metadata.js +95 -0
  29. package/src/browser/migration/passwords.js +78 -41
  30. package/src/browser/migration/safari-import.js +100 -0
  31. package/src/browser/migration/safari.js +110 -0
  32. package/src/browser/migration/sqlite-snapshot.js +31 -65
  33. package/src/browser/migration/validation.js +48 -0
  34. package/src/browser/real-browser.js +21 -3
  35. package/src/browser/safari-access.js +45 -0
  36. package/src/browser/safari-cookies.js +39 -36
  37. package/src/browser/safari-profiles.js +83 -0
  38. package/src/browser/storage-state.js +27 -0
  39. package/src/browser/system-browser.js +31 -143
  40. package/src/browser/webdriver.js +18 -2
  41. package/src/cli/args.js +7 -0
  42. package/src/cli/commands.js +12 -6
  43. package/src/cli/handle-methods.js +19 -2
  44. package/src/cli/high-level-methods.js +22 -3
  45. package/src/cli/modules.js +5 -1
  46. package/src/cli/sessions.js +14 -2
  47. package/src/tests/index.js +1 -0
  48. package/tests/e2e/engine-matrix.e2e.test.js +107 -0
  49. package/tests/e2e/playwright.e2e.test.js +11 -17
  50. package/tests/e2e/puppeteer.e2e.test.js +13 -17
  51. package/tests/unit/browser/attach/snapshot.test.js +30 -0
  52. package/tests/unit/browser/browser-cookies.test.js +35 -0
  53. package/tests/unit/browser/catalogue-launch.test.js +93 -0
  54. package/tests/unit/browser/cookie-sources.test.js +92 -17
  55. package/tests/unit/browser/credential-catalogue.test.js +50 -0
  56. package/tests/unit/browser/firefox-cookie-expiry.test.js +79 -0
  57. package/tests/unit/browser/migration/data-classes.test.js +89 -0
  58. package/tests/unit/browser/migration/domain-isolation.test.js +212 -0
  59. package/tests/unit/browser/migration/firefox-history.test.js +130 -0
  60. package/tests/unit/browser/migration/history-metadata.test.js +74 -0
  61. package/tests/unit/browser/migration/index.test.js +16 -1
  62. package/tests/unit/browser/migration/opera-keys.test.js +22 -0
  63. package/tests/unit/browser/migration/safari-data.test.js +143 -0
  64. package/tests/unit/browser/migration/sqlite-snapshot.test.js +56 -1
  65. package/tests/unit/browser/migration/validation.test.js +63 -0
  66. package/tests/unit/browser/migration/yandex.test.js +84 -0
  67. package/tests/unit/browser/real-browser.test.js +19 -0
  68. package/tests/unit/browser/safari-cookies.test.js +26 -1
  69. package/tests/unit/browser/safari-profiles.test.js +111 -0
  70. package/tests/unit/cli/dispatcher.test.js +6 -2
  71. package/tests/unit/cli/main.test.js +5 -0
  72. package/tests/unit/cli/selenium-parity.test.js +186 -0
  73. package/tests/unit/scripts/browser-support.test.js +14 -7
  74. package/tests/unit/scripts/check-pipeline-status.test.js +33 -1
  75. package/tests/unit/scripts/migration-support.test.js +31 -0
  76. package/tests/unit/tests/index.test.js +2 -1
  77. package/tests/unit/traces/links.test.js +2 -2
@@ -0,0 +1,83 @@
1
+ import path from 'node:path';
2
+ import { open } from 'node:fs/promises';
3
+ import { findSafariCookieFile } from './safari-cookies.js';
4
+ import { findSafariFile, withSafariAccess } from './safari-access.js';
5
+ import { openSqliteDatabase } from './browser-cookie-database.js';
6
+
7
+ /** SafariTabs supplies names; each UUID has separate Safari and WebKit stores. */
8
+ export async function listSafariProfiles(browser, root) {
9
+ const profiles = [];
10
+ const defaultFile =
11
+ (await findSafariCookieFile(root)) ??
12
+ (await findSafariFile([
13
+ path.join(root, 'Safari/History.db'),
14
+ path.join(root, 'History.db'),
15
+ path.join(root, 'Safari/Bookmarks.plist'),
16
+ path.join(root, 'Bookmarks.plist'),
17
+ ]));
18
+ if (defaultFile) {
19
+ profiles.push({
20
+ browser,
21
+ name: 'Default',
22
+ displayName: 'Default',
23
+ path: root,
24
+ isDefault: true,
25
+ });
26
+ }
27
+ const tabs = await findSafariFile([
28
+ path.join(root, 'Safari/SafariTabs.db'),
29
+ path.join(root, 'SafariTabs.db'),
30
+ ]);
31
+ if (!tabs) {
32
+ return profiles;
33
+ }
34
+ let rows;
35
+ try {
36
+ rows = await withSafariAccess(tabs, async () => {
37
+ const file = await open(tabs, 'r');
38
+ await file.close();
39
+ const db = await openSqliteDatabase(tabs, {
40
+ readOnly: true,
41
+ fileMustExist: true,
42
+ });
43
+ try {
44
+ return db
45
+ .prepare(
46
+ "SELECT DISTINCT external_uuid,title FROM bookmarks WHERE subtype=2 AND external_uuid != 'DefaultProfile' ORDER BY external_uuid"
47
+ )
48
+ .all();
49
+ } finally {
50
+ db.close();
51
+ }
52
+ });
53
+ } catch (error) {
54
+ profiles.push({
55
+ browser,
56
+ name: 'Profiles',
57
+ displayName: 'Safari profile discovery',
58
+ path: root,
59
+ isDefault: false,
60
+ error: error.message,
61
+ });
62
+ return profiles;
63
+ }
64
+ for (const row of rows) {
65
+ if (
66
+ !/^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/iu.test(
67
+ row.external_uuid ?? ''
68
+ )
69
+ ) {
70
+ continue;
71
+ }
72
+ const name = row.external_uuid.toUpperCase();
73
+ const profilePath = path.join(root, 'Safari/Profiles', name);
74
+ profiles.push({
75
+ browser,
76
+ name,
77
+ displayName: row.title ?? name,
78
+ path: profilePath,
79
+ isDefault: false,
80
+ });
81
+ }
82
+ return profiles;
83
+ }
@@ -30,6 +30,33 @@ function restoreOriginLocalStorage(origins) {
30
30
  }
31
31
  }
32
32
 
33
+ /** Restore classic WebDriver cookies; origin preload scripts require BiDi. */
34
+ export async function restoreWebDriverStorageState({ page, storageState }) {
35
+ if (!storageState) {
36
+ return;
37
+ }
38
+ const origins = storageState.origins ?? [];
39
+ if (origins.length) {
40
+ await page.evaluateOnNewDocument(restoreOriginLocalStorage, origins);
41
+ await page.evaluate(restoreOriginLocalStorage, origins);
42
+ }
43
+ const originalUrl = page.url();
44
+ const cookies = storageState.cookies ?? [];
45
+ try {
46
+ for (const cookie of cookies) {
47
+ const origin = cookie.url
48
+ ? new URL(cookie.url).origin
49
+ : `${cookie.secure ? 'https' : 'http'}://${cookie.domain.replace(/^\./u, '')}`;
50
+ await page.goto(origin);
51
+ await page.setCookie(cookie);
52
+ }
53
+ } finally {
54
+ if (cookies.length) {
55
+ await page.goto(originalUrl);
56
+ }
57
+ }
58
+ }
59
+
33
60
  /**
34
61
  * Apply Playwright-compatible storage state to a Puppeteer page.
35
62
  * @param {Object} options - Restore options
@@ -1,4 +1,10 @@
1
1
  import { constants } from 'node:fs';
2
+ import { physicalPath } from './browser-profile-files.js';
3
+ import {
4
+ BROWSER_SOURCES,
5
+ resolveBrowserRoots,
6
+ resolveBrowserExecutables,
7
+ } from './browser-sources.js';
2
8
  import { access } from 'node:fs/promises';
3
9
  import os from 'node:os';
4
10
  import path from 'node:path';
@@ -9,18 +15,16 @@ import path from 'node:path';
9
15
  * to refuse a default profile; profile migration uses it to find the source.
10
16
  */
11
17
 
12
- export const CHANNEL_EXECUTABLE_NAMES = {
13
- brave: ['brave-browser', 'brave-browser-stable', 'brave'],
14
- chrome: ['google-chrome', 'google-chrome-stable', 'chrome'],
15
- 'chrome-beta': ['google-chrome-beta'],
16
- 'chrome-canary': ['google-chrome-canary'],
17
- 'chrome-dev': ['google-chrome-unstable'],
18
- chromium: ['chromium', 'chromium-browser'],
19
- msedge: ['microsoft-edge', 'microsoft-edge-stable', 'msedge'],
20
- 'msedge-beta': ['microsoft-edge-beta'],
21
- 'msedge-canary': ['microsoft-edge-canary'],
22
- 'msedge-dev': ['microsoft-edge-dev'],
23
- };
18
+ export const CHANNEL_EXECUTABLE_NAMES = Object.freeze(
19
+ Object.fromEntries(
20
+ BROWSER_SOURCES.flatMap((browser) =>
21
+ [browser.id, ...(browser.aliases ?? [])].map((name) => [
22
+ name,
23
+ browser.executableNames ?? [],
24
+ ])
25
+ )
26
+ )
27
+ );
24
28
 
25
29
  function platformPath(platform) {
26
30
  return platform === 'win32' ? path.win32 : path;
@@ -41,72 +45,13 @@ export function knownDefaultUserDataDirs({
41
45
  homeDir = os.homedir(),
42
46
  environment = process.env,
43
47
  } = {}) {
44
- const pathApi = platformPath(platform);
45
- if (platform === 'darwin') {
46
- const applicationSupport = pathApi.join(
48
+ return BROWSER_SOURCES.flatMap((browser) =>
49
+ resolveBrowserRoots(browser.id, {
50
+ platform,
47
51
  homeDir,
48
- 'Library',
49
- 'Application Support'
50
- );
51
- return [
52
- pathApi.join(applicationSupport, 'Google', 'Chrome'),
53
- pathApi.join(applicationSupport, 'Google', 'Chrome Beta'),
54
- pathApi.join(applicationSupport, 'Google', 'Chrome Canary'),
55
- pathApi.join(applicationSupport, 'Google', 'Chrome Dev'),
56
- pathApi.join(applicationSupport, 'Chromium'),
57
- pathApi.join(applicationSupport, 'BraveSoftware', 'Brave-Browser'),
58
- pathApi.join(applicationSupport, 'BraveSoftware', 'Brave-Browser-Beta'),
59
- pathApi.join(
60
- applicationSupport,
61
- 'BraveSoftware',
62
- 'Brave-Browser-Nightly'
63
- ),
64
- pathApi.join(applicationSupport, 'Microsoft Edge'),
65
- pathApi.join(applicationSupport, 'Microsoft Edge Beta'),
66
- pathApi.join(applicationSupport, 'Microsoft Edge Canary'),
67
- pathApi.join(applicationSupport, 'Microsoft Edge Dev'),
68
- ];
69
- }
70
- if (platform === 'win32') {
71
- const localAppData =
72
- environment.LOCALAPPDATA ?? pathApi.join(homeDir, 'AppData', 'Local');
73
- return [
74
- pathApi.join(localAppData, 'Google', 'Chrome', 'User Data'),
75
- pathApi.join(localAppData, 'Google', 'Chrome Beta', 'User Data'),
76
- pathApi.join(localAppData, 'Google', 'Chrome Dev', 'User Data'),
77
- pathApi.join(localAppData, 'Google', 'Chrome SxS', 'User Data'),
78
- pathApi.join(localAppData, 'Chromium', 'User Data'),
79
- pathApi.join(localAppData, 'BraveSoftware', 'Brave-Browser', 'User Data'),
80
- pathApi.join(
81
- localAppData,
82
- 'BraveSoftware',
83
- 'Brave-Browser-Beta',
84
- 'User Data'
85
- ),
86
- pathApi.join(
87
- localAppData,
88
- 'BraveSoftware',
89
- 'Brave-Browser-Nightly',
90
- 'User Data'
91
- ),
92
- pathApi.join(localAppData, 'Microsoft', 'Edge', 'User Data'),
93
- pathApi.join(localAppData, 'Microsoft', 'Edge Beta', 'User Data'),
94
- pathApi.join(localAppData, 'Microsoft', 'Edge Dev', 'User Data'),
95
- pathApi.join(localAppData, 'Microsoft', 'Edge SxS', 'User Data'),
96
- ];
97
- }
98
- return [
99
- pathApi.join(homeDir, '.config', 'google-chrome'),
100
- pathApi.join(homeDir, '.config', 'google-chrome-beta'),
101
- pathApi.join(homeDir, '.config', 'google-chrome-unstable'),
102
- pathApi.join(homeDir, '.config', 'chromium'),
103
- pathApi.join(homeDir, '.config', 'BraveSoftware', 'Brave-Browser'),
104
- pathApi.join(homeDir, '.config', 'BraveSoftware', 'Brave-Browser-Beta'),
105
- pathApi.join(homeDir, '.config', 'BraveSoftware', 'Brave-Browser-Nightly'),
106
- pathApi.join(homeDir, '.config', 'microsoft-edge'),
107
- pathApi.join(homeDir, '.config', 'microsoft-edge-beta'),
108
- pathApi.join(homeDir, '.config', 'microsoft-edge-dev'),
109
- ];
52
+ environment,
53
+ })
54
+ );
110
55
  }
111
56
 
112
57
  /** Ensure Chrome is not asked to expose the user's default profile over CDP. */
@@ -117,12 +62,17 @@ export function assertDedicatedUserDataDir(userDataDir, platformOptions = {}) {
117
62
  const platform = platformOptions.platform ?? process.platform;
118
63
  const pathApi = platformPath(platform);
119
64
  const normalize = (value) => {
120
- const resolved = pathApi.resolve(value).replace(/[\\/]+$/, '');
65
+ let resolved = pathApi.resolve(value).replace(/[\\/]+$/, '');
66
+ if (platform === process.platform) {
67
+ resolved = physicalPath(resolved);
68
+ }
121
69
  return platform === 'win32' ? resolved.toLowerCase() : resolved;
122
70
  };
123
71
  const requested = normalize(userDataDir);
124
72
  const isDefault = knownDefaultUserDataDirs(platformOptions).some(
125
- (directory) => normalize(directory) === requested
73
+ (directory) =>
74
+ requested === normalize(directory) ||
75
+ requested.startsWith(`${normalize(directory)}${pathApi.sep}`)
126
76
  );
127
77
  if (isDefault) {
128
78
  throw new Error(
@@ -136,74 +86,12 @@ function browserInstallCandidates({
136
86
  platform = process.platform,
137
87
  environment = process.env,
138
88
  }) {
139
- const candidates = [];
140
- const names = CHANNEL_EXECUTABLE_NAMES[channel];
141
- if (!names) {
89
+ if (!CHANNEL_EXECUTABLE_NAMES[channel]?.length) {
142
90
  throw new Error(
143
91
  `Unknown browser channel: ${channel}. Expected one of ${Object.keys(CHANNEL_EXECUTABLE_NAMES).join(', ')}`
144
92
  );
145
93
  }
146
-
147
- if (platform === 'darwin') {
148
- const applications = {
149
- brave: 'Brave Browser.app/Contents/MacOS/Brave Browser',
150
- chrome: 'Google Chrome.app/Contents/MacOS/Google Chrome',
151
- 'chrome-beta': 'Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta',
152
- 'chrome-canary':
153
- 'Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
154
- 'chrome-dev': 'Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev',
155
- chromium: 'Chromium.app/Contents/MacOS/Chromium',
156
- msedge: 'Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
157
- 'msedge-beta':
158
- 'Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta',
159
- 'msedge-canary':
160
- 'Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary',
161
- 'msedge-dev': 'Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev',
162
- };
163
- candidates.push(path.join('/Applications', applications[channel]));
164
- } else if (platform === 'win32') {
165
- const roots = [
166
- environment.PROGRAMFILES,
167
- environment['PROGRAMFILES(X86)'],
168
- environment.LOCALAPPDATA,
169
- ].filter(Boolean);
170
- const relativePaths = {
171
- brave: ['BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'],
172
- chrome: ['Google', 'Chrome', 'Application', 'chrome.exe'],
173
- 'chrome-beta': ['Google', 'Chrome Beta', 'Application', 'chrome.exe'],
174
- 'chrome-canary': ['Google', 'Chrome SxS', 'Application', 'chrome.exe'],
175
- 'chrome-dev': ['Google', 'Chrome Dev', 'Application', 'chrome.exe'],
176
- chromium: ['Chromium', 'Application', 'chrome.exe'],
177
- msedge: ['Microsoft', 'Edge', 'Application', 'msedge.exe'],
178
- 'msedge-beta': ['Microsoft', 'Edge Beta', 'Application', 'msedge.exe'],
179
- 'msedge-canary': ['Microsoft', 'Edge SxS', 'Application', 'msedge.exe'],
180
- 'msedge-dev': ['Microsoft', 'Edge Dev', 'Application', 'msedge.exe'],
181
- };
182
- for (const root of roots) {
183
- candidates.push(path.win32.join(root, ...relativePaths[channel]));
184
- }
185
- } else {
186
- for (const name of names) {
187
- candidates.push(`/usr/bin/${name}`, `/usr/local/bin/${name}`);
188
- }
189
- if (channel === 'chrome') {
190
- candidates.push('/opt/google/chrome/google-chrome');
191
- }
192
- }
193
-
194
- const pathApi = platformPath(platform);
195
- const delimiter = platform === 'win32' ? ';' : path.delimiter;
196
- for (const directory of (environment.PATH ?? '').split(delimiter)) {
197
- if (!directory) {
198
- continue;
199
- }
200
- for (const name of names) {
201
- candidates.push(
202
- pathApi.join(directory, platform === 'win32' ? `${name}.exe` : name)
203
- );
204
- }
205
- }
206
- return [...new Set(candidates)];
94
+ return resolveBrowserExecutables(channel, { platform, environment });
207
95
  }
208
96
 
209
97
  /** Resolve a genuine installed Chrome-family browser executable. */
@@ -302,11 +302,20 @@ export function buildBrowserOptions({
302
302
  args,
303
303
  automationParity,
304
304
  bidi,
305
+ debuggerAddress,
305
306
  }) {
306
307
  let options;
307
308
  let browserArgs;
308
309
  if (browser === 'chrome') {
309
310
  options = new selenium.chrome.Options();
311
+ if (debuggerAddress) {
312
+ options.debuggerAddress(debuggerAddress);
313
+ if (bidi) {
314
+ options.set('webSocketUrl', true);
315
+ options.set('unhandledPromptBehavior', 'ignore');
316
+ }
317
+ return { options, args: [] };
318
+ }
310
319
  browserArgs = buildRealBrowserArgs({
311
320
  userDataDir,
312
321
  remoteDebuggingPort,
@@ -450,8 +459,12 @@ export async function launchWebDriver(options = {}, dependencies = {}) {
450
459
  env,
451
460
  startupTimeout = 30_000,
452
461
  verbose = false,
462
+ debuggerAddress,
453
463
  } = options;
454
464
  assertBrowser(browser);
465
+ if (debuggerAddress && browser !== 'chrome') {
466
+ throw new Error('debuggerAddress is only supported by Chrome WebDriver');
467
+ }
455
468
  if (browser === 'firefox' && restrictions.length > 0) {
456
469
  throw new Error(
457
470
  'Launch restrictions are Chrome switches; not available for firefox'
@@ -476,8 +489,10 @@ export async function launchWebDriver(options = {}, dependencies = {}) {
476
489
  ...(dependencies.platform ? { platform: dependencies.platform } : {}),
477
490
  });
478
491
 
479
- const temporaryProfile = !requestedUserDataDir;
480
- const userDataDir = await resolveProfile(browser, requestedUserDataDir);
492
+ const temporaryProfile = !debuggerAddress && !requestedUserDataDir;
493
+ const userDataDir = debuggerAddress
494
+ ? undefined
495
+ : await resolveProfile(browser, requestedUserDataDir);
481
496
  const childEnv = driverEnvironment(restrictions, env);
482
497
 
483
498
  let driverProcess;
@@ -513,6 +528,7 @@ export async function launchWebDriver(options = {}, dependencies = {}) {
513
528
  args,
514
529
  automationParity,
515
530
  bidi,
531
+ debuggerAddress,
516
532
  });
517
533
  driver = await buildSession({
518
534
  selenium,
package/src/cli/args.js CHANGED
@@ -31,12 +31,15 @@ const BROWSER_OPTIONS = Object.freeze({
31
31
  'local-state': STRINGS,
32
32
  'default-browser-check': FLAG,
33
33
  'first-run': FLAG,
34
+ 'driver-path': STRING,
35
+ bidi: FLAG,
34
36
  });
35
37
 
36
38
  /** Options accepted by every page command. */
37
39
  const PAGE_OPTIONS = Object.freeze({
38
40
  ...BROWSER_OPTIONS,
39
41
  'cdp-endpoint': STRING,
42
+ 'server-url': STRING,
40
43
  url: STRING,
41
44
  });
42
45
 
@@ -89,6 +92,8 @@ export const COMMANDS = Object.freeze({
89
92
  profile: STRING,
90
93
  'user-data-dir': STRING,
91
94
  'target-browser': STRING,
95
+ 'password-csv': STRING,
96
+ 'include-payment-cards': FLAG,
92
97
  to: STRING,
93
98
  include: STRING,
94
99
  domain: STRINGS,
@@ -248,6 +253,8 @@ export function launchParams(options) {
248
253
  localState: settings(options.localState, '--local-state'),
249
254
  defaultBrowserCheck: options.defaultBrowserCheck,
250
255
  firstRun: options.firstRun,
256
+ driverPath: options.driverPath,
257
+ bidi: options.bidi,
251
258
  };
252
259
  return Object.fromEntries(
253
260
  Object.entries(params).filter(([, value]) => value !== undefined)
@@ -75,12 +75,16 @@ async function withPageSession(parsed, io, work, { skipUrl = false } = {}) {
75
75
  const { options } = parsed;
76
76
  const dispatcher = createDispatcher({ dependencies: io.dependencies });
77
77
  try {
78
- const opened = options.cdpEndpoint
79
- ? await dispatcher.dispatch('session.connect', {
80
- cdpEndpoint: options.cdpEndpoint,
81
- engine: options.engine,
82
- })
83
- : await dispatcher.dispatch('session.launch', launchParams(options));
78
+ const opened =
79
+ options.cdpEndpoint || options.serverUrl
80
+ ? await dispatcher.dispatch('session.connect', {
81
+ cdpEndpoint: options.cdpEndpoint,
82
+ engine: options.engine,
83
+ ...(options.serverUrl ? { serverUrl: options.serverUrl } : {}),
84
+ ...(options.driverPath ? { driverPath: options.driverPath } : {}),
85
+ ...(options.bidi !== undefined ? { bidi: options.bidi } : {}),
86
+ })
87
+ : await dispatcher.dispatch('session.launch', launchParams(options));
84
88
  if (options.url && !skipUrl) {
85
89
  await dispatcher.dispatch('page.goto', {
86
90
  session: opened.session,
@@ -212,6 +216,8 @@ async function profileMigrate(parsed, io) {
212
216
  profile: options.profile,
213
217
  userDataDir: options.userDataDir,
214
218
  targetBrowser: options.targetBrowser,
219
+ passwordCsv: options.passwordCsv,
220
+ includePaymentCards: options.includePaymentCards,
215
221
  to: options.to,
216
222
  include: options.include
217
223
  ?.split(',')
@@ -9,7 +9,7 @@
9
9
  import { decodeValue, describeObject, encodeValue } from './handles.js';
10
10
  import { invalidParams, requireString } from './rpc-error.js';
11
11
 
12
- const ENGINE_ROOTS = Object.freeze(['playwright', 'puppeteer']);
12
+ const ENGINE_ROOTS = Object.freeze(['playwright', 'puppeteer', 'selenium']);
13
13
 
14
14
  function targetOf(state, params) {
15
15
  const handle = params.handle;
@@ -36,11 +36,14 @@ async function root(state, params) {
36
36
  browser: handleOrNull(state, session.browser),
37
37
  context: handleOrNull(state, session.context),
38
38
  page: handleOrNull(state, session.page),
39
+ ...(session.engine === 'selenium'
40
+ ? { driver: handleOrNull(state, session.driver) }
41
+ : {}),
39
42
  };
40
43
  }
41
44
  if (!ENGINE_ROOTS.includes(name)) {
42
45
  throw invalidParams(
43
- `Unknown root "${name}"; expected playwright, puppeteer or session:<id>`
46
+ `Unknown root "${name}"; expected playwright, puppeteer, selenium or session:<id>`
44
47
  );
45
48
  }
46
49
  const module = await state.dependencies.loadEngine(name);
@@ -71,6 +74,19 @@ async function get(state, params) {
71
74
  );
72
75
  }
73
76
 
77
+ /** Instantiate an engine constructor obtained through `handle.get`. */
78
+ function construct(state, params) {
79
+ const target = targetOf(state, params);
80
+ const args = params.args ?? [];
81
+ if (typeof target !== 'function' || !Array.isArray(args)) {
82
+ throw invalidParams(
83
+ 'handle.construct requires a constructor handle and an args array'
84
+ );
85
+ }
86
+ const result = Reflect.construct(target, decodeValue(args, state.handles));
87
+ return encodeValue(result, state.handles);
88
+ }
89
+
74
90
  function dispose(state, params) {
75
91
  const id =
76
92
  typeof params.handle === 'object' && params.handle
@@ -129,6 +145,7 @@ export const HANDLE_METHODS = Object.freeze({
129
145
  'handle.root': root,
130
146
  'handle.call': call,
131
147
  'handle.get': get,
148
+ 'handle.construct': construct,
132
149
  'handle.dispose': dispose,
133
150
  'handle.describe': describe,
134
151
  'events.subscribe': subscribe,
@@ -37,14 +37,27 @@ async function launchSession(state, params) {
37
37
  }
38
38
 
39
39
  async function connectSession(state, params) {
40
- const cdpEndpoint = requireString(params, 'cdpEndpoint');
41
40
  const engine = resolveEngine(params.engine);
41
+ const endpoint =
42
+ engine === 'selenium' && params.serverUrl !== undefined
43
+ ? {
44
+ serverUrl: requireString(params, 'serverUrl'),
45
+ capabilities: params.capabilities,
46
+ }
47
+ : { cdpEndpoint: requireString(params, 'cdpEndpoint') };
42
48
  const connected = await state.dependencies.connectBrowser({
43
49
  engine,
44
- cdpEndpoint,
50
+ ...endpoint,
51
+ ...(params.driverPath ? { driverPath: params.driverPath } : {}),
52
+ ...(params.bidi !== undefined ? { bidi: params.bidi } : {}),
45
53
  });
46
54
  const session = state.sessions.add(sessionFromConnect(engine, connected));
47
- return { session, cdpEndpoint };
55
+ return {
56
+ session,
57
+ ...(endpoint.serverUrl
58
+ ? { serverUrl: endpoint.serverUrl }
59
+ : { cdpEndpoint: endpoint.cdpEndpoint }),
60
+ };
48
61
  }
49
62
 
50
63
  async function closeSession(state, params) {
@@ -70,6 +83,10 @@ async function fill(state, params) {
70
83
  const { engine, page } = sessionOf(state, params);
71
84
  if (engine === 'playwright') {
72
85
  await page.fill(selector, value);
86
+ } else if (engine === 'selenium') {
87
+ const element = await page.waitForSelector(selector, { visible: true });
88
+ await element.clear();
89
+ await element.sendKeys(value);
73
90
  } else {
74
91
  await page.locator(selector).fill(value);
75
92
  }
@@ -203,6 +220,8 @@ function migrateProfile(state, params) {
203
220
  include: params.include,
204
221
  domains: params.domains,
205
222
  targetBrowser: params.targetBrowser,
223
+ passwordCsv: params.passwordCsv,
224
+ includePaymentCards: params.includePaymentCards,
206
225
  });
207
226
  }
208
227
 
@@ -70,7 +70,11 @@ export const DEFAULT_DEPENDENCIES = Object.freeze({
70
70
  (await import('../browser/browser-profiles.js')).listBrowserProfiles(
71
71
  options
72
72
  ),
73
- loadEngine: (name) => importOptional(name, `The ${name} engine`),
73
+ loadEngine: (name) =>
74
+ importOptional(
75
+ name === 'selenium' ? 'selenium-webdriver' : name,
76
+ `The ${name} engine`
77
+ ),
74
78
  openInUserBrowser: async (url) =>
75
79
  (
76
80
  await importOptional(
@@ -14,7 +14,7 @@ export const BROWSER_CHANNELS = Object.freeze({
14
14
  chromium: 'chromium',
15
15
  });
16
16
 
17
- const ENGINES = Object.freeze(['playwright', 'puppeteer']);
17
+ const ENGINES = Object.freeze(['playwright', 'puppeteer', 'selenium']);
18
18
 
19
19
  /**
20
20
  * Launch options that are forwarded to `launchBrowser()` as they are, so the
@@ -38,6 +38,8 @@ const LAUNCH_PASSTHROUGH = Object.freeze([
38
38
  'localState',
39
39
  'defaultBrowserCheck',
40
40
  'firstRun',
41
+ 'driverPath',
42
+ 'bidi',
41
43
  ]);
42
44
 
43
45
  /** Validate an engine name, defaulting to Playwright. */
@@ -85,13 +87,17 @@ export function buildLaunchOptions(params = {}) {
85
87
  engine: resolveEngine(params.engine),
86
88
  headless: params.headless === true,
87
89
  };
88
- const channel = resolveChannel(params.browser);
90
+ const channel =
91
+ options.engine === 'selenium' ? undefined : resolveChannel(params.browser);
89
92
  const defined = {
90
93
  launch: params.launch,
91
94
  executablePath: params.executablePath,
92
95
  userDataDir: params.userDataDir,
93
96
  restrictions: params.restrictions,
94
97
  channel,
98
+ ...(options.engine === 'selenium' && params.browser !== undefined
99
+ ? { browser: params.browser }
100
+ : {}),
95
101
  };
96
102
  for (const name of LAUNCH_PASSTHROUGH) {
97
103
  defined[name] ??= params[name];
@@ -121,6 +127,7 @@ export function sessionFromLaunch(engine, launched) {
121
127
  : handle,
122
128
  context: playwright ? handle : (handle?.defaultBrowserContext?.() ?? null),
123
129
  page: launched.page,
130
+ driver: launched.driver ?? (engine === 'selenium' ? handle : null),
124
131
  args: launched.args ?? [],
125
132
  close: () => launched.close(),
126
133
  connected: false,
@@ -141,10 +148,15 @@ export function sessionFromConnect(engine, connected) {
141
148
  ? (page?.context?.() ?? null)
142
149
  : (browser?.defaultBrowserContext?.() ?? null),
143
150
  page,
151
+ driver: connected.driver ?? null,
144
152
  // Playwright's close() on a connectOverCDP browser disconnects;
145
153
  // Puppeteer's close() would quit the browser, disconnect() does not.
146
154
  close: async () => {
147
155
  await connected.downloads?.dispose?.();
156
+ if (engine === 'selenium') {
157
+ await connected.close();
158
+ return;
159
+ }
148
160
  await (playwright ? browser.close() : browser.disconnect());
149
161
  },
150
162
  connected: true,
@@ -57,6 +57,7 @@ export {
57
57
  export const SUPPORTED_BROWSER_TEST_ENGINES = Object.freeze([
58
58
  'playwright',
59
59
  'puppeteer',
60
+ 'selenium',
60
61
  ]);
61
62
 
62
63
  export const DEFAULT_TIMINGS_FILENAME = '.browser-commander-test-timings.json';