@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,219 @@
1
+ import { GraphicalOverlayOptions } from '../types/test-runner-types';
2
+
3
+ export class GraphicalOverlayView {
4
+ private readonly options: GraphicalOverlayOptions;
5
+ private webviewEl: any = null;
6
+ private titleEl: HTMLElement | null = null;
7
+ private statusEl: HTMLElement | null = null;
8
+ private metricCurrentEl: HTMLElement | null = null;
9
+ private metricPassedEl: HTMLElement | null = null;
10
+ private metricFailedEl: HTMLElement | null = null;
11
+ private progressFillEl: HTMLElement | null = null;
12
+
13
+ constructor(options: GraphicalOverlayOptions = {}) {
14
+ this.options = options;
15
+ }
16
+
17
+ public mount(doc: Document = document): void {
18
+ const title = this.options.title || '🧪 NW.js Live Webview Test Runner';
19
+ const enableWebview = this.options.enableWebview ?? true;
20
+ const partition = this.options.webviewPartition || 'trusted';
21
+ const allowNw = this.options.allowNwInWebview ?? true;
22
+
23
+ if (!doc.getElementById('nwjstestrunner-styles')) {
24
+ const styleEl = doc.createElement('style');
25
+ styleEl.id = 'nwjstestrunner-styles';
26
+ styleEl.textContent = `
27
+ body, html {
28
+ margin: 0;
29
+ padding: 0;
30
+ width: 100vw;
31
+ height: 100vh;
32
+ background: #11111b;
33
+ overflow: hidden;
34
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
35
+ }
36
+ #webview-container {
37
+ width: 100vw;
38
+ height: 100vh;
39
+ position: absolute;
40
+ top: 0;
41
+ left: 0;
42
+ z-index: 1;
43
+ }
44
+ webview {
45
+ width: 100%;
46
+ height: 100%;
47
+ border: none;
48
+ opacity: 0.95;
49
+ }
50
+ #test-runner-overlay {
51
+ pointer-events: none;
52
+ position: fixed;
53
+ top: 0;
54
+ left: 0;
55
+ width: 100vw;
56
+ height: 100vh;
57
+ z-index: 9999;
58
+ background: rgba(0, 0, 0, 0.28);
59
+ display: flex;
60
+ flex-direction: column;
61
+ align-items: center;
62
+ justify-content: center;
63
+ color: #cdd6f4;
64
+ text-align: center;
65
+ }
66
+ .overlay-card {
67
+ background: #1e1e2e;
68
+ border: 1px solid #313244;
69
+ padding: 30px 45px;
70
+ border-radius: 12px;
71
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
72
+ min-width: 440px;
73
+ max-width: 560px;
74
+ }
75
+ .overlay-title {
76
+ font-size: 22px;
77
+ font-weight: bold;
78
+ margin-bottom: 12px;
79
+ color: #cba6f7;
80
+ }
81
+ .overlay-status {
82
+ font-size: 14px;
83
+ margin-bottom: 14px;
84
+ color: #89b4fa;
85
+ font-family: monospace;
86
+ white-space: nowrap;
87
+ overflow: hidden;
88
+ text-overflow: ellipsis;
89
+ max-width: 460px;
90
+ }
91
+ .overlay-progress-track {
92
+ width: 100%;
93
+ height: 6px;
94
+ background: #313244;
95
+ border-radius: 3px;
96
+ overflow: hidden;
97
+ margin-bottom: 16px;
98
+ }
99
+ .overlay-progress-fill {
100
+ width: 0%;
101
+ height: 100%;
102
+ background: linear-gradient(90deg, #89b4fa, #cba6f7);
103
+ transition: width 0.12s ease-out;
104
+ }
105
+ .overlay-metric {
106
+ font-size: 18px;
107
+ margin: 8px 0;
108
+ font-weight: 600;
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: space-between;
112
+ }
113
+ .status-running { color: #89b4fa; }
114
+ .status-passed { color: #a6e3a1; }
115
+ .status-failed { color: #f38ba8; }
116
+ `;
117
+ doc.head.appendChild(styleEl);
118
+ }
119
+
120
+ if (enableWebview && !doc.getElementById('client-webview')) {
121
+ let container = doc.getElementById('webview-container');
122
+ if (!container) {
123
+ container = doc.createElement('div');
124
+ container.id = 'webview-container';
125
+ doc.body.appendChild(container);
126
+ }
127
+ const wv = doc.createElement('webview');
128
+ wv.id = 'client-webview';
129
+ if (allowNw) {
130
+ wv.setAttribute('allownw', '');
131
+ }
132
+ wv.setAttribute('partition', partition);
133
+ container.appendChild(wv);
134
+ }
135
+
136
+ if (!doc.getElementById('test-runner-overlay')) {
137
+ const overlay = doc.createElement('div');
138
+ overlay.id = 'test-runner-overlay';
139
+ overlay.innerHTML = `
140
+ <div class="overlay-card">
141
+ <div id="overlay-title" class="overlay-title"></div>
142
+ <div id="overlay-status" class="overlay-status">Initializing test harness...</div>
143
+ <div class="overlay-progress-track">
144
+ <div id="overlay-progress-fill" class="overlay-progress-fill"></div>
145
+ </div>
146
+ <div class="overlay-metric status-running">
147
+ <span>Completed:</span>
148
+ <span id="metric-current">0 of 0</span>
149
+ </div>
150
+ <div class="overlay-metric status-passed">
151
+ <span>✅ Tests Passed:</span>
152
+ <span id="metric-passed">0</span>
153
+ </div>
154
+ <div class="overlay-metric status-failed">
155
+ <span>❌ Tests Failed:</span>
156
+ <span id="metric-failed">0</span>
157
+ </div>
158
+ </div>
159
+ `;
160
+ doc.body.appendChild(overlay);
161
+ }
162
+
163
+ this.webviewEl = doc.getElementById('client-webview');
164
+ this.titleEl =
165
+ doc.getElementById('overlay-title') ||
166
+ (doc.querySelector('#test-runner-overlay .overlay-title') as HTMLElement | null);
167
+ this.statusEl = doc.getElementById('overlay-status');
168
+ this.metricCurrentEl = doc.getElementById('metric-current');
169
+ this.metricPassedEl = doc.getElementById('metric-passed');
170
+ this.metricFailedEl = doc.getElementById('metric-failed');
171
+ this.progressFillEl = doc.getElementById('overlay-progress-fill');
172
+
173
+ if (this.titleEl && this.options.title) {
174
+ this.titleEl.textContent = title;
175
+ }
176
+ try {
177
+ if (this.options.title && doc.title !== this.options.title) {
178
+ doc.title = this.options.title;
179
+ }
180
+ } catch (_e) {}
181
+ }
182
+
183
+ public getWebviewElement(): any {
184
+ return this.webviewEl;
185
+ }
186
+
187
+ public setTitle(title: string): void {
188
+ if (this.titleEl) {
189
+ this.titleEl.textContent = title;
190
+ }
191
+ }
192
+
193
+ public setStatus(statusText: string): void {
194
+ if (this.statusEl) {
195
+ this.statusEl.textContent = statusText;
196
+ }
197
+ }
198
+
199
+ public updateMetrics(completed: number, total: number, passed: number, failed: number): void {
200
+ if (this.metricCurrentEl) {
201
+ this.metricCurrentEl.textContent = `${completed} of ${total}`;
202
+ }
203
+ if (this.metricPassedEl) {
204
+ this.metricPassedEl.textContent = `${passed}`;
205
+ }
206
+ if (this.metricFailedEl) {
207
+ this.metricFailedEl.textContent = `${failed}`;
208
+ }
209
+ if (this.progressFillEl) {
210
+ const pct = total > 0 ? Math.min(100, Math.round((completed / total) * 100)) : 0;
211
+ this.progressFillEl.style.width = `${pct}%`;
212
+ if (failed > 0) {
213
+ this.progressFillEl.style.background = 'linear-gradient(90deg, #f38ba8, #eba0ac)';
214
+ } else if (completed >= total && total > 0) {
215
+ this.progressFillEl.style.background = 'linear-gradient(90deg, #a6e3a1, #94e2d5)';
216
+ }
217
+ }
218
+ }
219
+ }
@@ -0,0 +1,468 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { HarnessConfigLoader } from '../config/harness-config-loader';
5
+ import { installJestGlobals, TestRunner } from '../core/test-runner';
6
+ import { NwjsHarnessConfig, TestProgressEvent } from '../types/test-runner-types';
7
+ import { GraphicalOverlayView } from './graphical-overlay-view';
8
+ import { TestHttpServer } from './test-http-server';
9
+ import { TestLogWriter } from './test-log-writer';
10
+ import { WebviewTestController } from './webview-test-controller';
11
+
12
+ export class NwjsGraphicalTestHarness {
13
+ private readonly config: NwjsHarnessConfig;
14
+ private readonly projectRootDir: string;
15
+ private readonly testSandboxDir: string;
16
+ private readonly testConfigDir: string;
17
+ private readonly logWriter: TestLogWriter;
18
+ private readonly overlayView: GraphicalOverlayView;
19
+ private httpServer: TestHttpServer | null = null;
20
+ private webviewController: WebviewTestController | null = null;
21
+ private initTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
22
+ private globalTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
23
+ private hasStartedExecution = false;
24
+ private hasFinished = false;
25
+
26
+ constructor(config: NwjsHarnessConfig = {}) {
27
+ this.config = config;
28
+ this.projectRootDir = HarnessConfigLoader.resolveProjectRoot(config.projectRootDir);
29
+
30
+ const sandboxPrefix = config.sandboxPrefix || 'nwjs-webview-test-';
31
+ this.testSandboxDir = fs.mkdtempSync(path.join(os.tmpdir(), sandboxPrefix));
32
+ this.testConfigDir = path.join(this.testSandboxDir, 'os-config');
33
+ fs.mkdirSync(this.testConfigDir, { recursive: true });
34
+
35
+ const defaultLogPath = path.join(
36
+ this.projectRootDir,
37
+ 'testrunner',
38
+ 'test-results',
39
+ 'desktop-tests.log'
40
+ );
41
+ const winAny = typeof window !== 'undefined' ? (window as any) : undefined;
42
+ const resolvedLogPath =
43
+ config.testLogPath ||
44
+ (winAny && winAny.__testLogPath) ||
45
+ process.env.NWJS_TEST_LOG_PATH ||
46
+ defaultLogPath;
47
+
48
+ const headerTitle =
49
+ config.logHeaderTitle ||
50
+ (config.title ? `${config.title} Log` : 'NW.js Desktop Client Test Harness Log');
51
+
52
+ this.logWriter = new TestLogWriter(resolvedLogPath, headerTitle);
53
+ this.overlayView = new GraphicalOverlayView({
54
+ title: config.title || '🧪 NW.js Live Webview Test Runner',
55
+ enableWebview: this.shouldEnableWebview()
56
+ });
57
+ }
58
+
59
+ private shouldEnableWebview(): boolean {
60
+ if (typeof this.config.enableWebview === 'boolean') {
61
+ return this.config.enableWebview;
62
+ }
63
+ if (this.config.liveWebviewTests && this.config.liveWebviewTests.length > 0) {
64
+ return true;
65
+ }
66
+ if (this.config.webviewEntryHtml) {
67
+ return true;
68
+ }
69
+ return false;
70
+ }
71
+
72
+ public getLogWriter(): TestLogWriter {
73
+ return this.logWriter;
74
+ }
75
+
76
+ public getProjectRootDir(): string {
77
+ return this.projectRootDir;
78
+ }
79
+
80
+ public getSandboxDir(): string {
81
+ return this.testSandboxDir;
82
+ }
83
+
84
+ public getConfigDir(): string {
85
+ return this.testConfigDir;
86
+ }
87
+
88
+ public async start(): Promise<{ exitCode: number; logText: string }> {
89
+ this.logWriter.writeProgressSnapshot('Tests interupted (Initializing test harness...)');
90
+
91
+ if (typeof document !== 'undefined') {
92
+ this.overlayView.mount(document);
93
+ }
94
+
95
+ this.setupWindowAndProcessLifecycle();
96
+
97
+ const initTimeoutMs = this.config.initTimeoutMs ?? 8000;
98
+ const globalTimeoutMs = this.config.globalTimeoutMs ?? 35000;
99
+
100
+ this.initTimeoutTimer = setTimeout(() => {
101
+ if (!this.hasStartedExecution && !this.hasFinished) {
102
+ this.logWriter.recordError(
103
+ 'InitializationTimeout',
104
+ `Tests interupted: Test runner failed to initialize within ${initTimeoutMs}ms.`
105
+ );
106
+ this.finishAndExit();
107
+ }
108
+ }, initTimeoutMs);
109
+
110
+ this.globalTimeoutTimer = setTimeout(() => {
111
+ if (!this.hasFinished) {
112
+ this.logWriter.recordError(
113
+ 'GlobalHarnessTimeout',
114
+ `Test runner harness timed out after ${globalTimeoutMs}ms.`
115
+ );
116
+ this.finishAndExit();
117
+ }
118
+ }, globalTimeoutMs);
119
+
120
+ try {
121
+ await this.prepareTestSuites();
122
+
123
+ if (this.shouldEnableWebview() && typeof document !== 'undefined') {
124
+ const webviewEl = this.overlayView.getWebviewElement();
125
+ if (!webviewEl) {
126
+ this.logWriter.recordError(
127
+ 'WebviewElementMissing',
128
+ 'Expected #client-webview element in DOM when enableWebview is true'
129
+ );
130
+ return await this.finishAndExit();
131
+ }
132
+
133
+ this.webviewController = new WebviewTestController(webviewEl, {
134
+ consoleErrorLevel: this.config.webviewConsoleErrorLevel ?? 2,
135
+ onError: (source, err) => this.logWriter.recordError(source, err),
136
+ onLoadAbort: () => {
137
+ this.finishAndExit();
138
+ }
139
+ });
140
+
141
+ return await new Promise<{ exitCode: number; logText: string }>((resolve) => {
142
+ this.webviewController!.attachEventListeners(() => {
143
+ if (this.hasStartedExecution) return;
144
+ this.markExecutionStarted();
145
+ this.executeAllTests()
146
+ .then((res) => resolve(res))
147
+ .catch((err) => {
148
+ this.logWriter.recordError('TestRunnerExecution', err);
149
+ this.finishAndExit().then(resolve);
150
+ });
151
+ });
152
+
153
+ this.startHttpServerAndNavigateWebview().catch((err) => {
154
+ this.logWriter.recordError('HttpServerStartError', err);
155
+ this.finishAndExit().then(resolve);
156
+ });
157
+ });
158
+ } else {
159
+ this.markExecutionStarted();
160
+ return await this.executeAllTests();
161
+ }
162
+ } catch (err) {
163
+ this.logWriter.recordError('TopLevelHarnessError', err);
164
+ return await this.finishAndExit();
165
+ }
166
+ }
167
+
168
+ private markExecutionStarted(): void {
169
+ this.hasStartedExecution = true;
170
+ if (this.initTimeoutTimer) {
171
+ clearTimeout(this.initTimeoutTimer);
172
+ this.initTimeoutTimer = null;
173
+ }
174
+ if (typeof window !== 'undefined') {
175
+ const winAny = window as any;
176
+ winAny.__harnessInitialized = true;
177
+ if (winAny.__initWatchdogTimer) {
178
+ clearTimeout(winAny.__initWatchdogTimer);
179
+ }
180
+ }
181
+ }
182
+
183
+ private setupWindowAndProcessLifecycle(): void {
184
+ try {
185
+ const nwGlobal = typeof (globalThis as any).nw !== 'undefined' ? (globalThis as any).nw : undefined;
186
+ if (nwGlobal && nwGlobal.Window && typeof nwGlobal.Window.get === 'function') {
187
+ const win = nwGlobal.Window.get();
188
+ win.on('close', () => {
189
+ if (!this.hasFinished) {
190
+ const m = this.logWriter.getMetrics();
191
+ this.logWriter.writeProgressSnapshot(
192
+ `Tests interupted (Closed by user at ${m.passedCount + m.failedCount} of ${m.totalTests})`
193
+ );
194
+ }
195
+ try {
196
+ win.close(true);
197
+ } catch (_e) {}
198
+ process.exit(1);
199
+ });
200
+ }
201
+ } catch (_e) {}
202
+
203
+ if (typeof window !== 'undefined') {
204
+ window.onerror = (message, _source, _lineno, _colno, error) => {
205
+ this.logWriter.recordError('HostWindowOnError', error || message);
206
+ this.finishAndExit();
207
+ };
208
+
209
+ window.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => {
210
+ this.logWriter.recordError('HostUnhandledRejection', e.reason);
211
+ this.finishAndExit();
212
+ });
213
+ }
214
+ }
215
+
216
+ private async startHttpServerAndNavigateWebview(): Promise<void> {
217
+ const entryHtml = this.config.webviewEntryHtml || 'index.html';
218
+ this.httpServer = new TestHttpServer({
219
+ projectRootDir: this.projectRootDir,
220
+ defaultEntryFile: entryHtml,
221
+ ignore404Paths: this.config.ignore404Paths,
222
+ routeHandler: this.config.httpRouteHandler,
223
+ onHttp404: (reqPath, resolvedFilePath) => {
224
+ this.logWriter.recordError(
225
+ 'Http404Error',
226
+ `HTTP 404 Not Found: ${reqPath} (resolved: ${resolvedFilePath})`
227
+ );
228
+ }
229
+ });
230
+
231
+ const port = await this.httpServer.start();
232
+ const queryParams = new URLSearchParams();
233
+ if (this.config.injectSandboxQueryParams !== false) {
234
+ queryParams.set('projectRoot', this.testSandboxDir);
235
+ queryParams.set('configDir', this.testConfigDir);
236
+ }
237
+ if (this.config.webviewQueryParams) {
238
+ for (const [k, v] of Object.entries(this.config.webviewQueryParams)) {
239
+ queryParams.set(k, v);
240
+ }
241
+ }
242
+
243
+ const cleanEntry = entryHtml.startsWith('/') ? entryHtml.slice(1) : entryHtml;
244
+ const qs = queryParams.toString();
245
+ const targetUrl = `http://127.0.0.1:${port}/${cleanEntry}${qs ? `?${qs}` : ''}`;
246
+ this.webviewController?.navigate(targetUrl);
247
+ }
248
+
249
+ private async prepareTestSuites(): Promise<void> {
250
+ installJestGlobals(globalThis);
251
+ if (typeof window !== 'undefined') {
252
+ installJestGlobals(window);
253
+ }
254
+
255
+ const runner = TestRunner.getInstance();
256
+ runner.reset();
257
+
258
+ const context = {
259
+ projectRootDir: this.projectRootDir,
260
+ sandboxDir: this.testSandboxDir,
261
+ configDir: this.testConfigDir
262
+ };
263
+
264
+ if (this.config.registerSuites) {
265
+ await this.config.registerSuites(runner, context);
266
+ }
267
+
268
+ const shouldLoadFiles =
269
+ (this.config.testFiles && this.config.testFiles.length > 0) || !this.config.registerSuites;
270
+
271
+ if (shouldLoadFiles) {
272
+ const files = HarnessConfigLoader.resolveTestFiles(
273
+ this.projectRootDir,
274
+ this.config.testFiles
275
+ );
276
+ for (const filePath of files) {
277
+ try {
278
+ delete require.cache[require.resolve(filePath)];
279
+ } catch (_e) {}
280
+ const mod = require(filePath);
281
+ if (typeof mod === 'function') {
282
+ await mod(runner, context);
283
+ } else if (mod && typeof mod === 'object') {
284
+ for (const [exportName, exportVal] of Object.entries(mod)) {
285
+ if (
286
+ typeof exportVal === 'function' &&
287
+ (exportName === 'registerSuites' ||
288
+ exportName === 'registerTestSuites' ||
289
+ (/^register.*Suites$/i.test(exportName)))
290
+ ) {
291
+ await (exportVal as any)(runner, context);
292
+ }
293
+ }
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ private async executeAllTests(): Promise<{ exitCode: number; logText: string }> {
300
+ try {
301
+ const runner = TestRunner.getInstance();
302
+ const liveTests = this.config.liveWebviewTests || [];
303
+ const unitCount = runner.totalCasesCount;
304
+ const totalTests = unitCount + liveTests.length;
305
+
306
+ this.logWriter.setTotalTests(totalTests);
307
+ this.overlayView.updateMetrics(0, totalTests, 0, 0);
308
+ this.overlayView.setStatus(`Running ${totalTests} tests...`);
309
+ this.logWriter.writeProgressSnapshot(
310
+ `Tests interupted (Running unit suites: 0 of ${totalTests})`
311
+ );
312
+
313
+ if (this.webviewController && this.shouldEnableWebview()) {
314
+ const screenLoaded = await this.webviewController.waitForScreenLoad(
315
+ this.config.screenLoadOptions
316
+ );
317
+ if (!screenLoaded) {
318
+ this.logWriter.recordError(
319
+ 'WebviewScreenTimeout',
320
+ 'Client screen inside webview failed readiness check within timeout.'
321
+ );
322
+ }
323
+ }
324
+
325
+ if (unitCount > 0) {
326
+ const jestSummary = await runner.runAll({
327
+ maxDurationMs: this.config.unitSuiteTimeoutMs ?? 15000,
328
+ defaultTestTimeoutMs: this.config.defaultTestTimeoutMs ?? 2000,
329
+ filter: this.config.testFilter,
330
+ onProgress: (ev: TestProgressEvent) => {
331
+ if (ev.type === 'test_start') {
332
+ this.overlayView.setStatus(`Running: ${ev.suiteName} > ${ev.testName}`);
333
+ } else if (ev.type === 'test_complete') {
334
+ this.logWriter.setCounts(ev.passedCount, ev.failedCount);
335
+ this.overlayView.updateMetrics(
336
+ ev.completedCount,
337
+ totalTests,
338
+ ev.passedCount,
339
+ ev.failedCount
340
+ );
341
+ this.logWriter.writeProgressSnapshot(
342
+ `Tests interupted (Running unit suites: ${ev.completedCount} of ${totalTests})`
343
+ );
344
+ }
345
+ }
346
+ });
347
+
348
+ this.logWriter.setCounts(jestSummary.passed, jestSummary.failed);
349
+ this.overlayView.updateMetrics(
350
+ jestSummary.passed + jestSummary.failed,
351
+ totalTests,
352
+ jestSummary.passed,
353
+ jestSummary.failed
354
+ );
355
+ this.logWriter.writeProgressSnapshot(
356
+ `Tests interupted (Completed unit suites: ${jestSummary.passed + jestSummary.failed} of ${totalTests})`
357
+ );
358
+
359
+ if (jestSummary.failures && jestSummary.failures.length > 0) {
360
+ for (const f of jestSummary.failures) {
361
+ this.logWriter.recordError(
362
+ `JestSuite[${f.suiteName}]`,
363
+ `${f.testName} -> ${f.error?.message || f.error}`
364
+ );
365
+ }
366
+ }
367
+ }
368
+
369
+ if (this.webviewController && liveTests.length > 0) {
370
+ const totalLive = liveTests.length;
371
+ for (let i = 0; i < totalLive; i++) {
372
+ const spec = liveTests[i];
373
+ const completedSoFar = unitCount + i + 1;
374
+ this.overlayView.setStatus(`Running [${i + 1}/${totalLive}]: ${spec.name}`);
375
+ this.logWriter.writeProgressSnapshot(
376
+ `Tests interupted (Running live test [${i + 1}/${totalLive}]: ${spec.id})`
377
+ );
378
+
379
+ const res = await this.webviewController.executeWebviewTest(
380
+ spec.code,
381
+ spec.timeoutMs ?? 4000
382
+ );
383
+ if (res.ok) {
384
+ this.logWriter.incrementPassed(1);
385
+ } else {
386
+ this.logWriter.incrementFailed(1);
387
+ const errs =
388
+ res.errors && res.errors.length > 0
389
+ ? res.errors
390
+ : [res.error || 'Unknown webview test error'];
391
+ for (const err of errs) {
392
+ this.logWriter.recordError(spec.id, err);
393
+ }
394
+ }
395
+
396
+ const m = this.logWriter.getMetrics();
397
+ this.overlayView.updateMetrics(completedSoFar, totalTests, m.passedCount, m.failedCount);
398
+ }
399
+ }
400
+
401
+ const finalMetrics = this.logWriter.getMetrics();
402
+ this.overlayView.setStatus('All tests completed!');
403
+ this.overlayView.updateMetrics(
404
+ totalTests,
405
+ totalTests,
406
+ finalMetrics.passedCount,
407
+ finalMetrics.failedCount
408
+ );
409
+ } catch (err) {
410
+ this.logWriter.recordError('TestRunnerExecution', err);
411
+ }
412
+
413
+ return await this.finishAndExit();
414
+ }
415
+
416
+ public async finishAndExit(): Promise<{ exitCode: number; logText: string }> {
417
+ if (this.hasFinished) {
418
+ return this.logWriter.writeFinalSummary();
419
+ }
420
+ this.hasFinished = true;
421
+
422
+ if (typeof window !== 'undefined') {
423
+ (window as any).__harnessCompleted = true;
424
+ if ((window as any).__initWatchdogTimer) {
425
+ clearTimeout((window as any).__initWatchdogTimer);
426
+ }
427
+ }
428
+
429
+ if (this.initTimeoutTimer) {
430
+ clearTimeout(this.initTimeoutTimer);
431
+ this.initTimeoutTimer = null;
432
+ }
433
+ if (this.globalTimeoutTimer) {
434
+ clearTimeout(this.globalTimeoutTimer);
435
+ this.globalTimeoutTimer = null;
436
+ }
437
+
438
+ if (this.httpServer) {
439
+ await this.httpServer.stop();
440
+ this.httpServer = null;
441
+ }
442
+
443
+ try {
444
+ if (this.testSandboxDir && fs.existsSync(this.testSandboxDir)) {
445
+ fs.rmSync(this.testSandboxDir, { recursive: true, force: true });
446
+ }
447
+ } catch (_e) {}
448
+
449
+ const result = this.logWriter.writeFinalSummary();
450
+ console.log(`Test runner finished with exit code ${result.exitCode}`);
451
+
452
+ const autoExit = this.config.autoExit ?? true;
453
+ const keepOpen = this.config.keepOpen ?? false;
454
+ if (autoExit && !keepOpen) {
455
+ process.exit(result.exitCode);
456
+ }
457
+
458
+ return result;
459
+ }
460
+ }
461
+
462
+ export async function runNwjsTestHarness(
463
+ config?: NwjsHarnessConfig
464
+ ): Promise<{ exitCode: number; logText: string }> {
465
+ const effectiveConfig = config || HarnessConfigLoader.loadConfig();
466
+ const harness = new NwjsGraphicalTestHarness(effectiveConfig);
467
+ return harness.start();
468
+ }