@nexustools/nwjstestrunner 0.0.0-stage → 1.0.0-beta.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.
Files changed (79) hide show
  1. package/AGENTS.md +21 -0
  2. package/README.md +142 -2
  3. package/bin/nwjstestrunner.js +13 -0
  4. package/dist/cli/nwjs-cli-launcher.d.ts +10 -0
  5. package/dist/cli/nwjs-cli-launcher.d.ts.map +1 -0
  6. package/dist/cli/nwjs-cli-launcher.js +281 -0
  7. package/dist/cli/nwjs-cli-launcher.js.map +1 -0
  8. package/dist/config/harness-config-loader.d.ts +10 -0
  9. package/dist/config/harness-config-loader.d.ts.map +1 -0
  10. package/dist/config/harness-config-loader.js +242 -0
  11. package/dist/config/harness-config-loader.js.map +1 -0
  12. package/dist/core/test-runner.d.ts +61 -0
  13. package/dist/core/test-runner.d.ts.map +1 -0
  14. package/dist/core/test-runner.js +450 -0
  15. package/dist/core/test-runner.js.map +1 -0
  16. package/dist/esm/index.d.mts +2 -0
  17. package/dist/esm/index.mjs +26 -0
  18. package/dist/harness/graphical-overlay-view.d.ts +18 -0
  19. package/dist/harness/graphical-overlay-view.d.ts.map +1 -0
  20. package/dist/harness/graphical-overlay-view.js +213 -0
  21. package/dist/harness/graphical-overlay-view.js.map +1 -0
  22. package/dist/harness/nwjs-graphical-test-harness.d.ts +40 -0
  23. package/dist/harness/nwjs-graphical-test-harness.d.ts.map +1 -0
  24. package/dist/harness/nwjs-graphical-test-harness.js +398 -0
  25. package/dist/harness/nwjs-graphical-test-harness.js.map +1 -0
  26. package/dist/harness/test-http-server.d.ts +12 -0
  27. package/dist/harness/test-http-server.d.ts.map +1 -0
  28. package/dist/harness/test-http-server.js +183 -0
  29. package/dist/harness/test-http-server.js.map +1 -0
  30. package/dist/harness/test-log-writer.d.ts +29 -0
  31. package/dist/harness/test-log-writer.d.ts.map +1 -0
  32. package/dist/harness/test-log-writer.js +173 -0
  33. package/dist/harness/test-log-writer.js.map +1 -0
  34. package/dist/harness/webview-test-controller.d.ts +18 -0
  35. package/dist/harness/webview-test-controller.d.ts.map +1 -0
  36. package/dist/harness/webview-test-controller.js +163 -0
  37. package/dist/harness/webview-test-controller.js.map +1 -0
  38. package/dist/index.d.ts +13 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +52 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/matchers/deep-equality-helper.d.ts +10 -0
  43. package/dist/matchers/deep-equality-helper.d.ts.map +1 -0
  44. package/dist/matchers/deep-equality-helper.js +188 -0
  45. package/dist/matchers/deep-equality-helper.js.map +1 -0
  46. package/dist/matchers/expect-matcher.d.ts +7 -0
  47. package/dist/matchers/expect-matcher.d.ts.map +1 -0
  48. package/dist/matchers/expect-matcher.js +261 -0
  49. package/dist/matchers/expect-matcher.js.map +1 -0
  50. package/dist/mock/jest-mock-factory.d.ts +20 -0
  51. package/dist/mock/jest-mock-factory.d.ts.map +1 -0
  52. package/dist/mock/jest-mock-factory.js +141 -0
  53. package/dist/mock/jest-mock-factory.js.map +1 -0
  54. package/dist/tests/NwjsTestRunnerSuites.d.ts +3 -0
  55. package/dist/tests/NwjsTestRunnerSuites.d.ts.map +1 -0
  56. package/dist/tests/NwjsTestRunnerSuites.js +266 -0
  57. package/dist/tests/NwjsTestRunnerSuites.js.map +1 -0
  58. package/dist/types/test-runner-types.d.ts +321 -0
  59. package/dist/types/test-runner-types.d.ts.map +1 -0
  60. package/dist/types/test-runner-types.js +3 -0
  61. package/dist/types/test-runner-types.js.map +1 -0
  62. package/package.json +56 -4
  63. package/runner/demo-client.html +70 -0
  64. package/runner/index.html +223 -0
  65. package/runner/package.json +27 -0
  66. package/src/cli/nwjs-cli-launcher.ts +243 -0
  67. package/src/config/harness-config-loader.ts +214 -0
  68. package/src/core/test-runner.ts +553 -0
  69. package/src/harness/graphical-overlay-view.ts +219 -0
  70. package/src/harness/nwjs-graphical-test-harness.ts +468 -0
  71. package/src/harness/test-http-server.ts +162 -0
  72. package/src/harness/test-log-writer.ts +154 -0
  73. package/src/harness/webview-test-controller.ts +192 -0
  74. package/src/index.ts +28 -0
  75. package/src/matchers/deep-equality-helper.ts +193 -0
  76. package/src/matchers/expect-matcher.ts +361 -0
  77. package/src/mock/jest-mock-factory.ts +170 -0
  78. package/src/tests/NwjsTestRunnerSuites.ts +267 -0
  79. package/src/types/test-runner-types.ts +335 -0
