ui5-test-runner 6.0.0-beta.4 → 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 +193 -43
- 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 +169 -4
- package/dist/browsers/networkResponse.js +15 -0
- package/dist/browsers/playwright.js +104 -0
- package/dist/browsers/puppeteer.js +55 -89
- 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 +392 -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 +39 -7
- 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 +5 -0
- 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/Url.js +4 -0
- package/dist/platform/assert.js +13 -12
- package/dist/platform/environment.js +9 -18
- package/dist/platform/index.js +3 -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 +124 -27
- 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
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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) => {
|
|
13
|
+
const puppeteer = await Npm.import(configuration, 'puppeteer');
|
|
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 () => {
|
|
|
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 () => {
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { logger, Path } 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: ['chrome', 'firefox', 'edge', 'safari'],
|
|
8
|
+
defaultBrowser: 'chrome',
|
|
9
|
+
screenshotFormat: '.png'
|
|
10
|
+
};
|
|
11
|
+
const headerValue = (value) => typeof value === 'string' ? value : (value?.value ?? '');
|
|
12
|
+
const headersToObject = (headers) => Object.fromEntries((headers ?? []).map(({ name, value }) => [name.toLowerCase(), headerValue(value)]));
|
|
13
|
+
export const factory = async (configuration) => {
|
|
14
|
+
const webdriverio = await Npm.import(configuration, 'webdriverio');
|
|
15
|
+
const { remote } = webdriverio;
|
|
16
|
+
let browser;
|
|
17
|
+
const contextToPageId = new Map();
|
|
18
|
+
return {
|
|
19
|
+
async setup(settings) {
|
|
20
|
+
logger.debug({ source: 'webdriverio', message: 'launching browser' });
|
|
21
|
+
process.env['WDIO_LOG_PATH'] = Path.join(configuration.reportDir, 'wdio.log');
|
|
22
|
+
browser = await remote({
|
|
23
|
+
capabilities: {
|
|
24
|
+
browserName: 'chrome',
|
|
25
|
+
webSocketUrl: true,
|
|
26
|
+
'goog:chromeOptions': buildChromeLaunchOptions(settings.options, [
|
|
27
|
+
...(settings.visible ? [] : ['--headless']),
|
|
28
|
+
'--start-maximized',
|
|
29
|
+
...(settings.viewport ? [`--window-size=${settings.viewport.width},${settings.viewport.height}`] : [])
|
|
30
|
+
])
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
browser.on('log.entryAdded', (entry) => {
|
|
34
|
+
const event = entry;
|
|
35
|
+
if (event.type !== 'console') {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const pageId = (event.source?.context === undefined ? undefined : contextToPageId.get(event.source.context)) ?? -1;
|
|
39
|
+
handleConsoleMessage(event.method ?? 'log', event.text ?? '', pageId);
|
|
40
|
+
});
|
|
41
|
+
await browser.sessionSubscribe({ events: ['network.responseStarted'] });
|
|
42
|
+
browser.on('network.responseStarted', (entry) => {
|
|
43
|
+
const event = entry;
|
|
44
|
+
const pageId = (event.context === null ? undefined : contextToPageId.get(event.context)) ?? -1;
|
|
45
|
+
handleNetworkResponse(pageId, event.request.url, event.request.method, event.response.status, headersToObject(event.request.headers), headersToObject(event.response.headers));
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
browserName: browser.capabilities.browserName ?? 'chrome',
|
|
49
|
+
browserVersion: browser.capabilities.browserVersion ?? ''
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
async newWindow(settings) {
|
|
53
|
+
const { pageId, scripts, url } = settings;
|
|
54
|
+
const { context } = await browser.browsingContextCreate({ type: 'tab', background: true });
|
|
55
|
+
contextToPageId.set(context, pageId);
|
|
56
|
+
for (const script of scripts) {
|
|
57
|
+
await browser.scriptAddPreloadScript({
|
|
58
|
+
functionDeclaration: `() => { ${script} }`,
|
|
59
|
+
contexts: [context]
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
await browser.browsingContextNavigate({ context, url, wait: 'interactive' });
|
|
63
|
+
const switchTo = () => browser.switchToWindow(context);
|
|
64
|
+
return {
|
|
65
|
+
async eval(script) {
|
|
66
|
+
await switchTo();
|
|
67
|
+
return browser.execute(`return (${script})`);
|
|
68
|
+
},
|
|
69
|
+
async screenshot(path) {
|
|
70
|
+
await switchTo();
|
|
71
|
+
await browser.saveScreenshot(path);
|
|
72
|
+
},
|
|
73
|
+
async close() {
|
|
74
|
+
contextToPageId.delete(context);
|
|
75
|
+
await browser.browsingContextClose({ context });
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
async shutdown() {
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
81
|
+
await browser?.deleteSession();
|
|
82
|
+
delete process.env['WDIO_LOG_PATH'];
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -3,18 +3,24 @@ import { Exit, Host } from './platform/index.js';
|
|
|
3
3
|
import { version } from './platform/version.js';
|
|
4
4
|
import { CommandLine } from './configuration/CommandLine.js';
|
|
5
5
|
import { execute } from './modes/execute.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
await
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
import { isIfEvaluatedAsTrue } from './if.js';
|
|
7
|
+
import { sendToParentProcess } from './sendToParentProcess.js';
|
|
8
|
+
try {
|
|
9
|
+
const { name: cliName } = await version();
|
|
10
|
+
const indexOfCli = Host.argv.findIndex((value) => /[\\/]cli(\.[tj]s)?$/.exec(value) || value.endsWith(cliName));
|
|
11
|
+
const configuration = await CommandLine.buildConfigurationFrom(Host.cwd(), indexOfCli === -1 ? [] : Host.argv.slice(indexOfCli + 1));
|
|
12
|
+
if (await isIfEvaluatedAsTrue(configuration)) {
|
|
13
|
+
await execute(configuration);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.log(String.raw `/!\ Skipping execution (--if)`);
|
|
17
|
+
sendToParentProcess({ type: 'skip' });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
15
21
|
console.error(error);
|
|
16
22
|
Exit.code = -1;
|
|
17
|
-
}
|
|
18
|
-
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
19
25
|
await Exit.shutdown();
|
|
20
|
-
}
|
|
26
|
+
}
|