ui5-test-runner 6.0.0-beta.5 → 6.0.1
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 +87 -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 +3 -79
- package/dist/configuration/validators/browser.js +0 -11
- package/dist/modes/test/REserve.js +0 -31
- package/dist/modes/test/server.js +0 -109
|
@@ -1,145 +1,340 @@
|
|
|
1
|
-
import { assert, logger } from '../../platform/index.js';
|
|
1
|
+
import { assert, Http, logger, Process } from '../../platform/index.js';
|
|
2
2
|
import { getAgentSource } from './agent.js';
|
|
3
3
|
import { getBrowser } from './browser.js';
|
|
4
4
|
import { Exit, ExitShutdownError } from '../../platform/Exit.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { getReportBuilder } from './report.js';
|
|
6
|
+
import { createEmptyTestResults } from '../../types/CommonTestReportFormat.js';
|
|
7
|
+
import { getBrowserConfigScript } from './browserConfig.js';
|
|
8
|
+
import { collect as collectCoverage } from './coverage/index.js';
|
|
9
|
+
import { makeScreenshotHandlers } from './screenshot.js';
|
|
10
|
+
import { getEffectiveTimeout, isGloballyTimedOut } from '../../utils/node/timeout.js';
|
|
11
|
+
const getPageTimeout = (configuration, startTime) => getEffectiveTimeout(configuration.pageTimeout, configuration.globalTimeout, startTime);
|
|
7
12
|
let lastPageId = 0;
|
|
8
|
-
const
|
|
9
|
-
if (agentState.
|
|
13
|
+
export const agentStateMessage = (agentState) => {
|
|
14
|
+
if (agentState.type === 'suite')
|
|
15
|
+
return 'agent state: suite done';
|
|
16
|
+
if (agentState.type === 'QUnit') {
|
|
17
|
+
const kind = agentState.isOpa ? 'OPA' : 'QUnit';
|
|
18
|
+
const progress = `${agentState.executed}/${agentState.total}`;
|
|
19
|
+
return agentState.done ? `agent state: ${kind} done ${progress}` : `agent state: ${kind} ${progress}`;
|
|
20
|
+
}
|
|
21
|
+
return agentState.type === 'unknown' ? 'agent state: unknown' : 'agent state: loading';
|
|
22
|
+
};
|
|
23
|
+
const reportQunitProgress = (context, agentState, isScreenshotEnabled) => {
|
|
24
|
+
if (!isScreenshotEnabled && agentState.isOpa) {
|
|
10
25
|
context.loopDelay = 1000;
|
|
11
26
|
}
|
|
12
|
-
if (agentState.total
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
if (agentState.total <= 0) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const { executed, total, errors } = agentState;
|
|
31
|
+
if (executed === context.lastExecuted && total === context.lastTotal) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const type = agentState.isOpa ? 'opa' : 'qunit';
|
|
35
|
+
context.lastExecuted = executed;
|
|
36
|
+
context.lastTotal = total;
|
|
37
|
+
context.errors = errors;
|
|
38
|
+
context.type = type;
|
|
39
|
+
logger.info({
|
|
40
|
+
source: 'progress',
|
|
41
|
+
message: context.url,
|
|
42
|
+
pageId: context.pageId,
|
|
43
|
+
data: { max: agentState.total, value: agentState.executed, type, errors }
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
export const reportError = (url, message, error) => {
|
|
47
|
+
const result = createEmptyTestResults();
|
|
48
|
+
result.summary.tests = 1;
|
|
49
|
+
result.summary.failed = 1;
|
|
50
|
+
result.tests.push({
|
|
51
|
+
name: url,
|
|
52
|
+
status: 'failed',
|
|
53
|
+
message: `${message}, check the logs`,
|
|
54
|
+
trace: error?.stack,
|
|
55
|
+
duration: 0
|
|
56
|
+
});
|
|
57
|
+
getReportBuilder().merge(url, result);
|
|
58
|
+
};
|
|
59
|
+
const shouldUncaughtErrorsFail = (context, errors) => {
|
|
60
|
+
const { lastUncaughtErrorsCount, pageId } = context;
|
|
61
|
+
let errorsCount = 0;
|
|
62
|
+
let firstError;
|
|
63
|
+
if (lastUncaughtErrorsCount < errors.length) {
|
|
64
|
+
let index = lastUncaughtErrorsCount;
|
|
65
|
+
context.lastUncaughtErrorsCount = errors.length;
|
|
66
|
+
for (; index < errors.length; ++index) {
|
|
67
|
+
const error = errors[index];
|
|
68
|
+
let logMethod = 'error';
|
|
69
|
+
let message = 'uncaught error';
|
|
70
|
+
if (error.message === 'Called start() outside of a test context too many times' &&
|
|
71
|
+
(error.stack?.includes('/sap/ui/thirdparty/qunit-2.js') || error.stack?.includes('/sap/ui/thirdparty/qunit.js'))) {
|
|
72
|
+
logMethod = 'warn';
|
|
73
|
+
message = 'uncaught error (expected)';
|
|
74
|
+
}
|
|
75
|
+
logger[logMethod]({
|
|
76
|
+
source: 'page',
|
|
77
|
+
message,
|
|
78
|
+
pageId,
|
|
79
|
+
error
|
|
25
80
|
});
|
|
81
|
+
if (logMethod !== 'error') {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (firstError === undefined) {
|
|
85
|
+
firstError = error;
|
|
86
|
+
}
|
|
87
|
+
++errorsCount;
|
|
26
88
|
}
|
|
27
89
|
}
|
|
90
|
+
if (errorsCount !== 0) {
|
|
91
|
+
const errorDetail = `${firstError.name}: ${firstError.message}`;
|
|
92
|
+
reportError(context.url, `Uncaught error(s): ${errorsCount} (first: ${errorDetail})`, firstError);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
28
96
|
};
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
97
|
+
const shouldStopBasedOnAgentState = (context, isScreenshotEnabled, agentState) => {
|
|
98
|
+
logger.debug({
|
|
99
|
+
source: 'page',
|
|
100
|
+
message: agentStateMessage(agentState),
|
|
101
|
+
data: { state: agentState },
|
|
102
|
+
pageId: context.pageId
|
|
103
|
+
});
|
|
32
104
|
if (agentState.done) {
|
|
33
105
|
if (agentState.type === 'suite') {
|
|
34
|
-
|
|
35
|
-
|
|
106
|
+
const resolvedPages = agentState.pages.map((page) => new URL(page, context.url).href);
|
|
107
|
+
for (const pageUrl of resolvedPages) {
|
|
36
108
|
context.urls.push(pageUrl);
|
|
37
109
|
}
|
|
110
|
+
context.isSuite = true;
|
|
111
|
+
logger.debug({
|
|
112
|
+
source: 'page',
|
|
113
|
+
message: `suite: dispatching ${resolvedPages.length} pages`,
|
|
114
|
+
pageId: context.pageId,
|
|
115
|
+
data: { pages: resolvedPages }
|
|
116
|
+
});
|
|
38
117
|
}
|
|
39
118
|
else if (agentState.type === 'unknown') {
|
|
40
|
-
logger.
|
|
119
|
+
logger.error({
|
|
41
120
|
source: 'page',
|
|
42
|
-
message: '
|
|
121
|
+
message: 'unable to detect page type',
|
|
43
122
|
pageId: context.pageId,
|
|
44
123
|
data: { state: agentState }
|
|
45
124
|
});
|
|
46
|
-
|
|
125
|
+
assert(false, 'unable to detect page type');
|
|
47
126
|
}
|
|
48
127
|
else {
|
|
49
128
|
assert(agentState.type === 'QUnit');
|
|
50
|
-
reportQunitProgress(context, agentState);
|
|
129
|
+
reportQunitProgress(context, agentState, isScreenshotEnabled);
|
|
51
130
|
}
|
|
52
131
|
return true;
|
|
53
132
|
}
|
|
54
133
|
if (agentState.type === 'QUnit') {
|
|
55
|
-
reportQunitProgress(context, agentState);
|
|
134
|
+
reportQunitProgress(context, agentState, isScreenshotEnabled);
|
|
56
135
|
if (agentState.uncaughtErrors?.length) {
|
|
57
|
-
return
|
|
136
|
+
return shouldUncaughtErrorsFail(context, agentState.uncaughtErrors);
|
|
58
137
|
}
|
|
59
138
|
}
|
|
60
139
|
return false;
|
|
61
140
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
141
|
+
const tryToFetchThePageFirst = async (url, pageId, configuration) => {
|
|
142
|
+
try {
|
|
143
|
+
const headers = {};
|
|
144
|
+
const { browserBasicAuthUsername, browserBasicAuthPassword } = configuration;
|
|
145
|
+
if (browserBasicAuthUsername !== undefined && browserBasicAuthPassword !== undefined) {
|
|
146
|
+
const credentials = Buffer.from(`${browserBasicAuthUsername}:${browserBasicAuthPassword.reveal()}`).toString('base64');
|
|
147
|
+
headers['Authorization'] = `Basic ${credentials}`;
|
|
148
|
+
}
|
|
149
|
+
const response = await Http.fetch(url, { headers });
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
throw new Error(`Unexpected status code ${response.status}`);
|
|
152
|
+
}
|
|
153
|
+
return response;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
logger.info({
|
|
157
|
+
source: 'progress',
|
|
158
|
+
message: url,
|
|
159
|
+
pageId,
|
|
160
|
+
data: {
|
|
161
|
+
max: 0,
|
|
162
|
+
value: 1,
|
|
163
|
+
type: 'unknown',
|
|
164
|
+
errors: 1,
|
|
165
|
+
remove: true
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
reportError(url, 'An error occurred while fetching the URL');
|
|
169
|
+
throw new Error('An error occurred while fetching the URL', { cause: error });
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const mergeTestResults = (url, pageId, context, testResults) => {
|
|
173
|
+
if (context.isSuite) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const { passed, failed, tests, duration } = testResults.summary;
|
|
177
|
+
const durationString = duration === undefined ? '' : ` (${duration}ms)`;
|
|
178
|
+
logger.debug({
|
|
179
|
+
source: 'page',
|
|
180
|
+
message: `test results: passed=${passed} failed=${failed} tests=${tests}${durationString}`,
|
|
68
181
|
pageId,
|
|
69
|
-
data: {
|
|
182
|
+
data: { results: testResults }
|
|
70
183
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
184
|
+
getReportBuilder().merge(url, testResults, { pageId, type: context.type });
|
|
185
|
+
};
|
|
186
|
+
const runPollingLoop = async (context, handlePendingScreenshot, signal) => {
|
|
187
|
+
while (!signal.aborted) {
|
|
188
|
+
try {
|
|
189
|
+
await Process.sleep(context.loopDelay);
|
|
190
|
+
const agentState = (await context.page.eval("window['ui5-test-runner'].state"));
|
|
191
|
+
if (handlePendingScreenshot) {
|
|
192
|
+
await handlePendingScreenshot(context.page, agentState, context.pageId);
|
|
79
193
|
}
|
|
80
|
-
|
|
81
|
-
|
|
194
|
+
if (shouldStopBasedOnAgentState(context, handlePendingScreenshot !== false, agentState)) {
|
|
195
|
+
break;
|
|
82
196
|
}
|
|
83
197
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
198
|
+
catch (error) {
|
|
199
|
+
logger.error({ source: 'page', message: 'an error occurred', error, pageId: context.pageId, data: {} });
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
export const makePageTask = (configuration) => {
|
|
205
|
+
const { screenshot } = configuration;
|
|
206
|
+
const { handlePendingScreenshot, handleFailureScreenshot } = makeScreenshotHandlers(configuration);
|
|
207
|
+
const ui5VersionByOrigin = new Map();
|
|
208
|
+
const logUi5VersionOnce = async (probeUrl) => {
|
|
209
|
+
const origin = new URL(probeUrl).origin;
|
|
210
|
+
if (ui5VersionByOrigin.has(origin)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
ui5VersionByOrigin.set(origin, '');
|
|
214
|
+
const versionUrl = new URL('/resources/sap-ui-version.json', probeUrl).href;
|
|
215
|
+
try {
|
|
216
|
+
const ui5Version = JSON.parse(await Http.getAsText(versionUrl));
|
|
217
|
+
const { version: coreVersion } = ui5Version.libraries.find(({ name }) => name === 'sap.ui.core') ?? {
|
|
218
|
+
version: 'unknown'
|
|
219
|
+
};
|
|
220
|
+
ui5VersionByOrigin.set(origin, coreVersion);
|
|
221
|
+
logger.info({ source: 'job', message: `UI5 version ${coreVersion} (from ${origin})` });
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
return async function (url, _index, urls) {
|
|
227
|
+
const pageId = ++lastPageId;
|
|
228
|
+
logger.debug({ source: 'page', message: 'new page task', pageId, data: { url } });
|
|
229
|
+
logger.info({
|
|
230
|
+
source: 'progress',
|
|
231
|
+
message: url,
|
|
91
232
|
pageId,
|
|
92
|
-
|
|
93
|
-
url
|
|
233
|
+
data: { max: 0, value: 1, type: 'unknown', errors: 0 }
|
|
94
234
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
url,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
235
|
+
const startTime = getReportBuilder().report.results.summary.start;
|
|
236
|
+
if (isGloballyTimedOut(configuration.globalTimeout, startTime)) {
|
|
237
|
+
logger.warn({ source: 'page', message: 'global timeout reached, skipping page', pageId, data: { url } });
|
|
238
|
+
reportError(url, 'Global timeout reached');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
await tryToFetchThePageFirst(url, pageId, configuration);
|
|
242
|
+
await logUi5VersionOnce(url);
|
|
243
|
+
const { promise: taskStopped, resolve: setTaskAsStopped } = Promise.withResolvers();
|
|
244
|
+
const stopController = new AbortController();
|
|
245
|
+
using _ = Exit.registerAsyncTask({
|
|
246
|
+
name: url,
|
|
247
|
+
stop: async () => {
|
|
248
|
+
stopController.abort();
|
|
249
|
+
try {
|
|
250
|
+
this.stop(new ExitShutdownError());
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
await taskStopped;
|
|
111
256
|
}
|
|
112
257
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (context !== undefined) {
|
|
124
|
-
logger.info({
|
|
125
|
-
source: 'progress',
|
|
126
|
-
message: url,
|
|
258
|
+
});
|
|
259
|
+
let page;
|
|
260
|
+
let context;
|
|
261
|
+
try {
|
|
262
|
+
const pageTimeoutMs = getPageTimeout(configuration, startTime);
|
|
263
|
+
const agentSource = await getAgentSource();
|
|
264
|
+
const browserConfig = getBrowserConfigScript(pageId);
|
|
265
|
+
const scripts = [browserConfig, agentSource];
|
|
266
|
+
const browser = getBrowser();
|
|
267
|
+
page = await browser.newWindow({
|
|
127
268
|
pageId,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
value: context.lastExecuted,
|
|
131
|
-
type: context.type,
|
|
132
|
-
errors: context.errors,
|
|
133
|
-
remove: true
|
|
134
|
-
}
|
|
269
|
+
scripts,
|
|
270
|
+
url
|
|
135
271
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
272
|
+
context = {
|
|
273
|
+
pageId,
|
|
274
|
+
urls,
|
|
275
|
+
url,
|
|
276
|
+
page,
|
|
277
|
+
loopDelay: 250,
|
|
278
|
+
type: 'unknown',
|
|
279
|
+
lastExecuted: 0,
|
|
280
|
+
errors: 0,
|
|
281
|
+
lastTotal: 0,
|
|
282
|
+
isSuite: false,
|
|
283
|
+
lastUncaughtErrorsCount: 0
|
|
284
|
+
};
|
|
285
|
+
const pageTimeoutController = new AbortController();
|
|
286
|
+
let pageTimeoutHandle;
|
|
287
|
+
if (pageTimeoutMs > 0) {
|
|
288
|
+
pageTimeoutHandle = setTimeout(() => {
|
|
289
|
+
logger.warn({ source: 'page', message: 'page timed out', pageId, data: { url } });
|
|
290
|
+
pageTimeoutController.abort();
|
|
291
|
+
}, pageTimeoutMs);
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
await runPollingLoop(context, !!screenshot && handlePendingScreenshot, AbortSignal.any([pageTimeoutController.signal, stopController.signal]));
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
if (pageTimeoutHandle !== undefined) {
|
|
298
|
+
clearTimeout(pageTimeoutHandle);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const testResults = (await page.eval("window['ui5-test-runner'].results"));
|
|
302
|
+
await handleFailureScreenshot(page, pageId, testResults);
|
|
303
|
+
await collectCoverage(configuration, context);
|
|
304
|
+
mergeTestResults(url, pageId, context, testResults);
|
|
305
|
+
if (pageTimeoutController.signal.aborted) {
|
|
306
|
+
reportError(url, 'page timed out');
|
|
307
|
+
}
|
|
139
308
|
}
|
|
140
309
|
catch (error) {
|
|
141
|
-
logger.error({ source: 'page', message: 'page
|
|
310
|
+
logger.error({ source: 'page', message: 'unexpected error while running page', error, pageId, data: {} });
|
|
311
|
+
reportError(url, 'An unexpected error occurred');
|
|
142
312
|
}
|
|
143
|
-
|
|
144
|
-
|
|
313
|
+
finally {
|
|
314
|
+
if (context !== undefined) {
|
|
315
|
+
logger.info({
|
|
316
|
+
source: 'progress',
|
|
317
|
+
message: url,
|
|
318
|
+
pageId,
|
|
319
|
+
data: {
|
|
320
|
+
max: context.lastTotal,
|
|
321
|
+
value: context.lastExecuted,
|
|
322
|
+
type: context.type,
|
|
323
|
+
errors: context.errors,
|
|
324
|
+
remove: true
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (page) {
|
|
329
|
+
try {
|
|
330
|
+
logger.debug({ source: 'page', message: 'closing page', pageId });
|
|
331
|
+
await page.close();
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
logger.error({ source: 'page', message: 'page.close failed', error, pageId, data: {} });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
setTaskAsStopped();
|
|
338
|
+
}
|
|
339
|
+
};
|
|
145
340
|
};
|
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { initReportBuilder as buildReportBuilder } from '../../reports/initReportBuilder.js';
|
|
2
|
+
import { assert } from '../../platform/index.js';
|
|
3
|
+
let _reportBuilder;
|
|
4
|
+
export async function initReportBuilder(configuration) {
|
|
5
|
+
_reportBuilder = await buildReportBuilder(configuration);
|
|
6
|
+
}
|
|
7
|
+
export function getReportBuilder() {
|
|
8
|
+
assert(_reportBuilder !== undefined, 'reportBuilder not initialized');
|
|
9
|
+
return _reportBuilder;
|
|
10
|
+
}
|
|
11
|
+
export function setReportBrowserInfo(capabilities) {
|
|
12
|
+
const environment = getReportBuilder().report.results.environment;
|
|
13
|
+
if (environment) {
|
|
14
|
+
environment.extra = {
|
|
15
|
+
...environment.extra,
|
|
16
|
+
browserName: capabilities.browserName,
|
|
17
|
+
browserVersion: capabilities.browserVersion
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { FileSystem, logger, Path } from '../../platform/index.js';
|
|
2
|
+
const hasFolder = async (path) => {
|
|
3
|
+
try {
|
|
4
|
+
await FileSystem.stat(path);
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
export const buildREserveConfiguration = async (configuration) => {
|
|
12
|
+
const resourcesMatch = /\/((?:test-)?resources\/.*)/;
|
|
13
|
+
let { ui5 } = configuration;
|
|
14
|
+
if (!ui5.endsWith('/')) {
|
|
15
|
+
ui5 += '/';
|
|
16
|
+
}
|
|
17
|
+
const ui5Mapping = new URL('$1', ui5).href;
|
|
18
|
+
const webapp = Path.join(configuration.webapp, '.');
|
|
19
|
+
const coverageMappings = configuration.coverage
|
|
20
|
+
? [
|
|
21
|
+
{
|
|
22
|
+
match: /(.*\.js)(\?.*)?$/,
|
|
23
|
+
cwd: Path.join(configuration.coverageTempDir, 'instrumented'),
|
|
24
|
+
file: '$1'
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
: [];
|
|
28
|
+
const libMappings = (configuration.lib ?? []).flatMap(({ resourcesSubFolder, sourceFolder }) => {
|
|
29
|
+
const match = new RegExp(String.raw `^/resources/${resourcesSubFolder}/(.*?)(?:\?.*)?$`);
|
|
30
|
+
const shouldUseInstrumented = configuration.coverage && sourceFolder.startsWith(configuration.cwd);
|
|
31
|
+
if (shouldUseInstrumented) {
|
|
32
|
+
const instrumentedCwd = Path.join(configuration.coverageTempDir, 'instrumented', sourceFolder.slice(configuration.cwd.length));
|
|
33
|
+
return [
|
|
34
|
+
{ match, cwd: instrumentedCwd, file: '$1' },
|
|
35
|
+
{ match, cwd: sourceFolder, file: '$1' }
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
return [{ match, cwd: sourceFolder, file: '$1' }];
|
|
39
|
+
});
|
|
40
|
+
const isWebappHasResources = (await hasFolder(Path.join(webapp, 'resources'))) || (await hasFolder(Path.join(webapp, 'test-resources')));
|
|
41
|
+
const webappResourcesMappings = isWebappHasResources
|
|
42
|
+
? [
|
|
43
|
+
{
|
|
44
|
+
method: 'GET,HEAD',
|
|
45
|
+
match: resourcesMatch,
|
|
46
|
+
cwd: webapp,
|
|
47
|
+
file: '$1'
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
: [];
|
|
51
|
+
return {
|
|
52
|
+
port: configuration.port ?? 0,
|
|
53
|
+
mappings: [
|
|
54
|
+
...libMappings,
|
|
55
|
+
...webappResourcesMappings,
|
|
56
|
+
{
|
|
57
|
+
method: 'GET,HEAD',
|
|
58
|
+
match: resourcesMatch,
|
|
59
|
+
url: ui5Mapping,
|
|
60
|
+
'ignore-unverifiable-certificate': true
|
|
61
|
+
},
|
|
62
|
+
...coverageMappings,
|
|
63
|
+
{
|
|
64
|
+
match: /^\/(.*)/,
|
|
65
|
+
cwd: webapp,
|
|
66
|
+
file: '$1'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
custom: (request) => {
|
|
70
|
+
logger.warn({ source: 'server/unhandled', message: request.url });
|
|
71
|
+
return 404;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
};
|
|
76
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { logger, Path } from '../../platform/index.js';
|
|
2
|
+
import { Folder } from '../../utils/node/Folder.js';
|
|
3
|
+
import { setTimeout } from 'node:timers/promises';
|
|
4
|
+
const screenshotTimeoutError = async (ms) => {
|
|
5
|
+
await setTimeout(ms);
|
|
6
|
+
throw new Error('screenshot timeout');
|
|
7
|
+
};
|
|
8
|
+
export const makeScreenshotHandlers = (configuration) => {
|
|
9
|
+
const { screenshotOnFailure, screenshotTimeout, reportDir } = configuration;
|
|
10
|
+
const takeScreenshot = async (page, filename) => {
|
|
11
|
+
await Folder.create(reportDir);
|
|
12
|
+
const absolutePath = Path.join(reportDir, filename);
|
|
13
|
+
await Promise.race([page.screenshot(absolutePath), screenshotTimeoutError(screenshotTimeout)]);
|
|
14
|
+
return filename;
|
|
15
|
+
};
|
|
16
|
+
const handlePendingScreenshot = async (page, agentState, pageId) => {
|
|
17
|
+
if (agentState.type !== 'QUnit' || !agentState.pendingScreenshot)
|
|
18
|
+
return;
|
|
19
|
+
const filename = agentState.pendingScreenshot;
|
|
20
|
+
try {
|
|
21
|
+
await takeScreenshot(page, filename);
|
|
22
|
+
logger.debug({ source: 'page', message: 'screenshot taken', pageId, data: { filename } });
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
logger.error({ source: 'page', message: 'screenshot failed', error, pageId });
|
|
26
|
+
}
|
|
27
|
+
await page.eval("window['ui5-test-runner'].state.pendingScreenshot = false");
|
|
28
|
+
};
|
|
29
|
+
const handleFailureScreenshot = async (page, pageId, testResults) => {
|
|
30
|
+
if (!screenshotOnFailure || testResults.tests.every((t) => t.status !== 'failed'))
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
const filename = await takeScreenshot(page, `${pageId}-failure.png`);
|
|
34
|
+
logger.debug({ source: 'page', message: 'failure screenshot taken', pageId, data: { path: filename } });
|
|
35
|
+
testResults.tests.push({
|
|
36
|
+
name: 'failure screenshot',
|
|
37
|
+
status: 'other',
|
|
38
|
+
duration: 0,
|
|
39
|
+
attachments: [{ name: 'failure screenshot', contentType: 'image/png', path: filename }]
|
|
40
|
+
});
|
|
41
|
+
testResults.summary.tests += 1;
|
|
42
|
+
testResults.summary.other += 1;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
logger.error({ source: 'page', message: 'failure screenshot failed', error, pageId });
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return { handlePendingScreenshot, handleFailureScreenshot };
|
|
49
|
+
};
|