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,20 +1,27 @@
1
1
  import { Exit, ExitShutdownError } from './Exit.js';
2
2
  import { Host } from './Host.js';
3
3
  import { Terminal } from './Terminal.js';
4
+ import { Path } from './Path.js';
4
5
  import { Thread } from './Thread.js';
6
+ import { performance } from 'node:perf_hooks';
5
7
  import { LogLevel } from './logger/types.js';
6
8
  import { toInternalLogAttributes } from './logger/toInternalLogAttributes.js';
9
+ import { traceFileName } from './logger/traceFileName.js';
7
10
  import assert from 'node:assert/strict';
8
11
  import { toPlainObject } from '../utils/shared/object.js';
12
+ import { redactSecrets } from '../utils/node/anonymize.js';
9
13
  import { toIError } from '../utils/shared/toIError.js';
10
14
  const startedAt = Date.now();
11
15
  let channel;
12
16
  let loggerWorker;
13
17
  let consoleWorker;
18
+ let reportDirectory;
14
19
  const buffer = [];
15
20
  const waitingFor = ['allCompressed', 'output'];
16
- let ready = !Thread.isMainThread;
21
+ let isReady = !Thread.isMainThread;
22
+ const { promise: startPromise, resolve: startResolve } = Promise.withResolvers();
17
23
  let metricsMonitorInterval;
