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.
Files changed (131) hide show
  1. package/README.md +20 -29
  2. package/dist/Command.js +52 -0
  3. package/dist/Npm.js +193 -43
  4. package/dist/browsers/RecoverableBrowserError.js +6 -0
  5. package/dist/browsers/chromeArguments.js +5 -0
  6. package/dist/browsers/chromeOptions.js +29 -0
  7. package/dist/browsers/consoleMessage.js +23 -0
  8. package/dist/browsers/factory.js +169 -4
  9. package/dist/browsers/networkResponse.js +15 -0
  10. package/dist/browsers/playwright.js +104 -0
  11. package/dist/browsers/puppeteer.js +55 -89
  12. package/dist/browsers/seleniumWebdriver.js +124 -0
  13. package/dist/browsers/webdriverio.js +85 -0
  14. package/dist/cli.js +18 -12
  15. package/dist/configuration/CommandLine.js +46 -26
  16. package/dist/configuration/ConfigurationValidator.js +132 -18
  17. package/dist/configuration/OptionValidationError.js +50 -2
  18. package/dist/configuration/derivations.js +8 -0
  19. package/dist/configuration/options.js +392 -23
  20. package/dist/configuration/validations.js +135 -0
  21. package/dist/configuration/validators/boolean.js +1 -1
  22. package/dist/configuration/validators/enumeration.js +7 -0
  23. package/dist/configuration/validators/fsEntry.js +12 -12
  24. package/dist/configuration/validators/index.js +8 -2
  25. package/dist/configuration/validators/integer.js +7 -1
  26. package/dist/configuration/validators/json.js +23 -0
  27. package/dist/configuration/validators/libraryMapping.js +28 -0
  28. package/dist/configuration/validators/percent.js +2 -2
  29. package/dist/configuration/validators/regexp.js +2 -2
  30. package/dist/configuration/validators/secret.js +8 -0
  31. package/dist/configuration/validators/string.js +1 -1
  32. package/dist/configuration/validators/timeout.js +2 -2
  33. package/dist/configuration/validators/url.js +3 -3
  34. package/dist/end.js +58 -0
  35. package/dist/if.js +22 -0
  36. package/dist/modes/Modes.js +2 -1
  37. package/dist/modes/batch/BatchItem.js +1 -0
  38. package/dist/modes/batch/batchTask.js +159 -0
  39. package/dist/modes/batch/index.js +98 -0
  40. package/dist/modes/batch/output.js +22 -0
  41. package/dist/modes/batch/report.js +45 -0
  42. package/dist/modes/batch/resolve.js +105 -0
  43. package/dist/modes/dumpConfig.js +6 -0
  44. package/dist/modes/execute.js +6 -5
  45. package/dist/modes/help.js +86 -2
  46. package/dist/modes/log/LogReader.js +3 -2
  47. package/dist/modes/log/LogStorage.js +31 -27
  48. package/dist/modes/log/index.js +39 -7
  49. package/dist/modes/log/{REserve.js → reserve.js} +25 -2
  50. package/dist/modes/mcp/IDocument.js +2 -0
  51. package/dist/modes/mcp/frontmatter.js +142 -0
  52. package/dist/modes/mcp/index.js +49 -0
  53. package/dist/modes/mcp/knowledgeBase.js +104 -0
  54. package/dist/modes/mcp/remoteKnowledgeBase.js +79 -0
  55. package/dist/modes/mcp/reserve.js +175 -0
  56. package/dist/modes/mcp/tools/getTopic.js +46 -0
  57. package/dist/modes/mcp/tools/index.js +6 -0
  58. package/dist/modes/mcp/tools/listTopics.js +14 -0
  59. package/dist/modes/test/PageContext.js +1 -0
  60. package/dist/modes/test/Server.js +99 -0
  61. package/dist/modes/test/browser.js +31 -17
  62. package/dist/modes/test/browserConfig.js +17 -0
  63. package/dist/modes/test/coverage/collect.js +38 -0
  64. package/dist/modes/test/coverage/index.js +3 -0
  65. package/dist/modes/test/coverage/instrument.js +70 -0
  66. package/dist/modes/test/coverage/nyc.js +15 -0
  67. package/dist/modes/test/coverage/report.js +71 -0
  68. package/dist/modes/test/coverage/settings.js +53 -0
  69. package/dist/modes/test/index.js +94 -34
  70. package/dist/modes/test/pageTask.js +293 -98
  71. package/dist/modes/test/report.js +20 -3
  72. package/dist/modes/test/reserve.js +76 -0
  73. package/dist/modes/test/screenshot.js +49 -0
  74. package/dist/platform/Crypto.js +9 -0
  75. package/dist/platform/Exit.js +33 -24
  76. package/dist/platform/FileSystem.js +6 -2
  77. package/dist/platform/Host.js +9 -1
  78. package/dist/platform/Http.js +54 -29
  79. package/dist/platform/Module.js +5 -0
  80. package/dist/platform/Path.js +5 -1
  81. package/dist/platform/Process.js +67 -13
  82. package/dist/platform/Terminal.js +12 -8
  83. package/dist/platform/Thread.js +1 -1
  84. package/dist/platform/Url.js +4 -0
  85. package/dist/platform/assert.js +13 -12
  86. package/dist/platform/environment.js +9 -18
  87. package/dist/platform/index.js +3 -0
  88. package/dist/platform/logger/CompressionContext.js +117 -0
  89. package/dist/platform/logger/allCompressed.js +8 -7
  90. package/dist/platform/logger/compress.js +29 -129
  91. package/dist/platform/logger/output/BaseLoggerOutput.js +49 -31
  92. package/dist/platform/logger/output/InteractiveLoggerOutput.js +1 -1
  93. package/dist/platform/logger/output/LlmLoggerOutput.js +29 -0
  94. package/dist/platform/logger/output/factory.js +5 -1
  95. package/dist/platform/logger/output.js +24 -6
  96. package/dist/platform/logger/toInternalLogAttributes.js +10 -11
  97. package/dist/platform/logger/traceFileName.js +1 -0
  98. package/dist/platform/logger.js +52 -21
  99. package/dist/platform/mock.js +124 -27
  100. package/dist/platform/version.js +1 -2
  101. package/dist/platform/workerBootstrap.js +1 -1
  102. package/dist/reports/initReportBuilder.js +18 -0
  103. package/dist/reports/junit.js +64 -0
  104. package/dist/reports/saveReport.js +10 -0
  105. package/dist/reports/textReport.js +92 -0
  106. package/dist/reserveLogger.js +22 -0
  107. package/dist/sendToParentProcess.js +6 -0
  108. package/dist/start.js +79 -0
  109. package/dist/types/ISecretValue.js +1 -0
  110. package/dist/types/SecretValue.js +15 -0
  111. package/dist/ui/agent.js +4 -2
  112. package/dist/ui/html-report.js +2 -2
  113. package/dist/ui/lib.js +1 -1
  114. package/dist/ui/log-viewer.js +1 -1
  115. package/dist/utils/node/Folder.js +2 -4
  116. package/dist/utils/node/FramedStreamReader.js +4 -5
  117. package/dist/utils/node/FramedStreamWriter.js +6 -5
  118. package/dist/utils/node/anonymize.js +11 -0
  119. package/dist/utils/node/timeout.js +11 -0
  120. package/dist/utils/shared/ProgressBar.js +1 -2
  121. package/dist/utils/shared/TestReportBuilder.js +11 -7
  122. package/dist/utils/shared/host.js +11 -0
  123. package/dist/utils/shared/object.js +12 -3
  124. package/dist/utils/shared/schema.js +20 -0
  125. package/dist/utils/shared/string.js +1 -0
  126. package/dist/utils/shared/toIError.js +3 -7
  127. package/dist/utils/shared/version.js +52 -0
  128. package/package.json +2 -78
  129. package/dist/configuration/validators/browser.js +0 -11
  130. package/dist/modes/test/REserve.js +0 -31
  131. 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 { setTimeout } from 'node:timers/promises';
