ui5-test-runner 6.0.0-beta.2 → 6.0.0-beta.5

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/dist/Npm.js CHANGED
@@ -1,6 +1,6 @@
1
- import { logger, FileSystem, Http, Path, Process } from './platform/index.js';
1
+ import { logger, FileSystem, Http, Module, Path, Process, Url } from './platform/index.js';
2
2
  import { memoize } from './utils/shared/memoize.js';
3
- const getNpmCliPath = memoize(async () => {
3
+ export const getNpmCliPath = async () => {
4
4
  const npmChildProcess = Process.spawn('npm', [], {
5
5
  shell: true
6
6
  });
@@ -18,9 +18,10 @@ const getNpmCliPath = memoize(async () => {
18
18
  }
19
19
  logger.debug({ source: 'npm', message: `npm@${semver} ${path}` });
20
20
  return Path.join(path, 'bin/npm-cli.js');
21
- });
21
+ };
22
+ const memoizedNpmCliPath = memoize(getNpmCliPath);
22
23
  const npm = async (...arguments_) => {
23
- const npmCliPath = await getNpmCliPath();
24
+ const npmCliPath = await memoizedNpmCliPath();
24
25
  return Process.spawn('node', [npmCliPath, ...arguments_], {
25
26
  detached: true
26
27
  });
@@ -37,8 +38,33 @@ const getRoots = memoize(async () => {
37
38
  global: globalRootProcess.stdout.trim()
38
39
  };
39
40
  });
40
- export const Npm = {
41
- async getLatestVersion(moduleName) {
41
+ const buildInstallPlan = (strategy, moduleName, globalRoot, prefix) => {
42
+ if (strategy === 'global') {
43
+ return { installArguments: ['install', '-g', moduleName], reimportPath: globalRoot };
44
+ }
45
+ if (strategy === 'prefix') {
46
+ return {
47
+ installArguments: ['install', '--prefix', prefix, '--no-save', moduleName],
48
+ reimportPath: Path.join(prefix, 'node_modules')
49
+ };
50
+ }
51
+ return { installArguments: ['install', '--no-save', moduleName], reimportPath: undefined };
52
+ };
53
+ export class Npm {
54
+ static dynamicImport(specifier) {
55
+ return import(specifier);
56
+ }
57
+ static async tryImportFromPath(configuration, moduleName, nodeModulesPath) {
58
+ try {
59
+ const require = Module.createRequire(Url.pathToFileURL(Path.join(configuration.cwd, 'package.json')).href);
60
+ const resolved = require.resolve(moduleName, { paths: [nodeModulesPath] });
61
+ return await this.dynamicImport(Url.pathToFileURL(resolved).href);
62
+ }
63
+ catch {
64
+ return undefined;
65
+ }
66
+ }
67
+ static async getLatestVersion(moduleName) {
42
68
  try {
43
69
  const response = await Http.get(`https://registry.npmjs.org/${moduleName}/latest`);
44
70
  const { version } = JSON.parse(response);
@@ -49,8 +75,8 @@ export const Npm = {
49
75
  cause: error
50
76
  });
51
77
  }
52
- },
53
- async checkIfLatestVersion(moduleName, isLocal) {
78
+ }
79
+ static async checkIfLatestVersion(moduleName, isLocal) {
54
80
  try {
55
81
  const { local, global } = await getRoots();
56
82
  const { version: installedVersion } = JSON.parse(await FileSystem.readFile(Path.join(isLocal ? local : global, moduleName, 'package.json'), 'utf8'));
@@ -63,18 +89,62 @@ export const Npm = {
63
89
  catch (error) {
64
90
  logger.error({ source: 'npm', message: 'Failed in checkIfLatestVersion', error });
65
91
  }
66
- },
67
- async import(moduleName) {
92
+ }
93
+ static async import(configuration, moduleName) {
68
94
  logger.debug({ source: 'npm', message: `Npm.import(${moduleName})` });
69
95
  try {
70
- const module = (await import(moduleName));
96
+ const module = await this.dynamicImport(moduleName);
71
97
  logger.debug({ source: 'npm', message: `Module ${moduleName} found locally` });
72
- void this.checkIfLatestVersion(moduleName, true);
98
+ void Npm.checkIfLatestVersion(moduleName, true);
73
99
  return module;
74
100
  }
75
- catch {
101
+ catch (error) {
102
+ const code = error.code;
103
+ if (code !== 'ERR_MODULE_NOT_FOUND' && code !== 'MODULE_NOT_FOUND') {
104
+ throw error;
105
+ }
76
106
  logger.warn({ source: 'npm', message: `Module ${moduleName} not found locally` });
77
107
  }
78
- throw new Error('Not implemented');
108
+ const { global: globalRoot } = await getRoots();
109
+ const fromGlobal = await this.tryImportFromPath(configuration, moduleName, globalRoot);
110
+ if (fromGlobal !== undefined) {
111
+ logger.debug({ source: 'npm', message: `Module ${moduleName} found globally` });
112
+ void Npm.checkIfLatestVersion(moduleName, false);
113
+ return fromGlobal;
114
+ }
115
+ if (configuration.alternateNpmPath) {
116
+ const fromAlternate = await this.tryImportFromPath(configuration, moduleName, configuration.alternateNpmPath);
117
+ if (fromAlternate !== undefined) {
118
+ logger.debug({ source: 'npm', message: `Module ${moduleName} found in alternateNpmPath` });
119
+ return fromAlternate;
120
+ }
121
+ }
122
+ if (configuration.npmInstallPrefix) {
123
+ const fromPrefix = await this.tryImportFromPath(configuration, moduleName, Path.join(configuration.npmInstallPrefix, 'node_modules'));
124
+ if (fromPrefix !== undefined) {
125
+ logger.debug({ source: 'npm', message: `Module ${moduleName} found in npmInstallPrefix` });
126
+ return fromPrefix;
127
+ }
128
+ }
129
+ if (configuration.noNpmInstall) {
130
+ const message = `Module ${moduleName} not found and noNpmInstall is set`;
131
+ logger.fatal({ source: 'npm', message });
132
+ throw new Error(message);
133
+ }
134
+ const strategy = configuration.npmInstall;
135
+ logger.info({ source: 'npm', message: `Installing ${moduleName} (strategy: ${strategy})` });
136
+ const { installArguments, reimportPath } = buildInstallPlan(strategy, moduleName, globalRoot, configuration.npmInstallPrefix ?? '');
137
+ const installProcess = await npm(...installArguments);
138
+ await installProcess.closed;
139
+ if (reimportPath !== undefined) {
140
+ const result = await this.tryImportFromPath(configuration, moduleName, reimportPath);
141
+ if (result === undefined) {
142
+ const message = `Module ${moduleName} could not be loaded after install`;
143
+ logger.fatal({ source: 'npm', message });
144
+ throw new Error(message);
145
+ }
146
+ return result;
147
+ }
148
+ return await this.dynamicImport(moduleName);
79
149
  }
80
- };
150
+ }
@@ -3,7 +3,7 @@ const factories = {
3
3
  puppeteer: puppeteerFactory
4
4
  };
5
5
  export const BrowserFactory = {
6
- async build(browser) {
7
- return await factories[browser]();
6
+ async build(configuration, browser) {
7
+ return await factories[browser](configuration);
8
8
  }
9
9
  };
@@ -1,8 +1,8 @@
1
1
  import { logger, Exit, Process } from '../platform/index.js';
2
2
  import { Npm } from '../Npm.js';
3
3
  import { agentLogPrefix } from '../types/AgentState.js';
4
- export const factory = async () => {
5
- const puppeteer = await Npm.import('puppeteer');
4
+ export const factory = async (configuration) => {
5
+ const puppeteer = await Npm.import(configuration, 'puppeteer');
6
6
  const { launch } = puppeteer;
7
7
  let browser;
8
8
  const abortController = new AbortController();
package/dist/cli.js CHANGED
@@ -5,8 +5,8 @@ import { CommandLine } from './configuration/CommandLine.js';
5
5
  import { execute } from './modes/execute.js';
6
6
  const main = async () => {
7
7
  const cliVersion = await version();
8
- const cliName = cliVersion.split('@')[0];
9
- const indexOfCli = process.argv.findIndex((value) => /[\\/]cli(\.[tj]s)?$/.exec(value) || value === cliName);
8
+ const cliName = cliVersion.split('@')[0] ?? 'ui5-test-runner';
9
+ const indexOfCli = process.argv.findIndex((value) => /[\\/]cli(\.[tj]s)?$/.exec(value) || value.endsWith(cliName));
10
10
  const configuration = await CommandLine.buildConfigurationFrom(Host.cwd(), process.argv.slice(indexOfCli + 1));
11
11
  await execute(configuration);
12
12
  };
@@ -106,6 +106,17 @@ export const options = [
106
106
  type: 'boolean',
107
107
  description: 'prevent any NPM install'
108
108
  },
109
+ {
110
+ name: 'npmInstall',
111
+ type: 'string',
112
+ description: 'npm install strategy for missing packages',
113
+ default: 'global'
114
+ },
115
+ {
116
+ name: 'npmInstallPrefix',
117
+ type: 'fs-entry',
118
+ description: 'path used as --prefix when npmInstall is set to prefix'
119
+ },
109
120
  {
110
121
  name: 'outputInterval',
111
122
  short: 'oi',
@@ -182,6 +193,7 @@ export const defaults = {
182
193
  config: 'ui5-test-runner.json',
183
194
  cwd: process.cwd(),
184
195
  localhost: 'localhost',
196
+ npmInstall: 'global',
185
197
  outputInterval: 30_000,
186
198
  parallel: 2,
187
199
  reportDir: 'report',
@@ -26,7 +26,7 @@ export const log = async (configuration) => {
26
26
  name: 'log',
27
27
  stop: stop
28
28
  });
29
- const browser = await BrowserFactory.build('puppeteer');
29
+ const browser = await BrowserFactory.build(configuration, 'puppeteer');
30
30
  const browserReady = browser.setup({
31
31
  visible: true
32
32
  });
@@ -3,7 +3,7 @@ import { Exit, assert, logger } from '../../platform/index.js';
3
3
  let browser;
4
4
  export const setupBrowser = async (configuration) => {
5
5
  assert(configuration.browser === 'puppeteer');
6
- browser = await BrowserFactory.build('puppeteer');
6
+ browser = await BrowserFactory.build(configuration, 'puppeteer');
7
7
  const { debugKeepBrowserOpen } = configuration;
8
8
  const settings = {
9
9
  visible: debugKeepBrowserOpen
@@ -0,0 +1,4 @@
1
+ import { createRequire } from 'node:module';
2
+ export class Module {
3
+ static createRequire = createRequire;
4
+ }
@@ -0,0 +1,4 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ export class Url {
3
+ static pathToFileURL = pathToFileURL;
4
+ }
@@ -6,8 +6,10 @@ export * from './Exit.js';
6
6
  export * from './FileSystem.js';
7
7
  export * from './Host.js';
8
8
  export * from './Http.js';
9
+ export * from './Module.js';
9
10
  export * from './Path.js';
10
11
  export * from './Process.js';
11
12
  export * from './Terminal.js';
12
13
  export * from './Thread.js';
14
+ export * from './Url.js';
13
15
  export * from './ZLib.js';
@@ -1,17 +1,21 @@
1
1
  import { vi } from 'vitest';
2
2
  import { join } from 'node:path';
3
- const mockStaticMethodsOfExportedClasses = (actual) => {
3
+ const mockMethods = (object, members) => {
4
+ for (const member of members) {
5
+ if (typeof object[member] === 'function') {
6
+ object[member] = vi.fn();
7
+ }
8
+ }
9
+ };
10
+ const mockStaticMethodsOfExports = (actual) => {
4
11
  const mocked = { ...actual };
5
12
  for (const exportName in mocked) {
6
13
  const exportValue = mocked[exportName];
7
14
  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
- }
15
+ mockMethods(exportValue, Object.getOwnPropertyNames(exportValue));
16
+ }
17
+ else if (typeof exportValue === 'object' && exportValue !== null) {
18
+ mockMethods(exportValue, Object.keys(exportValue));
15
19
  }
16
20
  }
17
21
  return mocked;
@@ -26,7 +30,7 @@ vi.mock(import('./constants.js'), async (importActual) => {
26
30
  export const __unregisterExitAsyncTask = vi.fn();
27
31
  export let __lastRegisteredExitAsyncTask;
28
32
  vi.mock(import('./Exit.js'), async (importActual) => {
29
- const mocked = mockStaticMethodsOfExportedClasses(await importActual());
33
+ const mocked = mockStaticMethodsOfExports(await importActual());
30
34
  const { Exit } = mocked;
31
35
  vi.mocked(Exit.registerAsyncTask).mockImplementation((task) => {
32
36
  __lastRegisteredExitAsyncTask = task;
@@ -35,7 +39,7 @@ vi.mock(import('./Exit.js'), async (importActual) => {
35
39
  return mocked;
36
40
  });
37
41
  vi.mock(import('./FileSystem.js'), async (importActual) => {
38
- const mocked = mockStaticMethodsOfExportedClasses(await importActual());
42
+ const mocked = mockStaticMethodsOfExports(await importActual());
39
43
  const { FileSystem } = mocked;
40
44
  const writeStream = {
41
45
  write: vi.fn().mockImplementation((_, callback) => callback()),
@@ -46,8 +50,9 @@ vi.mock(import('./FileSystem.js'), async (importActual) => {
46
50
  vi.mocked(FileSystem.createReadStream).mockReturnValue(readStream);
47
51
  return mocked;
48
52
  });
49
- vi.mock(import('./Host.js'), async (importActual) => mockStaticMethodsOfExportedClasses(await importActual()));
50
- vi.mock(import('./Http.js'), async (importActual) => mockStaticMethodsOfExportedClasses(await importActual()));
53
+ vi.mock(import('./Host.js'), async (importActual) => mockStaticMethodsOfExports(await importActual()));
54
+ vi.mock(import('./Http.js'), async (importActual) => mockStaticMethodsOfExports(await importActual()));
55
+ vi.mock(import('./Module.js'), async (importActual) => mockStaticMethodsOfExports(await importActual()));
51
56
  const logger = {
52
57
  start: vi.fn(),
53
58
  debug: vi.fn(),
@@ -59,18 +64,18 @@ const logger = {
59
64
  };
60
65
  vi.mock(import('./logger.js'), () => ({ logger }));
61
66
  vi.mock(import('./Path.js'), async (importActual) => {
62
- const mocked = mockStaticMethodsOfExportedClasses(await importActual());
67
+ const mocked = mockStaticMethodsOfExports(await importActual());
63
68
  const { Path } = mocked;
64
69
  vi.mocked(Path.isAbsolute).mockImplementation((path) => path.startsWith('/'));
65
70
  vi.mocked(Path.join).mockImplementation((...arguments_) => join(...arguments_).replaceAll('\\', '/'));
66
71
  return mocked;
67
72
  });
68
- vi.mock(import('./Process.js'), async (importActual) => mockStaticMethodsOfExportedClasses(await importActual()));
73
+ vi.mock(import('./Process.js'), async (importActual) => mockStaticMethodsOfExports(await importActual()));
69
74
  export let __lastTerminalRawModeCallback = false;
70
75
  vi.mock(import('./Terminal.js'), async (importActual) => {
71
76
  const actual = await importActual();
72
77
  const originalstripVTControlCharacters = actual.Terminal.stripVTControlCharacters;
73
- const mocked = mockStaticMethodsOfExportedClasses(actual);
78
+ const mocked = mockStaticMethodsOfExports(actual);
74
79
  const { Terminal } = mocked;
75
80
  vi.mocked(Terminal.setRawMode).mockImplementation((callback) => {
76
81
  __lastTerminalRawModeCallback = callback;
@@ -79,7 +84,7 @@ vi.mock(import('./Terminal.js'), async (importActual) => {
79
84
  return mocked;
80
85
  });
81
86
  vi.mock(import('./Thread.js'), async (importActual) => {
82
- const mocked = mockStaticMethodsOfExportedClasses(await importActual());
87
+ const mocked = mockStaticMethodsOfExports(await importActual());
83
88
  const { Thread } = mocked;
84
89
  const channel = {
85
90
  postMessage: vi.fn().mockImplementation((data) => {
@@ -101,4 +106,5 @@ vi.mock(import('./Thread.js'), async (importActual) => {
101
106
  vi.mock(import('./version.js'), () => ({
102
107
  version: vi.fn().mockResolvedValue('ui5-test-runner@1.2.3')
103
108
  }));
104
- vi.mock(import('./ZLib.js'), async (importActual) => mockStaticMethodsOfExportedClasses(await importActual()));
109
+ vi.mock(import('./ZLib.js'), async (importActual) => mockStaticMethodsOfExports(await importActual()));
110
+ vi.mock(import('./Url.js'), async (importActual) => mockStaticMethodsOfExports(await importActual()));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui5-test-runner",
3
- "version": "6.0.0-beta.2",
3
+ "version": "6.0.0-beta.5",
4
4
  "description": "Standalone test runner for UI5",
5
5
  "main": "dist/cli.js",
6
6
  "type": "module",
@@ -101,7 +101,7 @@
101
101
  "globals": "^17.6.0",
102
102
  "jsdom": "^29.1.1",
103
103
  "markdownlint-cli": "^0.48.0",
104
- "npm-check-updates": "^22.2.1",
104
+ "npm-check-updates": "^22.2.2",
105
105
  "puppeteer": "^25.1.0",
106
106
  "qunit": "^2.26.0",
107
107
  "rimraf": "^6.1.3",
@@ -110,7 +110,7 @@
110
110
  "stylelint-config-standard": "^40.0.0",
111
111
  "terser": "^5.48.0",
112
112
  "typescript": "^6.0.3",
113
- "typescript-eslint": "^8.60.0",
113
+ "typescript-eslint": "^8.60.1",
114
114
  "ui5-tooling-transpile": "^3.11.2",
115
115
  "vite": "^8.0.16",
116
116
  "vite-plugin-css-injected-by-js": "^5.0.1",