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.
- package/.jscpd-baseline.json +48 -30
- package/CHANGELOG.md +32 -0
- package/README.md +19 -7
- package/eslint.config.js +1 -1
- package/examples/react-test-app/package-lock.json +890 -0
- package/examples/react-test-app/package.json +4 -4
- package/package.json +20 -17
- package/src/browser/attach/snapshot.js +4 -28
- package/src/browser/browser-cookie-credentials.js +15 -8
- package/src/browser/browser-cookies.js +36 -30
- package/src/browser/browser-profile-files.js +31 -0
- package/src/browser/browser-profiles.js +11 -13
- package/src/browser/browser-sources.js +39 -9
- package/src/browser/browser-sources.json +600 -23
- package/src/browser/connector.js +46 -4
- package/src/browser/launcher.js +26 -1
- package/src/browser/migration/capabilities.json +117 -0
- package/src/browser/migration/chromium-history.sql +22 -0
- package/src/browser/migration/chromium-writers.js +85 -0
- package/src/browser/migration/cookies.js +4 -1
- package/src/browser/migration/data-classes.js +68 -0
- package/src/browser/migration/domains.js +32 -0
- package/src/browser/migration/firefox-history.js +106 -0
- package/src/browser/migration/firefox.js +39 -25
- package/src/browser/migration/history.js +161 -2
- package/src/browser/migration/index.js +229 -83
- package/src/browser/migration/os-crypt-keys.js +1 -5
- package/src/browser/migration/password-metadata.js +95 -0
- package/src/browser/migration/passwords.js +78 -41
- package/src/browser/migration/safari-import.js +100 -0
- package/src/browser/migration/safari.js +110 -0
- package/src/browser/migration/sqlite-snapshot.js +31 -65
- package/src/browser/migration/validation.js +48 -0
- package/src/browser/real-browser.js +21 -3
- package/src/browser/safari-access.js +45 -0
- package/src/browser/safari-cookies.js +39 -36
- package/src/browser/safari-profiles.js +83 -0
- package/src/browser/storage-state.js +27 -0
- package/src/browser/system-browser.js +31 -143
- package/src/browser/webdriver.js +18 -2
- package/src/cli/args.js +7 -0
- package/src/cli/commands.js +12 -6
- package/src/cli/handle-methods.js +19 -2
- package/src/cli/high-level-methods.js +22 -3
- package/src/cli/modules.js +5 -1
- package/src/cli/sessions.js +14 -2
- package/src/tests/index.js +1 -0
- package/tests/e2e/engine-matrix.e2e.test.js +107 -0
- package/tests/e2e/playwright.e2e.test.js +11 -17
- package/tests/e2e/puppeteer.e2e.test.js +13 -17
- package/tests/unit/browser/attach/snapshot.test.js +30 -0
- package/tests/unit/browser/browser-cookies.test.js +35 -0
- package/tests/unit/browser/catalogue-launch.test.js +93 -0
- package/tests/unit/browser/cookie-sources.test.js +92 -17
- package/tests/unit/browser/credential-catalogue.test.js +50 -0
- package/tests/unit/browser/firefox-cookie-expiry.test.js +79 -0
- package/tests/unit/browser/migration/data-classes.test.js +89 -0
- package/tests/unit/browser/migration/domain-isolation.test.js +212 -0
- package/tests/unit/browser/migration/firefox-history.test.js +130 -0
- package/tests/unit/browser/migration/history-metadata.test.js +74 -0
- package/tests/unit/browser/migration/index.test.js +16 -1
- package/tests/unit/browser/migration/opera-keys.test.js +22 -0
- package/tests/unit/browser/migration/safari-data.test.js +143 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +56 -1
- package/tests/unit/browser/migration/validation.test.js +63 -0
- package/tests/unit/browser/migration/yandex.test.js +84 -0
- package/tests/unit/browser/real-browser.test.js +19 -0
- package/tests/unit/browser/safari-cookies.test.js +26 -1
- package/tests/unit/browser/safari-profiles.test.js +111 -0
- package/tests/unit/cli/dispatcher.test.js +6 -2
- package/tests/unit/cli/main.test.js +5 -0
- package/tests/unit/cli/selenium-parity.test.js +186 -0
- package/tests/unit/scripts/browser-support.test.js +14 -7
- package/tests/unit/scripts/check-pipeline-status.test.js +33 -1
- package/tests/unit/scripts/migration-support.test.js +31 -0
- package/tests/unit/tests/index.test.js +2 -1
- 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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
return BROWSER_SOURCES.flatMap((browser) =>
|
|
49
|
+
resolveBrowserRoots(browser.id, {
|
|
50
|
+
platform,
|
|
47
51
|
homeDir,
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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. */
|
package/src/browser/webdriver.js
CHANGED
|
@@ -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 =
|
|
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)
|
package/src/cli/commands.js
CHANGED
|
@@ -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 =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
package/src/cli/modules.js
CHANGED
|
@@ -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) =>
|
|
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(
|
package/src/cli/sessions.js
CHANGED
|
@@ -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 =
|
|
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,
|