6
- import { reportBuilder } from './report.js';
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 reportQunitProgress = (context, agentState) => {
9
- if (agentState.isOpa) {
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 > 0) {
13
- const { executed, total, errors } = agentState;
14
- if (executed !== context.lastExecuted || total !== context.lastTotal) {
15
- const type = agentState.isOpa ? 'opa' : 'qunit';
16
- context.lastExecuted = executed;
17
- context.lastTotal = total;
18
- context.errors = errors;
19
- context.type = type;
20
- logger.info({
21
- source: 'progress',
22
- message: context.url,
23
- pageId: context.pageId,
24
- data: { max: agentState.total, value: agentState.executed, type, errors }
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 queryAgentState = async (context) => {
30
- const agentState = (await context.page.eval("window['ui5-test-runner'].state"));
31
- logger.debug({ source: 'page', message: 'agent state', data: { state: agentState }, pageId: context.pageId });
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
- for (const page of agentState.pages) {
35
- const pageUrl = new URL(page, context.url).toString();
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.fatal({
119
+ logger.error({
41
120
  source: 'page',
42
- message: 'Unable to detect page type',
121
+ message: 'unable to detect page type',
43
122
  pageId: context.pageId,
44
123
  data: { state: agentState }
45
124
  });
46
- throw new Error('Unable to detect page type');
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 true;
136
+ return shouldUncaughtErrorsFail(context, agentState.uncaughtErrors);
58
137
  }
59
138
  }
60
139
  return false;
61
140
  };
62
- export const pageTask = async function (url, index, urls) {
63
- const pageId = ++lastPageId;
64
- logger.debug({ source: 'page', message: 'new page task', pageId, data: { url } });
65
- logger.info({
66
- source: 'progress',
67
- message: url,
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: { max: 0, value: 1, type: 'unknown', errors: 0 }
182
+ data: { results: testResults }
70
183
  });
71
- const { promise: taskStopped, resolve: setTaskAsStopped } = Promise.withResolvers();
72
- using _ = Exit.registerAsyncTask({
73
- name: url,
74
- stop: async () => {
75
- try {
76
- this.stop(new ExitShutdownError());
77
- }
78
- catch {
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
- finally {
81
- await taskStopped;
194
+ if (shouldStopBasedOnAgentState(context, handlePendingScreenshot !== false, agentState)) {
195
+ break;
82
196
  }
83
197
  }
84
- });
85
- let page;
86
- let context;
87
- try {
88
- const agentSource = await getAgentSource();
89
- const browser = getBrowser();
90
- page = await browser.newWindow({
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
- scripts: [agentSource],
93
- url
233
+ data: { max: 0, value: 1, type: 'unknown', errors: 0 }
94
234
  });
95
- context = {
96
- pageId,
97
- urls,
98
- url,
99
- page,
100
- loopDelay: 250,
101
- type: 'unknown',
102
- lastExecuted: 0,
103
- errors: 0,
104
- lastTotal: 0
105
- };
106
- while (!this.stopRequested) {
107
- try {
108
- await setTimeout(context.loopDelay);
109
- if (await queryAgentState(context)) {
110
- break;
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
- catch (error) {
114
- logger.error({ source: 'page', message: 'An error occurred', error, pageId, data: {} });
115
- break;
116
- }
117
- }
118
- const testResults = (await page.eval("window['ui5-test-runner'].results"));
119
- logger.debug({ source: 'page', message: 'test results', pageId, data: { results: testResults } });
120
- reportBuilder.merge(url, testResults);
121
- }
122
- finally {
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
- data: {
129
- max: context.lastTotal,
130
- value: context.lastExecuted,
131
- type: context.type,
132
- errors: context.errors,
133
- remove: true
134
- }
269
+ scripts,
270
+ url
135
271
  });
136
- }
137
- try {
138
- await page?.close();
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.close failed', error, pageId, data: {} });
310
+ logger.error({ source: 'page', message: 'unexpected error while running page', error, pageId, data: {} });
311
+ reportError(url, 'An unexpected error occurred');
142
312
  }
143
- setTaskAsStopped();
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 { TestReportBuilder } from '../../utils/shared/TestReportBuilder.js';
2
- import { version } from '../../platform/version.js';
3
- export const reportBuilder = new TestReportBuilder(crypto.randomUUID(), await version());
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
+ };
@@ -0,0 +1,9 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ export const Crypto = {
3
+ sha256hex(input) {
4
+ return createHash('sha256').update(input).digest('hex');
5
+ },
6
+ randomUUID() {
7
+ return randomUUID();
8
+ }
9
+ };