24
+ let lastELU = performance.eventLoopUtilization();
18
25
  const terminalResized = (width) => {
19
26
  channel?.postMessage({
20
27
  command: 'terminal-resized',
@@ -24,13 +31,17 @@ const terminalResized = (width) => {
24
31
  const start = () => {
25
32
  channel = Thread.createBroadcastChannel('logger');
26
33
  metricsMonitorInterval = setInterval(() => {
34
+ const cpu = Thread.threadCpuUsage();
35
+ const mem = Host.memoryUsage();
36
+ const temporaryELU = performance.eventLoopUtilization();
37
+ const elu = performance.eventLoopUtilization(temporaryELU, lastELU);
38
+ lastELU = temporaryELU;
39
+ const heapUsedMB = Math.round(mem.heapUsed / 1024 / 1024);
40
+ const cpuUserMs = Math.round(cpu.user / 1000);
27
41
  logger.debug({
28
42
  source: 'metric',
29
- message: '',
30
- data: {
31
- cpu: Thread.threadCpuUsage(),
32
- mem: Host.memoryUsage()
33
- }
43
+ message: `heapUsed=${heapUsedMB}MB cpu=${cpuUserMs}ms elu=${elu.utilization.toFixed(3)}`,
44
+ data: { cpu, mem, elu }
34
45
  });
35
46
  }, 1000);
36
47
  channel.onmessage = (event) => {
@@ -39,17 +50,23 @@ const start = () => {
39
50
  terminalResized(Terminal.width);
40
51
  const index = waitingFor.indexOf(message.source);
41
52
  waitingFor.splice(index, 1);
42
- ready = waitingFor.length === 0;
43
- if (ready && buffer.length > 0) {
44
- for (const buffered of buffer) {
45
- channel.postMessage({
46
- command: 'log',
47
- ...buffered
48
- });
53
+ isReady = waitingFor.length === 0;
54
+ if (isReady) {
55
+ if (buffer.length > 0) {
56
+ for (const buffered of buffer) {
57
+ channel.postMessage({
58
+ command: 'log',
59
+ ...buffered
60
+ });
61
+ }
62
+ buffer.length = 0;
49
63
  }
50
- buffer.length = 0;
64
+ startResolve();
51
65
  }
52
66
  }
67
+ else if (message.command === 'fatal') {
68
+ void Exit.shutdown();
69
+ }
53
70
  else if (message.command === 'terminate') {
54
71
  clearInterval(metricsMonitorInterval);
55
72
  channel.close();
@@ -61,7 +78,7 @@ const log = (level, attributes) => {
61
78
  if (attributes.error) {
62
79
  allAttributes.error = toIError(attributes.error);
63
80
  }
64
- if (ready && !stopping) {
81
+ if (isReady && !stopping) {
65
82
  channel.postMessage({
66
83
  command: 'log',
67
84
  ...allAttributes
@@ -76,26 +93,30 @@ if (!Thread.isMainThread) {
76
93
  }
77
94
  let stopping;
78
95
  export const logger = {
79
- start(configuration) {
96
+ async start(configuration) {
80
97
  assert.ok(Thread.isMainThread, 'Call logger.start only in main thread');
81
98
  assert.ok(!loggerWorker && !consoleWorker, 'Call logger.start only once');
99
+ reportDirectory = configuration.reportDir;
82
100
  start();
83
101
  loggerWorker = Thread.createWorker('platform/logger/allCompressed', {
84
- configuration: toPlainObject(configuration)
102
+ configuration: redactSecrets(toPlainObject(configuration)),
103
+ startedAt
85
104
  });
86
105
  consoleWorker = Thread.createWorker('platform/logger/output', {
87
- configuration: toPlainObject(configuration),
106
+ configuration: redactSecrets(toPlainObject(configuration)),
88
107
  startedAt
89
108
  });
90
109
  if (!configuration.ci) {
91
110
  Terminal.setRawMode((data) => {
92
- if (data.length === 1 && data[0] === 3) {
93
- logger.warn({ source: 'job', message: 'User requested interruption' });
94
- Exit.sigInt();
111
+ if (!(data.length === 1 && data[0] === 3)) {
112
+ return;
95
113
  }
114
+ logger.warn({ source: 'job', message: 'user requested interruption' });
115
+ Exit.sigInt();
96
116
  });
97
117
  Terminal.onResize(terminalResized);
98
118
  }
119
+ return startPromise;
99
120
  },
100
121
  debug(attributes) {
101
122
  log(LogLevel.debug, attributes);
@@ -111,12 +132,22 @@ export const logger = {
111
132
  },
112
133
  fatal(attributes) {
113
134
  log(LogLevel.fatal, attributes);
135
+ if (Thread.isMainThread) {
136
+ void Exit.shutdown();
137
+ }
138
+ else {
139
+ channel.postMessage({ command: 'fatal' });
140
+ }
141
+ throw new ExitShutdownError();
114
142
  },
115
143
  async stop() {
116
144
  assert.ok(Thread.isMainThread, 'Call logger.stop only in main thread');
117
145
  if (stopping) {
118
146
  return await stopping;
119
147
  }
148
+ if (reportDirectory !== undefined) {
149
+ logger.info({ source: 'job', message: `trace file: ${Path.join(reportDirectory, traceFileName(startedAt))}` });
150
+ }
120
151
  const { promise, resolve } = Promise.withResolvers();
121
152
  stopping = promise;
122
153
  clearInterval(metricsMonitorInterval);
@@ -1,21 +1,60 @@
1
1
  import { vi } from 'vitest';
2
- import { join } from 'node:path';
3
- const mockStaticMethodsOfExportedClasses = (actual) => {
2
+ import { basename, extname, join, relative } from 'node:path';
3
+ import { setTimeout } from 'node:timers/promises';
4
+ import { options, defaults } from '../configuration/options.js';
5
+ import assert from 'node:assert';
6
+ export const MOCK_CWD = '~/ui5-test-runner/vitest';
7
+ assert.ok(options[0].name === 'cwd');
8
+ Object.assign(options[0], { default: MOCK_CWD });
9
+ Object.assign(defaults, { cwd: MOCK_CWD });
10
+ const _registry = new WeakMap();
11
+ const functionMembers = (object) => {
12
+ const out = {};
13
+ for (const key of Object.getOwnPropertyNames(object)) {
14
+ if (typeof object[key] === 'function') {
15
+ out[key] = object[key];
16
+ }
17
+ }
18
+ return out;
19
+ };
20
+ const registerUnmock = (mocked, real) => {
21
+ _registry.set(mocked, { real: functionMembers(real), mocked: functionMembers(mocked) });
22
+ };
23
+ export const unmock = (target) => {
24
+ const entry = _registry.get(target);
25
+ assert.ok(entry, 'unmock: object was not registered as a mocked platform export');
26
+ Object.assign(target, entry.real);
27
+ const restore = () => {
28
+ Object.assign(target, entry.mocked);
29
+ };
30
+ return { restore, [Symbol.dispose]: restore };
31
+ };
32
+ const mockMethods = (object, members) => {
33
+ for (const member of members) {
34
+ if (typeof object[member] === 'function') {
35
+ object[member] = vi.fn();
36
+ }
37
+ }
38
+ };
39
+ const mockStaticMethodsOfExports = (actual) => {
4
40
  const mocked = { ...actual };
5
41
  for (const exportName in mocked) {
6
42
  const exportValue = mocked[exportName];
7
43
  if (typeof exportValue === 'function') {
8
- const exportClass = exportValue;
9
- for (const staticName of Object.getOwnPropertyNames(exportValue)) {
10
- const staticValue = exportClass[staticName];
11
- if (typeof staticValue === 'function') {
12
- exportClass[staticName] = vi.fn();
13
- }
14
- }
44
+ mockMethods(exportValue, Object.getOwnPropertyNames(exportValue));
45
+ }
46
+ else if (typeof exportValue === 'object' && exportValue !== null) {
47
+ mockMethods(exportValue, Object.keys(exportValue));
15
48
  }
16
49
  }
17
50
  return mocked;
18
51
  };
52
+ vi.mock(import('./Crypto.js'), async (importActual) => {
53
+ const actual = await importActual();
54
+ const mocked = mockStaticMethodsOfExports(actual);
55
+ registerUnmock(mocked.Crypto, actual.Crypto);
56
+ return mocked;
57
+ });
19
58
  vi.mock(import('./constants.js'), async (importActual) => {
20
59
  const mocked = await importActual();
21
60
  return {
@@ -25,8 +64,11 @@ vi.mock(import('./constants.js'), async (importActual) => {
25
64
  });
26
65
  export const __unregisterExitAsyncTask = vi.fn();
27
66
  export let __lastRegisteredExitAsyncTask;
67
+ let _ExitShutdownError;
28
68
  vi.mock(import('./Exit.js'), async (importActual) => {
29
- const mocked = mockStaticMethodsOfExportedClasses(await importActual());
69
+ const actual = await importActual();
70
+ _ExitShutdownError = actual.ExitShutdownError;
71
+ const mocked = mockStaticMethodsOfExports(actual);
30
72
  const { Exit } = mocked;
31
73
  vi.mocked(Exit.registerAsyncTask).mockImplementation((task) => {
32
74
  __lastRegisteredExitAsyncTask = task;
@@ -35,7 +77,8 @@ vi.mock(import('./Exit.js'), async (importActual) => {
35
77
  return mocked;
36
78
  });
37
79
  vi.mock(import('./FileSystem.js'), async (importActual) => {
38
- const mocked = mockStaticMethodsOfExportedClasses(await importActual());
80
+ const actual = await importActual();
81
+ const mocked = mockStaticMethodsOfExports(actual);
39
82
  const { FileSystem } = mocked;
40
83
  const writeStream = {
41
84
  write: vi.fn().mockImplementation((_, callback) => callback()),
@@ -44,33 +87,75 @@ vi.mock(import('./FileSystem.js'), async (importActual) => {
44
87
  vi.mocked(FileSystem.createWriteStream).mockReturnValue(writeStream);
45
88
  const readStream = {};
46
89
  vi.mocked(FileSystem.createReadStream).mockReturnValue(readStream);
90
+ registerUnmock(FileSystem, actual.FileSystem);
91
+ return mocked;
92
+ });
93
+ vi.mock(import('./Host.js'), async (importActual) => {
94
+ const actual = await importActual();
95
+ const mocked = mockStaticMethodsOfExports(actual);
96
+ const { Host } = mocked;
97
+ vi.mocked(Host.cpus).mockReturnValue([]);
98
+ vi.mocked(Host.cwd).mockReturnValue(MOCK_CWD);
99
+ vi.mocked(Host.tmpdir).mockReturnValue('/tmp');
100
+ registerUnmock(Host, actual.Host);
101
+ return mocked;
102
+ });
103
+ vi.mock(import('./Http.js'), async (importActual) => {
104
+ const actual = await importActual();
105
+ const mocked = mockStaticMethodsOfExports(actual);
106
+ registerUnmock(mocked.Http, actual.Http);
107
+ return mocked;
108
+ });
109
+ vi.mock(import('./Module.js'), async (importActual) => {
110
+ const actual = await importActual();
111
+ const mocked = mockStaticMethodsOfExports(actual);
112
+ registerUnmock(mocked.Module, actual.Module);
47
113
  return mocked;
48
114
  });
49
- vi.mock(import('./Host.js'), async (importActual) => mockStaticMethodsOfExportedClasses(await importActual()));
50
- vi.mock(import('./Http.js'), async (importActual) => mockStaticMethodsOfExportedClasses(await importActual()));
51
115
  const logger = {
52
- start: vi.fn(),
53
- debug: vi.fn(),
54
- info: vi.fn(),
55
- warn: vi.fn(),
56
- error: vi.fn(),
57
- fatal: vi.fn(),
116
+ start: vi.fn(() => Promise.resolve()),
117
+ debug: vi.fn().mockImplementation((attributes) => console.debug(attributes)),
118
+ info: vi.fn().mockImplementation((attributes) => console.log(attributes)),
119
+ warn: vi.fn().mockImplementation((attributes) => console.warn(attributes)),
120
+ error: vi.fn().mockImplementation((attributes) => console.error(attributes)),
121
+ fatal: vi.fn().mockImplementation((attributes) => {
122
+ console.error(attributes);
123
+ const error = new _ExitShutdownError();
124
+ error.message = `logger.fatal [${attributes.source}] ${attributes.message}`;
125
+ if (attributes.error !== undefined) {
126
+ error.cause = attributes.error;
127
+ }
128
+ throw error;
129
+ }),
58
130
  stop: vi.fn()
59
131
  };
60
132
  vi.mock(import('./logger.js'), () => ({ logger }));
61
133
  vi.mock(import('./Path.js'), async (importActual) => {
62
- const mocked = mockStaticMethodsOfExportedClasses(await importActual());
134
+ const actual = await importActual();
135
+ const mocked = mockStaticMethodsOfExports(actual);
63
136
  const { Path } = mocked;
64
- vi.mocked(Path.isAbsolute).mockImplementation((path) => path.startsWith('/'));
137
+ vi.mocked(Path.basename).mockImplementation((path) => basename(path));
138
+ vi.mocked(Path.dirname).mockImplementation((path) => join(path, '..').replaceAll('\\', '/'));
139
+ vi.mocked(Path.extname).mockImplementation((path) => extname(path));
140
+ vi.mocked(Path.isAbsolute).mockImplementation((path) => path.startsWith('/') || path.startsWith('~/'));
65
141
  vi.mocked(Path.join).mockImplementation((...arguments_) => join(...arguments_).replaceAll('\\', '/'));
142
+ vi.mocked(Path.relative).mockImplementation((from, to) => relative(from, to).replaceAll('\\', '/'));
143
+ registerUnmock(Path, actual.Path);
144
+ return mocked;
145
+ });
146
+ vi.mock(import('./Process.js'), async (importActual) => {
147
+ const actual = await importActual();
148
+ const mocked = mockStaticMethodsOfExports(actual);
149
+ const { Process } = mocked;
150
+ vi.mocked(Process.sleep).mockImplementation((ms) => setTimeout(ms));
151
+ registerUnmock(Process, actual.Process);
66
152
  return mocked;
67
153
  });
68
- vi.mock(import('./Process.js'), async (importActual) => mockStaticMethodsOfExportedClasses(await importActual()));
69
154
  export let __lastTerminalRawModeCallback = false;
70
155
  vi.mock(import('./Terminal.js'), async (importActual) => {
71
156
  const actual = await importActual();
72
157
  const originalstripVTControlCharacters = actual.Terminal.stripVTControlCharacters;
73
- const mocked = mockStaticMethodsOfExportedClasses(actual);
158
+ const mocked = mockStaticMethodsOfExports(actual);
74
159
  const { Terminal } = mocked;
75
160
  vi.mocked(Terminal.setRawMode).mockImplementation((callback) => {
76
161
  __lastTerminalRawModeCallback = callback;
@@ -79,7 +164,7 @@ vi.mock(import('./Terminal.js'), async (importActual) => {
79
164
  return mocked;
80
165
  });
81
166
  vi.mock(import('./Thread.js'), async (importActual) => {
82
- const mocked = mockStaticMethodsOfExportedClasses(await importActual());
167
+ const mocked = mockStaticMethodsOfExports(await importActual());
83
168
  const { Thread } = mocked;
84
169
  const channel = {
85
170
  postMessage: vi.fn().mockImplementation((data) => {
@@ -93,12 +178,24 @@ vi.mock(import('./Thread.js'), async (importActual) => {
93
178
  const worker = eventTarget;
94
179
  Object.assign(worker, {
95
180
  on: eventTarget.addEventListener.bind(eventTarget),
96
- postMessage: vi.fn()
181
+ postMessage: vi.fn(),
182
+ unref: vi.fn()
97
183
  });
98
184
  vi.mocked(Thread.createWorker).mockReturnValue(worker);
99
185
  return mocked;
100
186
  });
101
187
  vi.mock(import('./version.js'), () => ({
102
- version: vi.fn().mockResolvedValue('ui5-test-runner@1.2.3')
188
+ version: vi.fn().mockResolvedValue({ name: 'ui5-test-runner', version: '1.2.3' })
103
189
  }));
104
- vi.mock(import('./ZLib.js'), async (importActual) => mockStaticMethodsOfExportedClasses(await importActual()));
190
+ vi.mock(import('./ZLib.js'), async (importActual) => {
191
+ const actual = await importActual();
192
+ const mocked = mockStaticMethodsOfExports(actual);
193
+ registerUnmock(mocked.ZLib, actual.ZLib);
194
+ return mocked;
195
+ });
196
+ vi.mock(import('./Url.js'), async (importActual) => {
197
+ const actual = await importActual();
198
+ const mocked = mockStaticMethodsOfExports(actual);
199
+ registerUnmock(mocked.Url, actual.Url);
200
+ return mocked;
201
+ });
@@ -3,6 +3,5 @@ import { __sourcesRoot } from './constants.js';
3
3
  import { Path } from './Path.js';
4
4
  import { memoize } from '../utils/shared/memoize.js';
5
5
  export const version = memoize(async () => {
6
- const { name, version } = JSON.parse(await FileSystem.readFile(Path.join(__sourcesRoot, '../package.json'), 'utf8'));
7
- return `${name}@${version}`;
6
+ return JSON.parse(await FileSystem.readFile(Path.join(__sourcesRoot, '../package.json'), 'utf8'));
8
7
  });
@@ -8,7 +8,7 @@ const main = async () => {
8
8
  await workerMain(data);
9
9
  }
10
10
  catch (error) {
11
- logger.fatal({ source: 'thread', message: `An error occurred`, error, data: { path } });
11
+ logger.fatal({ source: 'thread', message: `an error occurred`, error, data: { path } });
12
12
  }
13
13
  finally {
14
14
  logger.debug({
@@ -0,0 +1,18 @@
1
+ import { version } from '../platform/version.js';
2
+ import { Host } from '../platform/index.js';
3
+ import { toPlainObject } from '../utils/shared/object.js';
4
+ import { anonymize } from '../utils/node/anonymize.js';
5
+ import { TestReportBuilder } from '../utils/shared/TestReportBuilder.js';
6
+ export async function initReportBuilder(configuration) {
7
+ const { name: toolName, version: toolVersion } = await version();
8
+ const builder = new TestReportBuilder(crypto.randomUUID(), `${toolName}@${toolVersion}`);
9
+ builder.report.results.tool = { name: toolName, version: toolVersion };
10
+ builder.report.results.environment = {
11
+ osPlatform: Host.platform(),
12
+ osRelease: Host.osRelease(),
13
+ osVersion: Host.osVersion(),
14
+ extra: { machine: Host.machine(), cpus: Host.cpus() }
15
+ };
16
+ builder.report.extra = { configuration: anonymize(toPlainObject(configuration)) };
17
+ return builder;
18
+ }
@@ -0,0 +1,64 @@
1
+ import { FileSystem, Path } from '../platform/index.js';
2
+ function xmlEscape(text) {
3
+ return text.replaceAll(/[<>&"]/g, (match) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' })[match]);
4
+ }
5
+ function groupBySuite(tests) {
6
+ const suitesByName = new Map();
7
+ for (const test of tests) {
8
+ const suiteName = test.suite?.[0] ?? '(root)';
9
+ let bucket = suitesByName.get(suiteName);
10
+ if (bucket === undefined) {
11
+ bucket = [];
12
+ suitesByName.set(suiteName, bucket);
13
+ }
14
+ bucket.push(test);
15
+ }
16
+ return suitesByName;
17
+ }
18
+ function testcaseLines(test, suiteName) {
19
+ const classname = test.suite && test.suite.length > 1 ? test.suite.slice(1).join('.') : suiteName;
20
+ const lines = [
21
+ ` <testcase name="${xmlEscape(test.name)}" classname="${xmlEscape(classname)}" time="${(test.duration / 1000).toFixed(3)}">`
22
+ ];
23
+ if (test.status === 'skipped' || test.status === 'pending') {
24
+ lines.push(' <skipped/>');
25
+ }
26
+ else if (test.status === 'failed') {
27
+ const message = test.message ? ` message="${xmlEscape(test.message)}"` : '';
28
+ lines.push(` <failure${message}>`);
29
+ if (test.trace) {
30
+ lines.push(xmlEscape(test.trace));
31
+ }
32
+ lines.push(' </failure>');
33
+ }
34
+ const attachments = test.attachments ?? [];
35
+ for (const attachment of attachments) {
36
+ lines.push(` <system-out>[[ATTACHMENT|${attachment.path}]]</system-out>`);
37
+ }
38
+ lines.push(' </testcase>');
39
+ return lines;
40
+ }
41
+ function suiteLines(suiteName, suiteTests) {
42
+ const failures = suiteTests.filter((t) => t.status === 'failed').length;
43
+ const skipped = suiteTests.filter((t) => t.status === 'skipped' || t.status === 'pending').length;
44
+ const time = (suiteTests.reduce((accumulator, t) => accumulator + t.duration, 0) / 1000).toFixed(3);
45
+ return [
46
+ ` <testsuite name="${xmlEscape(suiteName)}" tests="${suiteTests.length}" failures="${failures}" skipped="${skipped}" time="${time}">`,
47
+ ...suiteTests.flatMap((test) => testcaseLines(test, suiteName)),
48
+ ' </testsuite>'
49
+ ];
50
+ }
51
+ export function generateJUnitReport(configuration, report) {
52
+ if (!configuration.junitXmlFilename) {
53
+ return;
54
+ }
55
+ const { summary, tests } = report.results;
56
+ const time = ((summary.stop - summary.start) / 1000).toFixed(3);
57
+ const lines = [
58
+ '<?xml version="1.0" encoding="UTF-8"?>',
59
+ `<testsuites name="ui5-test-runner" tests="${summary.tests}" failures="${summary.failed}" skipped="${summary.skipped}" time="${time}">`,
60
+ ...[...groupBySuite(tests)].flatMap(([name, suiteTests]) => suiteLines(name, suiteTests)),
61
+ '</testsuites>'
62
+ ];
63
+ FileSystem.writeFileSync(Path.join(configuration.reportDir, configuration.junitXmlFilename), lines.join('\n'), 'utf8');
64
+ }
@@ -0,0 +1,10 @@
1
+ import { FileSystem, Path } from '../platform/index.js';
2
+ import { generateHtmlReport } from './html.js';
3
+ import { generateJUnitReport } from './junit.js';
4
+ import { generateTextReport } from './textReport.js';
5
+ export async function saveReport(configuration, report) {
6
+ FileSystem.writeFileSync(Path.join(configuration.reportDir, 'report.json'), JSON.stringify(report, undefined, 2), 'utf8');
7
+ generateTextReport(configuration, report);
8
+ await generateHtmlReport(configuration, report);
9
+ generateJUnitReport(configuration, report);
10
+ }
@@ -0,0 +1,92 @@
1
+ import { logger } from '../platform/index.js';
2
+ const PASSED_COUNT_WIDTH = 3;
3
+ const pageUrlOf = (test) => {
4
+ const suite = test.suite ?? [];
5
+ for (let index = suite.length - 1; index >= 0; --index) {
6
+ const entry = suite[index];
7
+ try {
8
+ const { protocol } = new URL(entry);
9
+ if (protocol === 'http:' || protocol === 'https:') {
10
+ return entry;
11
+ }
12
+ }
13
+ catch {
14
+ }
15
+ }
16
+ return suite[0] ?? test.name;
17
+ };
18
+ const moduleOf = (test, pageUrl) => {
19
+ const suite = test.suite ?? [];
20
+ const pageIndex = suite.lastIndexOf(pageUrl);
21
+ const tail = pageIndex === -1 ? suite : suite.slice(pageIndex + 1);
22
+ return tail.join(' ▶ ');
23
+ };
24
+ const groupByPage = (tests) => {
25
+ const pages = new Map();
26
+ for (const test of tests) {
27
+ const url = pageUrlOf(test);
28
+ let page = pages.get(url);
29
+ if (page === undefined) {
30
+ page = { url, passed: 0, skipped: 0, total: 0, isOpa: false, failures: [] };
31
+ pages.set(url, page);
32
+ }
33
+ ++page.total;
34
+ if (test.status === 'passed') {
35
+ ++page.passed;
36
+ }
37
+ else if (test.status === 'failed') {
38
+ page.failures.push(test);
39
+ }
40
+ else if (test.status === 'skipped' || test.status === 'pending') {
41
+ ++page.skipped;
42
+ }
43
+ if (test.extra?.type === 'opa') {
44
+ page.isOpa = true;
45
+ }
46
+ }
47
+ return pages.values().toArray();
48
+ };
49
+ const reportFailures = (page) => {
50
+ const shown = page.isOpa ? page.failures.slice(0, 1) : page.failures;
51
+ for (const test of shown) {
52
+ const module = moduleOf(test, page.url);
53
+ let message = `[${page.url}]\n` + (module ? `${module} ▶ ${test.name}` : test.name);
54
+ if (test.message) {
55
+ message += `\n ${test.message}`;
56
+ }
57
+ if (test.trace) {
58
+ message += `\n ${test.trace}`;
59
+ }
60
+ logger.error({ source: 'job', message });
61
+ }
62
+ if (page.isOpa && page.failures.length > 1) {
63
+ logger.warn({
64
+ source: 'job',
65
+ message: `[${page.url}]\n${page.failures.length} failures; showing first OPA error only`
66
+ });
67
+ }
68
+ };
69
+ const statusOf = (page) => {
70
+ if (page.failures.length > 0) {
71
+ return 'FAIL';
72
+ }
73
+ return page.passed === 0 && page.skipped > 0 ? 'SKIP' : 'PASS';
74
+ };
75
+ export function generateTextReport(_configuration, report) {
76
+ const pages = groupByPage(report.results.tests);
77
+ if (pages.length === 0) {
78
+ return;
79
+ }
80
+ logger.info({ source: 'job', message: 'Test report:' });
81
+ for (const page of pages) {
82
+ const ratio = `${page.passed.toString().padStart(PASSED_COUNT_WIDTH, ' ')}/${page.total}`;
83
+ logger.info({ source: 'job', message: `${statusOf(page)} ${ratio} ${page.url}` });
84
+ }
85
+ if (report.results.summary.failed > 0) {
86
+ for (const page of pages) {
87
+ if (page.failures.length > 0) {
88
+ reportFailures(page);
89
+ }
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,22 @@
1
+ import { logger } from './platform/index.js';
2
+ export const logReserve = (server) => {
3
+ server
4
+ .on('created', () => {
5
+ logger.debug({ source: 'reserve', message: 'created', data: {} });
6
+ })
7
+ .on('ready', (event) => {
8
+ const { eventName: message, ...data } = event;
9
+ logger.debug({ source: 'reserve', message, data });
10
+ logger.info({ source: 'server', message: `server listening on: ${event.url}` });
11
+ })
12
+ .on('error', (event) => {
13
+ const { eventName: message, reason: error, ...data } = event;
14
+ logger.debug({ source: 'reserve', message, data, error });
15
+ });
16
+ for (const eventName of ['incoming', 'redirecting', 'redirected', 'aborted', 'closed']) {
17
+ server.on(eventName, (event) => {
18
+ const { eventName: message, ...data } = event;
19
+ logger.debug({ source: 'reserve', message, data });
20
+ });
21
+ }
22
+ };
@@ -0,0 +1,6 @@
1
+ import { Host, Process } from './platform/index.js';
2
+ export const sendToParentProcess = (message) => {
3
+ if (Host.env['UI5TR_BATCH_MODE']) {
4
+ Process.sendToParent(message);
5
+ }
6
+ };