ui5-test-runner 6.0.0-beta.5 → 6.0.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/README.md +20 -29
- package/dist/Command.js +52 -0
- package/dist/Npm.js +154 -74
- package/dist/browsers/RecoverableBrowserError.js +6 -0
- package/dist/browsers/chromeArguments.js +5 -0
- package/dist/browsers/chromeOptions.js +29 -0
- package/dist/browsers/consoleMessage.js +23 -0
- package/dist/browsers/factory.js +168 -3
- package/dist/browsers/networkResponse.js +15 -0
- package/dist/browsers/playwright.js +104 -0
- package/dist/browsers/puppeteer.js +54 -88
- package/dist/browsers/seleniumWebdriver.js +124 -0
- package/dist/browsers/webdriverio.js +85 -0
- package/dist/cli.js +18 -12
- package/dist/configuration/CommandLine.js +46 -26
- package/dist/configuration/ConfigurationValidator.js +132 -18
- package/dist/configuration/OptionValidationError.js +50 -2
- package/dist/configuration/derivations.js +8 -0
- package/dist/configuration/options.js +380 -23
- package/dist/configuration/validations.js +135 -0
- package/dist/configuration/validators/boolean.js +1 -1
- package/dist/configuration/validators/enumeration.js +7 -0
- package/dist/configuration/validators/fsEntry.js +12 -12
- package/dist/configuration/validators/index.js +8 -2
- package/dist/configuration/validators/integer.js +7 -1
- package/dist/configuration/validators/json.js +23 -0
- package/dist/configuration/validators/libraryMapping.js +28 -0
- package/dist/configuration/validators/percent.js +2 -2
- package/dist/configuration/validators/regexp.js +2 -2
- package/dist/configuration/validators/secret.js +8 -0
- package/dist/configuration/validators/string.js +1 -1
- package/dist/configuration/validators/timeout.js +2 -2
- package/dist/configuration/validators/url.js +3 -3
- package/dist/end.js +58 -0
- package/dist/if.js +22 -0
- package/dist/modes/Modes.js +2 -1
- package/dist/modes/batch/BatchItem.js +1 -0
- package/dist/modes/batch/batchTask.js +159 -0
- package/dist/modes/batch/index.js +98 -0
- package/dist/modes/batch/output.js +22 -0
- package/dist/modes/batch/report.js +45 -0
- package/dist/modes/batch/resolve.js +105 -0
- package/dist/modes/dumpConfig.js +6 -0
- package/dist/modes/execute.js +6 -5
- package/dist/modes/help.js +86 -2
- package/dist/modes/log/LogReader.js +3 -2
- package/dist/modes/log/LogStorage.js +31 -27
- package/dist/modes/log/index.js +38 -6
- package/dist/modes/log/{REserve.js → reserve.js} +25 -2
- package/dist/modes/mcp/IDocument.js +2 -0
- package/dist/modes/mcp/frontmatter.js +142 -0
- package/dist/modes/mcp/index.js +49 -0
- package/dist/modes/mcp/knowledgeBase.js +104 -0
- package/dist/modes/mcp/remoteKnowledgeBase.js +79 -0
- package/dist/modes/mcp/reserve.js +175 -0
- package/dist/modes/mcp/tools/getTopic.js +46 -0
- package/dist/modes/mcp/tools/index.js +6 -0
- package/dist/modes/mcp/tools/listTopics.js +14 -0
- package/dist/modes/test/PageContext.js +1 -0
- package/dist/modes/test/Server.js +99 -0
- package/dist/modes/test/browser.js +31 -17
- package/dist/modes/test/browserConfig.js +17 -0
- package/dist/modes/test/coverage/collect.js +38 -0
- package/dist/modes/test/coverage/index.js +3 -0
- package/dist/modes/test/coverage/instrument.js +70 -0
- package/dist/modes/test/coverage/nyc.js +15 -0
- package/dist/modes/test/coverage/report.js +71 -0
- package/dist/modes/test/coverage/settings.js +53 -0
- package/dist/modes/test/index.js +94 -34
- package/dist/modes/test/pageTask.js +293 -98
- package/dist/modes/test/report.js +20 -3
- package/dist/modes/test/reserve.js +76 -0
- package/dist/modes/test/screenshot.js +49 -0
- package/dist/platform/Crypto.js +9 -0
- package/dist/platform/Exit.js +33 -24
- package/dist/platform/FileSystem.js +6 -2
- package/dist/platform/Host.js +9 -1
- package/dist/platform/Http.js +54 -29
- package/dist/platform/Module.js +2 -1
- package/dist/platform/Path.js +5 -1
- package/dist/platform/Process.js +67 -13
- package/dist/platform/Terminal.js +12 -8
- package/dist/platform/Thread.js +1 -1
- package/dist/platform/assert.js +13 -12
- package/dist/platform/environment.js +9 -18
- package/dist/platform/index.js +1 -0
- package/dist/platform/logger/CompressionContext.js +117 -0
- package/dist/platform/logger/allCompressed.js +8 -7
- package/dist/platform/logger/compress.js +29 -129
- package/dist/platform/logger/output/BaseLoggerOutput.js +49 -31
- package/dist/platform/logger/output/InteractiveLoggerOutput.js +1 -1
- package/dist/platform/logger/output/LlmLoggerOutput.js +29 -0
- package/dist/platform/logger/output/factory.js +5 -1
- package/dist/platform/logger/output.js +24 -6
- package/dist/platform/logger/toInternalLogAttributes.js +10 -11
- package/dist/platform/logger/traceFileName.js +1 -0
- package/dist/platform/logger.js +52 -21
- package/dist/platform/mock.js +110 -19
- package/dist/platform/version.js +1 -2
- package/dist/platform/workerBootstrap.js +1 -1
- package/dist/reports/initReportBuilder.js +18 -0
- package/dist/reports/junit.js +64 -0
- package/dist/reports/saveReport.js +10 -0
- package/dist/reports/textReport.js +92 -0
- package/dist/reserveLogger.js +22 -0
- package/dist/sendToParentProcess.js +6 -0
- package/dist/start.js +79 -0
- package/dist/types/ISecretValue.js +1 -0
- package/dist/types/SecretValue.js +15 -0
- package/dist/ui/agent.js +4 -2
- package/dist/ui/html-report.js +2 -2
- package/dist/ui/lib.js +1 -1
- package/dist/ui/log-viewer.js +1 -1
- package/dist/utils/node/Folder.js +2 -4
- package/dist/utils/node/FramedStreamReader.js +4 -5
- package/dist/utils/node/FramedStreamWriter.js +6 -5
- package/dist/utils/node/anonymize.js +11 -0
- package/dist/utils/node/timeout.js +11 -0
- package/dist/utils/shared/ProgressBar.js +1 -2
- package/dist/utils/shared/TestReportBuilder.js +11 -7
- package/dist/utils/shared/host.js +11 -0
- package/dist/utils/shared/object.js +12 -3
- package/dist/utils/shared/schema.js +20 -0
- package/dist/utils/shared/string.js +1 -0
- package/dist/utils/shared/toIError.js +3 -7
- package/dist/utils/shared/version.js +52 -0
- package/package.json +2 -78
- package/dist/configuration/validators/browser.js +0 -11
- package/dist/modes/test/REserve.js +0 -31
- package/dist/modes/test/server.js +0 -109
package/dist/browsers/factory.js
CHANGED
|
@@ -1,9 +1,174 @@
|
|
|
1
|
-
import { factory as puppeteerFactory } from './puppeteer.js';
|
|
1
|
+
import { factory as puppeteerFactory, descriptor as puppeteerDescriptor } from './puppeteer.js';
|
|
2
|
+
import { factory as playwrightFactory, descriptor as playwrightDescriptor } from './playwright.js';
|
|
3
|
+
import { factory as webdriverioFactory, descriptor as webdriverioDescriptor } from './webdriverio.js';
|
|
4
|
+
import { factory as seleniumWebdriverFactory, descriptor as seleniumWebdriverDescriptor } from './seleniumWebdriver.js';
|
|
5
|
+
import { Exit, logger } from '../platform/index.js';
|
|
6
|
+
import { RecoverableBrowserError } from './RecoverableBrowserError.js';
|
|
7
|
+
const DRIVERS = ['puppeteer', 'playwright', 'webdriverio', 'selenium-webdriver'];
|
|
8
|
+
export const isDriver = (value) => DRIVERS.includes(value);
|
|
2
9
|
const factories = {
|
|
3
|
-
puppeteer: puppeteerFactory
|
|
10
|
+
puppeteer: puppeteerFactory,
|
|
11
|
+
playwright: playwrightFactory,
|
|
12
|
+
webdriverio: webdriverioFactory,
|
|
13
|
+
'selenium-webdriver': seleniumWebdriverFactory
|
|
14
|
+
};
|
|
15
|
+
const descriptors = {
|
|
16
|
+
puppeteer: puppeteerDescriptor,
|
|
17
|
+
playwright: playwrightDescriptor,
|
|
18
|
+
webdriverio: webdriverioDescriptor,
|
|
19
|
+
'selenium-webdriver': seleniumWebdriverDescriptor
|
|
20
|
+
};
|
|
21
|
+
export const getDescriptor = (driver) => descriptors[driver];
|
|
22
|
+
let _instanceCount = 0;
|
|
23
|
+
const _activeInstances = new Set();
|
|
24
|
+
const shutdown = async (browser, inner) => {
|
|
25
|
+
logger.debug({ source: browser, message: 'shutdown' });
|
|
26
|
+
try {
|
|
27
|
+
await inner.shutdown();
|
|
28
|
+
logger.debug({ source: browser, message: 'shutdown completed' });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
logger.warn({
|
|
32
|
+
source: browser,
|
|
33
|
+
message: 'shutdown failed',
|
|
34
|
+
error
|
|
35
|
+
});
|
|
36
|
+
}
|
|
4
37
|
};
|
|
5
38
|
export const BrowserFactory = {
|
|
6
39
|
async build(configuration, browser) {
|
|
7
|
-
|
|
40
|
+
if (_activeInstances.has(browser)) {
|
|
41
|
+
logger.fatal({ source: browser, message: 'build failed: already has an active instance' });
|
|
42
|
+
}
|
|
43
|
+
_activeInstances.add(browser);
|
|
44
|
+
const abortController = new AbortController();
|
|
45
|
+
let inner;
|
|
46
|
+
try {
|
|
47
|
+
logger.debug({ source: browser, message: 'build' });
|
|
48
|
+
inner = await factories[browser](configuration, abortController.signal);
|
|
49
|
+
logger.debug({ source: browser, message: 'build completed' });
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
_activeInstances.delete(browser);
|
|
53
|
+
logger.fatal({ source: browser, message: 'build failed', error });
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
const _openWindows = new Set();
|
|
57
|
+
const closeAllWindows = async () => {
|
|
58
|
+
await Promise.all([..._openWindows].map(async (w) => {
|
|
59
|
+
try {
|
|
60
|
+
await w.close();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
}
|
|
64
|
+
}));
|
|
65
|
+
};
|
|
66
|
+
let task;
|
|
67
|
+
return {
|
|
68
|
+
async setup(settings) {
|
|
69
|
+
task = Exit.registerAsyncTask({
|
|
70
|
+
name: `${browser}#${++_instanceCount}`,
|
|
71
|
+
async stop() {
|
|
72
|
+
abortController.abort();
|
|
73
|
+
await closeAllWindows();
|
|
74
|
+
await shutdown(browser, inner);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
logger.debug({ source: browser, message: 'setup', data: settings });
|
|
78
|
+
try {
|
|
79
|
+
const capabilities = await inner.setup(settings);
|
|
80
|
+
logger.debug({ source: browser, message: 'setup completed', data: capabilities });
|
|
81
|
+
return capabilities;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
task[Symbol.dispose]();
|
|
85
|
+
_activeInstances.delete(browser);
|
|
86
|
+
logger.fatal({
|
|
87
|
+
source: browser,
|
|
88
|
+
message: 'setup failed',
|
|
89
|
+
error
|
|
90
|
+
});
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
async newWindow(settings) {
|
|
95
|
+
const { pageId } = settings;
|
|
96
|
+
logger.debug({ source: browser, message: 'newWindow', pageId, data: settings });
|
|
97
|
+
try {
|
|
98
|
+
const innerWindow = await inner.newWindow(settings);
|
|
99
|
+
logger.debug({ source: browser, message: 'newWindow completed', pageId });
|
|
100
|
+
const wrapped = wrapWindow(innerWindow, browser, pageId, () => _openWindows.delete(wrapped));
|
|
101
|
+
_openWindows.add(wrapped);
|
|
102
|
+
return wrapped;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const logMethod = error instanceof RecoverableBrowserError ? 'error' : 'fatal';
|
|
106
|
+
logger[logMethod]({
|
|
107
|
+
source: browser,
|
|
108
|
+
message: 'newWindow failed',
|
|
109
|
+
error
|
|
110
|
+
});
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
async shutdown() {
|
|
115
|
+
await closeAllWindows();
|
|
116
|
+
await shutdown(browser, inner);
|
|
117
|
+
task?.[Symbol.dispose]();
|
|
118
|
+
_activeInstances.delete(browser);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
8
121
|
}
|
|
9
122
|
};
|
|
123
|
+
const wrapWindow = (innerWindow, source, pageId, onClosed) => {
|
|
124
|
+
let isClosed = false;
|
|
125
|
+
return {
|
|
126
|
+
async eval(script) {
|
|
127
|
+
logger.debug({ source, message: 'eval', pageId, data: { script } });
|
|
128
|
+
try {
|
|
129
|
+
if (isClosed) {
|
|
130
|
+
throw new Error('window closed');
|
|
131
|
+
}
|
|
132
|
+
const result = await innerWindow.eval(script);
|
|
133
|
+
logger.debug({ source, message: 'eval completed', pageId, data: { result } });
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
logger.error({ source, message: 'eval failed', pageId, error });
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
async screenshot(path) {
|
|
142
|
+
logger.debug({ source, message: 'screenshot', pageId, data: { path } });
|
|
143
|
+
try {
|
|
144
|
+
if (isClosed) {
|
|
145
|
+
throw new Error('window closed');
|
|
146
|
+
}
|
|
147
|
+
await innerWindow.screenshot(path);
|
|
148
|
+
logger.debug({ source, message: 'screenshot completed', pageId, data: { path } });
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
logger.error({ source, message: 'screenshot failed', pageId, error, data: { path } });
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
async close() {
|
|
156
|
+
if (isClosed) {
|
|
157
|
+
logger.warn({ source, message: 'closing an already closed window, ignored', pageId });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
isClosed = true;
|
|
161
|
+
logger.debug({ source, message: 'window close', pageId });
|
|
162
|
+
try {
|
|
163
|
+
await innerWindow.close();
|
|
164
|
+
logger.debug({ source, message: 'window closed', pageId });
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
logger.error({ source, message: 'window close failed', error });
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
onClosed();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { logger } from '../platform/index.js';
|
|
2
|
+
const LOG_TYPES = [null, null, null, null, 'warn', 'error'];
|
|
3
|
+
export const handleNetworkResponse = (pageId, url, method, status, requestHeaders, responseHeaders) => {
|
|
4
|
+
const statusType = Math.floor(status / 100);
|
|
5
|
+
const logType = LOG_TYPES[statusType] ?? 'info';
|
|
6
|
+
logger[logType]({
|
|
7
|
+
source: 'browser/network',
|
|
8
|
+
message: url,
|
|
9
|
+
pageId,
|
|
10
|
+
data: {
|
|
11
|
+
request: { method, headers: requestHeaders },
|
|
12
|
+
response: { status, headers: responseHeaders }
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { logger, Process, assert } from '../platform/index.js';
|
|
2
|
+
import { Npm } from '../Npm.js';
|
|
3
|
+
import { handleConsoleMessage } from './consoleMessage.js';
|
|
4
|
+
import { handleNetworkResponse } from './networkResponse.js';
|
|
5
|
+
import { buildChromeLaunchOptions } from './chromeOptions.js';
|
|
6
|
+
export const descriptor = {
|
|
7
|
+
supportedBrowsers: ['chromium', 'firefox', 'webkit'],
|
|
8
|
+
defaultBrowser: 'chromium',
|
|
9
|
+
screenshotFormat: '.png'
|
|
10
|
+
};
|
|
11
|
+
export const factory = async (configuration, signal) => {
|
|
12
|
+
const playwright = await Npm.import(configuration, 'playwright');
|
|
13
|
+
const { chromium } = playwright;
|
|
14
|
+
let browser;
|
|
15
|
+
let context;
|
|
16
|
+
let isFirstWindow = true;
|
|
17
|
+
const launchAndInstallIfNeeded = async (settings) => {
|
|
18
|
+
const launchOptions = {
|
|
19
|
+
headless: !settings.visible,
|
|
20
|
+
handleSIGINT: false,
|
|
21
|
+
...buildChromeLaunchOptions(settings.options)
|
|
22
|
+
};
|
|
23
|
+
logger.debug({ source: 'playwright', message: 'launching browser' });
|
|
24
|
+
try {
|
|
25
|
+
browser = await chromium.launch(launchOptions);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof Error && error.message.includes('Executable doesn')) {
|
|
29
|
+
logger.info({
|
|
30
|
+
source: 'progress',
|
|
31
|
+
message: 'installing chromium (playwright)',
|
|
32
|
+
pageId: undefined,
|
|
33
|
+
data: {
|
|
34
|
+
value: 1,
|
|
35
|
+
max: 0
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
await Process.spawn('npx', 'playwright install chromium'.split(' '), {
|
|
39
|
+
shell: true,
|
|
40
|
+
signal
|
|
41
|
+
}).closed;
|
|
42
|
+
browser = await chromium.launch(launchOptions);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
context = await browser.newContext({
|
|
49
|
+
viewport: settings.viewport ?? null,
|
|
50
|
+
httpCredentials: settings.basicAuthentication
|
|
51
|
+
? { username: settings.basicAuthentication.username, password: settings.basicAuthentication.password.reveal() }
|
|
52
|
+
: undefined
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
browserName: browser.browserType().name(),
|
|
56
|
+
browserVersion: browser.version()
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
async setup(settings) {
|
|
61
|
+
return await launchAndInstallIfNeeded(settings);
|
|
62
|
+
},
|
|
63
|
+
async newWindow(settings) {
|
|
64
|
+
let page;
|
|
65
|
+
if (isFirstWindow) {
|
|
66
|
+
isFirstWindow = false;
|
|
67
|
+
const existingPages = context?.pages();
|
|
68
|
+
page = existingPages?.[0] ?? (await context?.newPage());
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
page = await context?.newPage();
|
|
72
|
+
}
|
|
73
|
+
assert(page !== undefined);
|
|
74
|
+
for (const script of settings.scripts) {
|
|
75
|
+
await page.addInitScript(script);
|
|
76
|
+
}
|
|
77
|
+
const { pageId } = settings;
|
|
78
|
+
page
|
|
79
|
+
.on('console', (message) => {
|
|
80
|
+
handleConsoleMessage(message.type(), message.text(), pageId);
|
|
81
|
+
})
|
|
82
|
+
.on('response', (response) => {
|
|
83
|
+
const request = response.request();
|
|
84
|
+
handleNetworkResponse(pageId, request.url(), request.method(), response.status(), request.headers(), response.headers());
|
|
85
|
+
});
|
|
86
|
+
await page.goto(settings.url);
|
|
87
|
+
return {
|
|
88
|
+
async eval(script) {
|
|
89
|
+
return await page?.evaluate(script);
|
|
90
|
+
},
|
|
91
|
+
async screenshot(path) {
|
|
92
|
+
await page.screenshot({ path });
|
|
93
|
+
},
|
|
94
|
+
async close() {
|
|
95
|
+
await page.goto('about:blank');
|
|
96
|
+
await page.close();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
async shutdown() {
|
|
101
|
+
await browser?.close();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
};
|
|
@@ -1,43 +1,51 @@
|
|
|
1
|
-
import { logger,
|
|
1
|
+
import { logger, Process } from '../platform/index.js';
|
|
2
2
|
import { Npm } from '../Npm.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { handleConsoleMessage } from './consoleMessage.js';
|
|
4
|
+
import { handleNetworkResponse } from './networkResponse.js';
|
|
5
|
+
import { buildChromeLaunchOptions } from './chromeOptions.js';
|
|
6
|
+
import { RecoverableBrowserError } from './RecoverableBrowserError.js';
|
|
7
|
+
export const descriptor = {
|
|
8
|
+
supportedBrowsers: ['chrome', 'firefox'],
|
|
9
|
+
defaultBrowser: 'chrome',
|
|
10
|
+
screenshotFormat: '.png'
|
|
11
|
+
};
|
|
12
|
+
export const factory = async (configuration, signal) => {
|
|
5
13
|
const puppeteer = await Npm.import(configuration, 'puppeteer');
|
|
6
|
-
const
|
|
14
|
+
const launch = puppeteer.launch;
|
|
7
15
|
let browser;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
name: 'puppeteer',
|
|
12
|
-
async stop() {
|
|
13
|
-
abortController.abort();
|
|
14
|
-
await browser?.close();
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
let openedPages = 0;
|
|
16
|
+
let isFirstWindow = true;
|
|
17
|
+
let basicAuthentication;
|
|
18
|
+
let deviceName;
|
|
18
19
|
const launchAndInstallIfNeeded = async (settings) => {
|
|
20
|
+
const target = settings.browser ?? 'chrome';
|
|
21
|
+
const arguments_ = [
|
|
22
|
+
settings.viewport ? `--window-size=${settings.viewport.width},${settings.viewport.height}` : '--start-maximized'
|
|
23
|
+
];
|
|
24
|
+
const chromeLaunchOptions = buildChromeLaunchOptions(settings.options, arguments_, target === 'chrome');
|
|
19
25
|
const launchOptions = {
|
|
26
|
+
browser: target,
|
|
20
27
|
headless: !settings.visible,
|
|
21
28
|
defaultViewport: null,
|
|
22
29
|
handleSIGINT: false,
|
|
23
30
|
signal,
|
|
24
|
-
|
|
31
|
+
...chromeLaunchOptions
|
|
25
32
|
};
|
|
33
|
+
logger.debug({ source: 'puppeteer', message: 'launching browser' });
|
|
26
34
|
try {
|
|
27
35
|
browser = await launch(launchOptions);
|
|
28
36
|
}
|
|
29
37
|
catch (error) {
|
|
30
|
-
if (error instanceof Error && error.message.startsWith('Could not find
|
|
38
|
+
if (error instanceof Error && error.message.startsWith('Could not find')) {
|
|
31
39
|
logger.info({
|
|
32
40
|
source: 'progress',
|
|
33
|
-
message:
|
|
41
|
+
message: `Installing ${target} (puppeteer)`,
|
|
34
42
|
pageId: undefined,
|
|
35
43
|
data: {
|
|
36
44
|
value: 1,
|
|
37
45
|
max: 0
|
|
38
46
|
}
|
|
39
47
|
});
|
|
40
|
-
await Process.spawn('npx',
|
|
48
|
+
await Process.spawn('npx', `puppeteer browsers install ${target}`.split(' '), {
|
|
41
49
|
shell: true,
|
|
42
50
|
signal
|
|
43
51
|
}).closed;
|
|
@@ -47,27 +55,22 @@ export const factory = async (configuration) => {
|
|
|
47
55
|
throw error;
|
|
48
56
|
}
|
|
49
57
|
}
|
|
50
|
-
logger.debug({ source: 'puppeteer', message: 'setup completed' });
|
|
51
58
|
return {
|
|
52
|
-
|
|
59
|
+
browserName: target,
|
|
60
|
+
browserVersion: await browser.version()
|
|
53
61
|
};
|
|
54
62
|
};
|
|
55
63
|
return {
|
|
56
64
|
async setup(settings) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
logger.error({ source: 'puppeteer', message: 'setup failed', error });
|
|
63
|
-
task[Symbol.dispose]();
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
65
|
+
basicAuthentication = settings.basicAuthentication;
|
|
66
|
+
deviceName = settings.device;
|
|
67
|
+
return await launchAndInstallIfNeeded(settings);
|
|
66
68
|
},
|
|
67
69
|
async newWindow(settings) {
|
|
68
|
-
|
|
70
|
+
const { pageId } = settings;
|
|
69
71
|
let page;
|
|
70
|
-
if (
|
|
72
|
+
if (isFirstWindow) {
|
|
73
|
+
isFirstWindow = false;
|
|
71
74
|
const pages = await browser?.pages(true);
|
|
72
75
|
page = pages?.[0];
|
|
73
76
|
}
|
|
@@ -76,83 +79,46 @@ export const factory = async (configuration) => {
|
|
|
76
79
|
type: 'window'
|
|
77
80
|
});
|
|
78
81
|
}
|
|
82
|
+
if (deviceName) {
|
|
83
|
+
const knownDevices = puppeteer.KnownDevices ?? {};
|
|
84
|
+
const device = knownDevices[deviceName];
|
|
85
|
+
if (!device) {
|
|
86
|
+
throw new RecoverableBrowserError(`Unknown device "${deviceName}", available devices: ${Object.keys(knownDevices).join(', ')}`);
|
|
87
|
+
}
|
|
88
|
+
await page?.emulate(device);
|
|
89
|
+
}
|
|
79
90
|
for (const script of settings.scripts) {
|
|
80
91
|
await page?.evaluateOnNewDocument(script);
|
|
81
92
|
}
|
|
82
|
-
const { pageId } = settings;
|
|
83
93
|
page
|
|
84
94
|
?.on('console', (message) => {
|
|
85
|
-
|
|
86
|
-
error: 'error',
|
|
87
|
-
warn: 'warn',
|
|
88
|
-
debug: 'debug'
|
|
89
|
-
};
|
|
90
|
-
const logType = LOG_TYPES[message.type()] ?? 'info';
|
|
91
|
-
let source;
|
|
92
|
-
let messageText = message.text();
|
|
93
|
-
if (messageText.startsWith(agentLogPrefix)) {
|
|
94
|
-
source = 'browser/agent';
|
|
95
|
-
messageText = messageText.slice(agentLogPrefix.length);
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
source = 'browser/console';
|
|
99
|
-
}
|
|
100
|
-
logger[logType]({
|
|
101
|
-
source,
|
|
102
|
-
message: messageText,
|
|
103
|
-
pageId,
|
|
104
|
-
data: { type: message.type() }
|
|
105
|
-
});
|
|
95
|
+
handleConsoleMessage(message.type(), message.text(), pageId);
|
|
106
96
|
})
|
|
107
97
|
?.on('response', (response) => {
|
|
108
98
|
const request = response.request();
|
|
109
|
-
|
|
110
|
-
const LOG_TYPES = [null, null, null, null, 'warn', 'error'];
|
|
111
|
-
const logType = LOG_TYPES[statusType] ?? 'info';
|
|
112
|
-
logger[logType]({
|
|
113
|
-
source: 'browser/network',
|
|
114
|
-
message: request.url(),
|
|
115
|
-
pageId,
|
|
116
|
-
data: {
|
|
117
|
-
request: {
|
|
118
|
-
method: request.method(),
|
|
119
|
-
headers: request.headers()
|
|
120
|
-
},
|
|
121
|
-
response: {
|
|
122
|
-
status: response.status(),
|
|
123
|
-
headers: response.headers()
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
});
|
|
99
|
+
handleNetworkResponse(pageId, request.url(), request.method(), response.status(), request.headers(), response.headers());
|
|
127
100
|
});
|
|
101
|
+
if (basicAuthentication) {
|
|
102
|
+
await page?.authenticate({
|
|
103
|
+
username: basicAuthentication.username,
|
|
104
|
+
password: basicAuthentication.password.reveal()
|
|
105
|
+
});
|
|
106
|
+
}
|
|
128
107
|
await page?.goto(settings.url);
|
|
129
|
-
logger.debug({ source: 'puppeteer', message: 'newWindow completed', data: settings });
|
|
130
108
|
return {
|
|
131
109
|
async eval(script) {
|
|
132
110
|
return await page?.evaluate(script);
|
|
133
111
|
},
|
|
134
|
-
screenshot() {
|
|
135
|
-
|
|
112
|
+
async screenshot(path) {
|
|
113
|
+
await page?.screenshot({ path });
|
|
136
114
|
},
|
|
137
115
|
async close() {
|
|
138
|
-
|
|
139
|
-
await page?.close();
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
logger.error({ source: 'puppeteer', message: 'page.close failed', error });
|
|
143
|
-
}
|
|
116
|
+
await page?.close();
|
|
144
117
|
}
|
|
145
118
|
};
|
|
146
119
|
},
|
|
147
120
|
async shutdown() {
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
await browser?.close();
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
logger.error({ source: 'puppeteer', message: 'browser.close failed', error });
|
|
154
|
-
}
|
|
155
|
-
task[Symbol.dispose]();
|
|
121
|
+
await browser?.close();
|
|
156
122
|
}
|
|
157
123
|
};
|
|
158
124
|
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { logger } from '../platform/index.js';
|
|
2
|
+
import { Npm } from '../Npm.js';
|
|
3
|
+
import { handleConsoleMessage } from './consoleMessage.js';
|
|
4
|
+
import { handleNetworkResponse } from './networkResponse.js';
|
|
5
|
+
import { buildChromeLaunchOptions } from './chromeOptions.js';
|
|
6
|
+
import { writeFile } from 'node:fs/promises';
|
|
7
|
+
import { RecoverableBrowserError } from './RecoverableBrowserError.js';
|
|
8
|
+
export const descriptor = {
|
|
9
|
+
supportedBrowsers: ['chrome', 'firefox', 'edge', 'safari'],
|
|
10
|
+
defaultBrowser: 'chrome',
|
|
11
|
+
screenshotFormat: '.png'
|
|
12
|
+
};
|
|
13
|
+
const headersToObject = (headers) => Object.fromEntries(headers.map(({ name, value }) => [name.toLowerCase(), value.value ?? '']));
|
|
14
|
+
const withApiTimeout = async (ms, promise) => {
|
|
15
|
+
let timer;
|
|
16
|
+
const timeout = new Promise((_, reject) => {
|
|
17
|
+
timer = setTimeout(() => reject(new RecoverableBrowserError(`Selenium WebDriver API call did not respond within ${ms}ms — the session may have expired`)), ms);
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
return await Promise.race([promise, timeout]);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
export const factory = async (configuration) => {
|
|
27
|
+
const selenium = await Npm.import(configuration, 'selenium-webdriver');
|
|
28
|
+
const { Builder: SeleniumBuilder, Browser } = selenium;
|
|
29
|
+
const { Options: ChromeOptions } = (await Npm.import(configuration, 'selenium-webdriver/chrome'));
|
|
30
|
+
const LogInspector = (await Npm.import(configuration, 'selenium-webdriver/bidi/logInspector')).default;
|
|
31
|
+
const BrowsingContext = (await Npm.import(configuration, 'selenium-webdriver/bidi/browsingContext')).default;
|
|
32
|
+
const ScriptManager = (await Npm.import(configuration, 'selenium-webdriver/bidi/scriptManager')).default;
|
|
33
|
+
const { CreateContextParameters: CreateContextParametersClass } = (await Npm.import(configuration, 'selenium-webdriver/bidi/createContextParameters'));
|
|
34
|
+
const { Network } = (await Npm.import(configuration, 'selenium-webdriver/bidi/network'));
|
|
35
|
+
let driver;
|
|
36
|
+
const contextToPageId = new Map();
|
|
37
|
+
let isFirstWindow = true;
|
|
38
|
+
return {
|
|
39
|
+
async setup(settings) {
|
|
40
|
+
logger.debug({ source: 'selenium-webdriver', message: 'launching browser' });
|
|
41
|
+
const options = new ChromeOptions();
|
|
42
|
+
const { args } = buildChromeLaunchOptions(settings.options, [
|
|
43
|
+
...(settings.visible ? [] : ['--headless']),
|
|
44
|
+
'--start-maximized',
|
|
45
|
+
...(settings.viewport ? [`--window-size=${settings.viewport.width},${settings.viewport.height}`] : [])
|
|
46
|
+
]);
|
|
47
|
+
options.addArguments(...args);
|
|
48
|
+
options.enableBidi();
|
|
49
|
+
driver = new SeleniumBuilder().forBrowser(Browser.CHROME).setChromeOptions(options).build();
|
|
50
|
+
const caps = await driver.getCapabilities();
|
|
51
|
+
const logInspector = await LogInspector(driver);
|
|
52
|
+
await logInspector.onConsoleEntry((entry) => {
|
|
53
|
+
const pageId = (entry.source.browsingContextId === null ? undefined : contextToPageId.get(entry.source.browsingContextId)) ??
|
|
54
|
+
-1;
|
|
55
|
+
handleConsoleMessage(entry.method, entry.text, pageId);
|
|
56
|
+
});
|
|
57
|
+
const network = await Network(driver);
|
|
58
|
+
await network.responseStarted((event) => {
|
|
59
|
+
if (event === null || !('response' in event))
|
|
60
|
+
return;
|
|
61
|
+
const pageId = (event.id === null ? undefined : contextToPageId.get(event.id)) ?? -1;
|
|
62
|
+
handleNetworkResponse(pageId, event.request.url, event.request.method, event.response.status, headersToObject(event.request.headers), headersToObject(event.response.headers));
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
browserName: caps.get('browserName') ?? 'chrome',
|
|
66
|
+
browserVersion: caps.get('browserVersion') ?? ''
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
async newWindow(settings) {
|
|
70
|
+
const { pageId, scripts, url } = settings;
|
|
71
|
+
const isFirst = isFirstWindow;
|
|
72
|
+
isFirstWindow = false;
|
|
73
|
+
let context;
|
|
74
|
+
if (isFirst) {
|
|
75
|
+
context = await driver.getWindowHandle();
|
|
76
|
+
contextToPageId.set(context, pageId);
|
|
77
|
+
for (const script of scripts) {
|
|
78
|
+
const sm = await ScriptManager(context, driver);
|
|
79
|
+
await sm.addPreloadScript(`() => { ${script} }`);
|
|
80
|
+
}
|
|
81
|
+
await driver.get(url);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const tabParameters = new CreateContextParametersClass().background(true);
|
|
85
|
+
const bc = await BrowsingContext(driver, { type: 'tab', createParameters: tabParameters });
|
|
86
|
+
context = bc.id;
|
|
87
|
+
contextToPageId.set(context, pageId);
|
|
88
|
+
for (const script of scripts) {
|
|
89
|
+
const sm = await ScriptManager(context, driver);
|
|
90
|
+
await sm.addPreloadScript(`() => { ${script} }`);
|
|
91
|
+
}
|
|
92
|
+
await bc.navigate(url, 'interactive');
|
|
93
|
+
}
|
|
94
|
+
const { seleniumApiTimeout } = configuration;
|
|
95
|
+
return {
|
|
96
|
+
async eval(script) {
|
|
97
|
+
return withApiTimeout(seleniumApiTimeout, (async () => {
|
|
98
|
+
await driver.switchTo().window(context);
|
|
99
|
+
return driver.executeScript(`return (${script})`);
|
|
100
|
+
})());
|
|
101
|
+
},
|
|
102
|
+
async screenshot(path) {
|
|
103
|
+
await withApiTimeout(seleniumApiTimeout, (async () => {
|
|
104
|
+
await driver.switchTo().window(context);
|
|
105
|
+
const data = await driver.takeScreenshot();
|
|
106
|
+
await writeFile(path, Buffer.from(data, 'base64'));
|
|
107
|
+
})());
|
|
108
|
+
},
|
|
109
|
+
async close() {
|
|
110
|
+
contextToPageId.delete(context);
|
|
111
|
+
if (contextToPageId.size === 0) {
|
|
112
|
+
await BrowsingContext(driver, { type: 'tab' });
|
|
113
|
+
}
|
|
114
|
+
const bc = await BrowsingContext(driver, { browsingContextId: context });
|
|
115
|
+
await bc.close();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
async shutdown() {
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
121
|
+
await driver?.quit();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
};
|