@@ -0,0 +1,223 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>NW.js Graphical Webview Test Harness</title>
6
+ <style id="nwjstestrunner-styles">
7
+ body, html {
8
+ margin: 0;
9
+ padding: 0;
10
+ width: 100vw;
11
+ height: 100vh;
12
+ background: #11111b;
13
+ overflow: hidden;
14
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
15
+ }
16
+
17
+ #webview-container {
18
+ width: 100vw;
19
+ height: 100vh;
20
+ position: absolute;
21
+ top: 0;
22
+ left: 0;
23
+ z-index: 1;
24
+ }
25
+
26
+ webview {
27
+ width: 100%;
28
+ height: 100%;
29
+ border: none;
30
+ opacity: 0.95;
31
+ }
32
+
33
+ #test-runner-overlay {
34
+ pointer-events: none;
35
+ position: fixed;
36
+ top: 0;
37
+ left: 0;
38
+ width: 100vw;
39
+ height: 100vh;
40
+ z-index: 9999;
41
+ background: rgba(0, 0, 0, 0.28);
42
+ display: flex;
43
+ flex-direction: column;
44
+ align-items: center;
45
+ justify-content: center;
46
+ color: #cdd6f4;
47
+ text-align: center;
48
+ }
49
+
50
+ .overlay-card {
51
+ background: #1e1e2e;
52
+ border: 1px solid #313244;
53
+ padding: 30px 45px;
54
+ border-radius: 12px;
55
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
56
+ min-width: 440px;
57
+ max-width: 560px;
58
+ }
59
+
60
+ .overlay-title {
61
+ font-size: 22px;
62
+ font-weight: bold;
63
+ margin-bottom: 12px;
64
+ color: #cba6f7;
65
+ }
66
+
67
+ .overlay-status {
68
+ font-size: 14px;
69
+ margin-bottom: 14px;
70
+ color: #89b4fa;
71
+ font-family: monospace;
72
+ white-space: nowrap;
73
+ overflow: hidden;
74
+ text-overflow: ellipsis;
75
+ max-width: 460px;
76
+ }
77
+
78
+ .overlay-progress-track {
79
+ width: 100%;
80
+ height: 6px;
81
+ background: #313244;
82
+ border-radius: 3px;
83
+ overflow: hidden;
84
+ margin-bottom: 16px;
85
+ }
86
+
87
+ .overlay-progress-fill {
88
+ width: 0%;
89
+ height: 100%;
90
+ background: linear-gradient(90deg, #89b4fa, #cba6f7);
91
+ transition: width 0.12s ease-out;
92
+ }
93
+
94
+ .overlay-metric {
95
+ font-size: 18px;
96
+ margin: 8px 0;
97
+ font-weight: 600;
98
+ display: flex;
99
+ align-items: center;
100
+ justify-content: space-between;
101
+ }
102
+
103
+ .status-running { color: #89b4fa; }
104
+ .status-passed { color: #a6e3a1; }
105
+ .status-failed { color: #f38ba8; }
106
+ </style>
107
+ </head>
108
+ <body>
109
+ <div id="webview-container">
110
+ <webview id="client-webview" allownw partition="trusted"></webview>
111
+ </div>
112
+
113
+ <div id="test-runner-overlay">
114
+ <div class="overlay-card">
115
+ <div id="overlay-title" class="overlay-title">🧪 NW.js Live Webview Test Runner</div>
116
+ <div id="overlay-status" class="overlay-status">Initializing test harness...</div>
117
+ <div class="overlay-progress-track">
118
+ <div id="overlay-progress-fill" class="overlay-progress-fill"></div>
119
+ </div>
120
+ <div class="overlay-metric status-running">
121
+ <span>Completed:</span>
122
+ <span id="metric-current">0 of 0</span>
123
+ </div>
124
+ <div class="overlay-metric status-passed">
125
+ <span>✅ Tests Passed:</span>
126
+ <span id="metric-passed">0</span>
127
+ </div>
128
+ <div class="overlay-metric status-failed">
129
+ <span>❌ Tests Failed:</span>
130
+ <span id="metric-failed">0</span>
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ <script>
136
+ // Bootstrap logger & initialization watchdog (isolated in its own <script> block so even a syntax error in the main harness script is caught and logged)
137
+ (function() {
138
+ try {
139
+ const fs = require('fs');
140
+ const path = require('path');
141
+ const projectRootDir = process.env.NWJS_TEST_PROJECT_ROOT || process.cwd();
142
+ const defaultResultsDir = path.join(projectRootDir, 'testrunner', 'test-results');
143
+ const testLogPath = process.env.NWJS_TEST_LOG_PATH || path.join(defaultResultsDir, 'desktop-tests.log');
144
+ fs.mkdirSync(path.dirname(testLogPath), { recursive: true });
145
+
146
+ window.__testLogPath = testLogPath;
147
+ window.__harnessInitialized = false;
148
+ window.__harnessCompleted = false;
149
+
150
+ // Immediately write "Tests interupted" on startup so if the window is closed by the user or crashes early, there is always readable output
151
+ fs.writeFileSync(
152
+ testLogPath,
153
+ [
154
+ '==================================================',
155
+ 'NW.js Desktop Client Test Harness Log',
156
+ '==================================================',
157
+ 'Started at: ' + new Date().toISOString(),
158
+ 'Status: Tests interupted'
159
+ ].join('\n'),
160
+ 'utf-8'
161
+ );
162
+
163
+ window.__appendBootstrapFailure = function(source, err) {
164
+ try {
165
+ const msg = '[ERROR][' + source + '] ' + (err && (err.stack || err.message) ? (err.stack || err.message) : String(err));
166
+ fs.writeFileSync(
167
+ testLogPath,
168
+ [
169
+ '==================================================',
170
+ 'NW.js Desktop Client Test Harness Log',
171
+ '==================================================',
172
+ 'Interrupted at: ' + new Date().toISOString(),
173
+ 'Status: Tests interupted',
174
+ 'FAILURES & ERRORS:',
175
+ msg
176
+ ].join('\n'),
177
+ 'utf-8'
178
+ );
179
+ } catch (_e) {}
180
+ };
181
+
182
+ window.__initWatchdogTimer = setTimeout(function() {
183
+ if (!window.__harnessInitialized) {
184
+ window.__appendBootstrapFailure('InitializationTimeout', 'Tests interupted: Test runner stuck at initializing and timed out after 8 seconds.');
185
+ process.exit(1);
186
+ }
187
+ }, 8000);
188
+
189
+ window.onerror = function(message, source, lineno, colno, error) {
190
+ if (!window.__harnessInitialized) {
191
+ window.__appendBootstrapFailure('BootstrapWindowOnError', error || (message + ' (' + source + ':' + lineno + ':' + colno + ')'));
192
+ process.exit(1);
193
+ }
194
+ };
195
+ } catch (_bootErr) {}
196
+ })();
197
+ </script>
198
+ <script>
199
+ (function() {
200
+ try {
201
+ const path = require('path');
202
+ const pkgRoot = process.env.NWJS_TEST_PACKAGE_ROOT || path.resolve(process.cwd());
203
+ let runnerLib;
204
+ try {
205
+ runnerLib = require(path.join(pkgRoot, 'dist', 'index.js'));
206
+ } catch (_e) {
207
+ runnerLib = require('@nexustools/nwjstestrunner');
208
+ }
209
+
210
+ const { HarnessConfigLoader, NwjsGraphicalTestHarness } = runnerLib;
211
+ const config = HarnessConfigLoader.loadConfig();
212
+ const harness = new NwjsGraphicalTestHarness(config);
213
+ harness.start();
214
+ } catch (err) {
215
+ if (window.__appendBootstrapFailure) {
216
+ window.__appendBootstrapFailure('BootstrapLoadError', err);
217
+ }
218
+ process.exit(1);
219
+ }
220
+ })();
221
+ </script>
222
+ </body>
223
+ </html>
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "nwjstestrunner-window",
3
+ "version": "1.0.0",
4
+ "main": "index.html",
5
+ "node-remote": [
6
+ "http://*",
7
+ "https://*"
8
+ ],
9
+ "chromium-args": "--allow-file-access-from-files --allow-file-access",
10
+ "window": {
11
+ "title": "NW.js Graphical Webview Test Harness",
12
+ "width": 1280,
13
+ "height": 850,
14
+ "position": "center",
15
+ "show": true
16
+ },
17
+ "webview": {
18
+ "partitions": [
19
+ {
20
+ "name": "trusted",
21
+ "accessible_resources": [
22
+ "*"
23
+ ]
24
+ }
25
+ ]
26
+ }
27
+ }
@@ -0,0 +1,243 @@
1
+ import { spawn } from 'child_process';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { HarnessConfigLoader } from '../config/harness-config-loader';
5
+ import { NwjsCliOptions } from '../types/test-runner-types';
6
+
7
+ export class NwjsCliLauncher {
8
+ public static parseCliArgs(argv: string[]): NwjsCliOptions & { help?: boolean } {
9
+ const options: NwjsCliOptions & { help?: boolean } = {
10
+ testFiles: []
11
+ };
12
+
13
+ for (let i = 0; i < argv.length; i++) {
14
+ const arg = argv[i];
15
+ if (arg === '--help' || arg === '-h') {
16
+ options.help = true;
17
+ } else if ((arg === '--project-root' || arg === '-r') && argv[i + 1]) {
18
+ options.projectRootDir = argv[++i];
19
+ } else if (arg.startsWith('--project-root=')) {
20
+ options.projectRootDir = arg.slice('--project-root='.length);
21
+ } else if ((arg === '--config' || arg === '-c') && argv[i + 1]) {
22
+ options.configPath = argv[++i];
23
+ } else if (arg.startsWith('--config=')) {
24
+ options.configPath = arg.slice('--config='.length);
25
+ } else if ((arg === '--filter' || arg === '-t') && argv[i + 1]) {
26
+ options.testFilter = argv[++i];
27
+ } else if (arg.startsWith('--filter=')) {
28
+ options.testFilter = arg.slice('--filter='.length);
29
+ } else if ((arg === '--log-path' || arg === '-o') && argv[i + 1]) {
30
+ options.logPath = argv[++i];
31
+ } else if (arg.startsWith('--log-path=')) {
32
+ options.logPath = arg.slice('--log-path='.length);
33
+ } else if (arg === '--title' && argv[i + 1]) {
34
+ options.title = argv[++i];
35
+ } else if (arg.startsWith('--title=')) {
36
+ options.title = arg.slice('--title='.length);
37
+ } else if (arg === '--webview-entry' && argv[i + 1]) {
38
+ options.webviewEntryHtml = argv[++i];
39
+ } else if (arg.startsWith('--webview-entry=')) {
40
+ options.webviewEntryHtml = arg.slice('--webview-entry='.length);
41
+ } else if (arg === '--enable-webview') {
42
+ options.enableWebview = true;
43
+ } else if (arg === '--no-webview') {
44
+ options.enableWebview = false;
45
+ } else if (arg === '--init-timeout' && argv[i + 1]) {
46
+ options.initTimeoutMs = Number(argv[++i]);
47
+ } else if (arg.startsWith('--init-timeout=')) {
48
+ options.initTimeoutMs = Number(arg.slice('--init-timeout='.length));
49
+ } else if (arg === '--timeout' && argv[i + 1]) {
50
+ options.globalTimeoutMs = Number(argv[++i]);
51
+ } else if (arg.startsWith('--timeout=')) {
52
+ options.globalTimeoutMs = Number(arg.slice('--timeout='.length));
53
+ } else if (arg === '--user-data-dir' && argv[i + 1]) {
54
+ options.userDataDir = argv[++i];
55
+ } else if (arg.startsWith('--user-data-dir=')) {
56
+ options.userDataDir = arg.slice('--user-data-dir='.length);
57
+ } else if (arg === '--nw-path' && argv[i + 1]) {
58
+ options.nwExecutablePath = argv[++i];
59
+ } else if (arg.startsWith('--nw-path=')) {
60
+ options.nwExecutablePath = arg.slice('--nw-path='.length);
61
+ } else if (arg === '--keep-open') {
62
+ options.keepOpen = true;
63
+ } else if (arg === '--test-files' && argv[i + 1]) {
64
+ const parts = argv[++i]
65
+ .split(',')
66
+ .map((s) => s.trim())
67
+ .filter(Boolean);
68
+ options.testFiles!.push(...parts);
69
+ } else if (!arg.startsWith('-')) {
70
+ options.testFiles!.push(arg);
71
+ }
72
+ }
73
+
74
+ if (options.testFiles && options.testFiles.length === 0) {
75
+ delete options.testFiles;
76
+ }
77
+
78
+ return options;
79
+ }
80
+
81
+ public static getHelpText(): string {
82
+ return [
83
+ 'Usage: nwjstestrunner [options] [testFilePatterns...]',
84
+ '',
85
+ 'Options:',
86
+ ' -r, --project-root <dir> Project root directory (default: process.cwd())',
87
+ ' -c, --config <file> Path to nwjstestrunner.config.{cjs,js,json}',
88
+ ' -t, --filter <pattern> Run only tests matching the given substring',
89
+ ' -o, --log-path <file> Output path for desktop-tests.log',
90
+ ' --title <title> Window & overlay card title',
91
+ ' --webview-entry <html> Entry HTML file to load into <webview>',
92
+ ' --enable-webview Force enable <webview> client loading',
93
+ ' --no-webview Disable <webview> and run Jest unit suites only',
94
+ ' --init-timeout <ms> Watchdog initialization timeout (default: 8000)',
95
+ ' --timeout <ms> Global harness deadline timeout (default: 35000)',
96
+ ' --user-data-dir <dir> Custom Chromium user-data-dir for NW.js',
97
+ ' --nw-path <executable> Explicit path to NW.js executable',
98
+ ' --keep-open Keep the graphical NW.js window open after tests finish',
99
+ ' -h, --help Display this help message'
100
+ ].join('\n');
101
+ }
102
+
103
+ public static resolveNwExecutable(projectRootDir: string, explicitPath?: string): string {
104
+ if (explicitPath && fs.existsSync(explicitPath)) {
105
+ return path.resolve(explicitPath);
106
+ }
107
+ if (process.env.NWJS_EXECUTABLE_PATH && fs.existsSync(process.env.NWJS_EXECUTABLE_PATH)) {
108
+ return path.resolve(process.env.NWJS_EXECUTABLE_PATH);
109
+ }
110
+
111
+ const packageRoot = path.resolve(__dirname, '..', '..');
112
+ const searchPaths = [projectRootDir, packageRoot, process.cwd()];
113
+ const isWin = process.platform === 'win32';
114
+
115
+ // Prefer direct native NW.js executable (nw.exe / nw) inside node_modules/nw/nwjs* first
116
+ for (const base of searchPaths) {
117
+ let dir = path.resolve(base);
118
+ for (let depth = 0; depth < 4; depth++) {
119
+ const nwPkgDir = path.join(dir, 'node_modules', 'nw');
120
+ if (fs.existsSync(nwPkgDir) && fs.statSync(nwPkgDir).isDirectory()) {
121
+ try {
122
+ const subEntries = fs.readdirSync(nwPkgDir, { withFileTypes: true });
123
+ for (const sub of subEntries) {
124
+ if ((sub.isDirectory() || sub.isSymbolicLink()) && sub.name.startsWith('nwjs')) {
125
+ const candidate = isWin
126
+ ? path.join(nwPkgDir, sub.name, 'nw.exe')
127
+ : process.platform === 'darwin'
128
+ ? path.join(nwPkgDir, sub.name, 'nwjs.app', 'Contents', 'MacOS', 'nwjs')
129
+ : path.join(nwPkgDir, sub.name, 'nw');
130
+ if (fs.existsSync(candidate)) {
131
+ return candidate;
132
+ }
133
+ }
134
+ }
135
+ } catch (_e) {}
136
+ }
137
+ const parent = path.dirname(dir);
138
+ if (parent === dir) break;
139
+ dir = parent;
140
+ }
141
+ }
142
+
143
+ for (const base of searchPaths) {
144
+ try {
145
+ const nwModPath = require.resolve('nw', { paths: [base] });
146
+ const nwMod = require(nwModPath);
147
+ if (nwMod && typeof nwMod.findpath === 'function') {
148
+ const binPath = nwMod.findpath();
149
+ if (typeof binPath === 'string' && fs.existsSync(binPath)) {
150
+ return binPath;
151
+ }
152
+ }
153
+ } catch (_e) {}
154
+ }
155
+
156
+ for (const base of searchPaths) {
157
+ let dir = path.resolve(base);
158
+ for (let depth = 0; depth < 4; depth++) {
159
+ const dotBin = isWin
160
+ ? path.join(dir, 'node_modules', '.bin', 'nw.cmd')
161
+ : path.join(dir, 'node_modules', '.bin', 'nw');
162
+ if (typeof dotBin === 'string' && fs.existsSync(dotBin)) {
163
+ return dotBin;
164
+ }
165
+ const parent = path.dirname(dir);
166
+ if (parent === dir) break;
167
+ dir = parent;
168
+ }
169
+ }
170
+
171
+ return isWin ? 'nw.exe' : 'nw';
172
+ }
173
+
174
+ public static async launch(cliOptions: NwjsCliOptions = {}): Promise<number> {
175
+ const projectRootDir = HarnessConfigLoader.resolveProjectRoot(cliOptions.projectRootDir);
176
+ const configFile = HarnessConfigLoader.findConfigFile(projectRootDir, cliOptions.configPath);
177
+ const mergedConfig = HarnessConfigLoader.loadConfig(
178
+ projectRootDir,
179
+ configFile || undefined,
180
+ cliOptions
181
+ );
182
+
183
+ const logPath =
184
+ mergedConfig.testLogPath ||
185
+ path.join(projectRootDir, 'testrunner', 'test-results', 'desktop-tests.log');
186
+
187
+ const userDataDir = cliOptions.userDataDir
188
+ ? path.resolve(projectRootDir, cliOptions.userDataDir)
189
+ : path.join(projectRootDir, '.nw-userdata-test');
190
+
191
+ const packageRoot = path.resolve(__dirname, '..', '..');
192
+ const runnerDir = path.join(packageRoot, 'runner');
193
+ const nwExe = NwjsCliLauncher.resolveNwExecutable(projectRootDir, cliOptions.nwExecutablePath);
194
+
195
+ const args = [runnerDir, `--user-data-dir=${userDataDir}`, ...(cliOptions.headlessArgs || [])];
196
+
197
+ const env: NodeJS.ProcessEnv = {
198
+ ...process.env,
199
+ NWJS_TEST_PROJECT_ROOT: projectRootDir,
200
+ NWJS_TEST_LOG_PATH: logPath,
201
+ NWJS_TEST_PACKAGE_ROOT: packageRoot,
202
+ NWJS_TEST_CLI_OVERRIDES: JSON.stringify(cliOptions)
203
+ };
204
+ if (configFile) {
205
+ env.NWJS_TEST_CONFIG_PATH = configFile;
206
+ }
207
+
208
+ const useShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(nwExe);
209
+
210
+ return new Promise<number>((resolve) => {
211
+ const child = spawn(nwExe, args, {
212
+ cwd: projectRootDir,
213
+ env,
214
+ stdio: 'inherit',
215
+ shell: useShell
216
+ });
217
+
218
+ child.on('error', (err) => {
219
+ console.error(`[nwjstestrunner] Failed to spawn NW.js (${nwExe}):`, err.message);
220
+ resolve(1);
221
+ });
222
+
223
+ child.on('close', (code) => {
224
+ let logContent = '';
225
+ if (fs.existsSync(logPath)) {
226
+ try {
227
+ logContent = fs.readFileSync(logPath, 'utf-8');
228
+ console.log('\n' + logContent + '\n');
229
+ } catch (_e) {}
230
+ }
231
+
232
+ const logFailed =
233
+ !logContent ||
234
+ logContent.includes('Status: Tests interupted') ||
235
+ logContent.includes('FAILURES & ERRORS:') ||
236
+ !logContent.includes('SUCCESS: All tests completed cleanly.');
237
+
238
+ const exitCode = code === 0 && !logFailed ? 0 : 1;
239
+ resolve(exitCode);
240
+ });
241
+ });
242
+ }
243
+ }