@git.zone/tstest 1.11.5 → 2.2.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 (47) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/index.js +24 -2
  3. package/dist_ts/tstest.classes.tap.parser.d.ts +10 -5
  4. package/dist_ts/tstest.classes.tap.parser.js +239 -99
  5. package/dist_ts/tstest.classes.tap.parser.old.d.ts +50 -0
  6. package/dist_ts/tstest.classes.tap.parser.old.js +332 -0
  7. package/dist_ts/tstest.classes.tstest.d.ts +1 -0
  8. package/dist_ts/tstest.classes.tstest.js +93 -4
  9. package/dist_ts/tstest.logging.d.ts +4 -0
  10. package/dist_ts/tstest.logging.js +36 -1
  11. package/dist_ts/tstest.plugins.d.ts +2 -1
  12. package/dist_ts/tstest.plugins.js +3 -2
  13. package/dist_ts_tapbundle/index.d.ts +1 -3
  14. package/dist_ts_tapbundle/index.js +3 -5
  15. package/dist_ts_tapbundle/tapbundle.classes.settingsmanager.d.ts +36 -0
  16. package/dist_ts_tapbundle/tapbundle.classes.settingsmanager.js +114 -0
  17. package/dist_ts_tapbundle/tapbundle.classes.tap.d.ts +26 -3
  18. package/dist_ts_tapbundle/tapbundle.classes.tap.js +218 -22
  19. package/dist_ts_tapbundle/tapbundle.classes.taptest.d.ts +5 -0
  20. package/dist_ts_tapbundle/tapbundle.classes.taptest.js +161 -7
  21. package/dist_ts_tapbundle/tapbundle.classes.taptools.d.ts +14 -0
  22. package/dist_ts_tapbundle/tapbundle.classes.taptools.js +24 -1
  23. package/dist_ts_tapbundle/tapbundle.expect.wrapper.d.ts +11 -0
  24. package/dist_ts_tapbundle/tapbundle.expect.wrapper.js +71 -0
  25. package/dist_ts_tapbundle/tapbundle.interfaces.d.ts +27 -0
  26. package/dist_ts_tapbundle/tapbundle.interfaces.js +2 -0
  27. package/dist_ts_tapbundle/tapbundle.utilities.diff.d.ts +5 -0
  28. package/dist_ts_tapbundle/tapbundle.utilities.diff.js +179 -0
  29. package/dist_ts_tapbundle_protocol/index.d.ts +6 -0
  30. package/dist_ts_tapbundle_protocol/index.js +11 -0
  31. package/dist_ts_tapbundle_protocol/protocol.emitter.d.ts +55 -0
  32. package/dist_ts_tapbundle_protocol/protocol.emitter.js +155 -0
  33. package/dist_ts_tapbundle_protocol/protocol.parser.d.ts +79 -0
  34. package/dist_ts_tapbundle_protocol/protocol.parser.js +359 -0
  35. package/dist_ts_tapbundle_protocol/protocol.types.d.ts +111 -0
  36. package/dist_ts_tapbundle_protocol/protocol.types.js +19 -0
  37. package/package.json +2 -1
  38. package/readme.hints.md +166 -5
  39. package/readme.md +134 -3
  40. package/readme.plan.md +145 -56
  41. package/ts/00_commitinfo_data.ts +1 -1
  42. package/ts/index.ts +22 -1
  43. package/ts/tspublish.json +1 -1
  44. package/ts/tstest.classes.tap.parser.ts +271 -110
  45. package/ts/tstest.classes.tstest.ts +112 -5
  46. package/ts/tstest.logging.ts +43 -0
  47. package/ts/tstest.plugins.ts +2 -0
@@ -8,28 +8,27 @@ import * as plugins from './tstest.plugins.js';
8
8
  import { TapTestResult } from './tstest.classes.tap.testresult.js';
9
9
  import * as logPrefixes from './tstest.logprefixes.js';
10
10
  import { TsTestLogger } from './tstest.logging.js';
11
+ import { ProtocolParser } from '../dist_ts_tapbundle_protocol/index.js';
12
+ import type { IProtocolMessage, ITestResult, IPlanLine, IErrorBlock, ITestEvent } from '../dist_ts_tapbundle_protocol/index.js';
11
13
 
12
14
  export class TapParser {
13
15
  testStore: TapTestResult[] = [];
14
16
 
15
- expectedTestsRegex = /([0-9]*)\.\.([0-9]*)$/;
16
- expectedTests: number;
17
- receivedTests: number;
17
+ expectedTests: number = 0;
18
+ receivedTests: number = 0;
18
19
 
19
- testStatusRegex = /(ok|not\sok)\s([0-9]+)\s-\s(.*?)(\s#\s(.*))?$/;
20
20
  activeTapTestResult: TapTestResult;
21
- collectingErrorDetails: boolean = false;
22
- currentTestError: string[] = [];
23
-
24
- pretaskRegex = /^::__PRETASK:(.*)$/;
25
21
 
26
22
  private logger: TsTestLogger;
23
+ private protocolParser: ProtocolParser;
24
+ private protocolVersion: string | null = null;
27
25
 
28
26
  /**
29
27
  * the constructor for TapParser
30
28
  */
31
29
  constructor(public fileName: string, logger?: TsTestLogger) {
32
30
  this.logger = logger;
31
+ this.protocolParser = new ProtocolParser();
33
32
  }
34
33
 
35
34
  /**
@@ -75,137 +74,299 @@ export class TapParser {
75
74
  logLineArray.pop();
76
75
  }
77
76
 
78
- // lets parse the log information
77
+ // Process each line through the protocol parser
79
78
  for (const logLine of logLineArray) {
80
- let logLineIsTapProtocol = false;
81
- if (!this.expectedTests && this.expectedTestsRegex.test(logLine)) {
82
- logLineIsTapProtocol = true;
83
- const regexResult = this.expectedTestsRegex.exec(logLine);
84
- this.expectedTests = parseInt(regexResult[2]);
85
- if (this.logger) {
86
- this.logger.tapOutput(`Expecting ${this.expectedTests} tests!`);
87
- }
88
-
89
- // initiating first TapResult
90
- this._getNewTapTestResult();
91
- } else if (this.pretaskRegex.test(logLine)) {
92
- logLineIsTapProtocol = true;
93
- const pretaskContentMatch = this.pretaskRegex.exec(logLine);
94
- if (pretaskContentMatch && pretaskContentMatch[1]) {
95
- if (this.logger) {
96
- this.logger.tapOutput(`Pretask -> ${pretaskContentMatch[1]}: Success.`);
97
- }
98
- }
99
- } else if (this.testStatusRegex.test(logLine)) {
100
- logLineIsTapProtocol = true;
101
- const regexResult = this.testStatusRegex.exec(logLine);
102
- // const testId = parseInt(regexResult[2]); // Currently unused
103
- const testOk = (() => {
104
- if (regexResult[1] === 'ok') {
105
- return true;
106
- }
107
- return false;
108
- })();
109
-
110
- const testSubject = regexResult[3].trim();
111
- const testMetadata = regexResult[5]; // This will be either "time=XXXms" or "SKIP reason" or "TODO reason"
112
-
113
- let testDuration = 0;
114
-
115
- if (testMetadata) {
116
- const timeMatch = testMetadata.match(/time=(\d+)ms/);
117
- // const skipMatch = testMetadata.match(/SKIP\s*(.*)/); // Currently unused
118
- // const todoMatch = testMetadata.match(/TODO\s*(.*)/); // Currently unused
119
-
120
- if (timeMatch) {
121
- testDuration = parseInt(timeMatch[1]);
122
- }
123
- // Skip/todo handling could be added here in the future
124
- }
125
-
126
- // test for protocol error - disabled as it's not critical
127
- // The test ID mismatch can occur when tests are filtered, skipped, or use todo
128
- // if (testId !== this.activeTapTestResult.id) {
129
- // if (this.logger) {
130
- // this.logger.error('Something is strange! Test Ids are not equal!');
131
- // }
132
- // }
133
- this.activeTapTestResult.setTestResult(testOk);
134
-
135
- if (testOk) {
136
- if (this.logger) {
137
- this.logger.testResult(testSubject, true, testDuration);
138
- }
139
- } else {
140
- // Start collecting error details for failed test
141
- this.collectingErrorDetails = true;
142
- this.currentTestError = [];
143
- if (this.logger) {
144
- this.logger.testResult(testSubject, false, testDuration);
145
- }
79
+ const messages = this.protocolParser.parseLine(logLine);
80
+
81
+ if (messages.length > 0) {
82
+ // Handle protocol messages
83
+ for (const message of messages) {
84
+ this._handleProtocolMessage(message, logLine);
146
85
  }
147
- }
148
-
149
- if (!logLineIsTapProtocol) {
86
+ } else {
87
+ // Not a protocol message, handle as console output
150
88
  if (this.activeTapTestResult) {
151
89
  this.activeTapTestResult.addLogLine(logLine);
152
90
  }
153
91
 
154
- // Check for snapshot communication
92
+ // Check for snapshot communication (legacy)
155
93
  const snapshotMatch = logLine.match(/###SNAPSHOT###(.+)###SNAPSHOT###/);
156
94
  if (snapshotMatch) {
157
95
  const base64Data = snapshotMatch[1];
158
96
  try {
159
97
  const snapshotData = JSON.parse(Buffer.from(base64Data, 'base64').toString());
160
98
  this.handleSnapshot(snapshotData);
161
- } catch (error) {
99
+ } catch (error: any) {
162
100
  if (this.logger) {
163
101
  this.logger.testConsoleOutput(`Error parsing snapshot data: ${error.message}`);
164
102
  }
165
103
  }
104
+ } else if (this.logger) {
105
+ // This is console output from the test file
106
+ this.logger.testConsoleOutput(logLine);
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ private _handleProtocolMessage(message: IProtocolMessage, originalLine: string) {
113
+ switch (message.type) {
114
+ case 'protocol':
115
+ this.protocolVersion = message.content.version;
116
+ if (this.logger) {
117
+ this.logger.tapOutput(`Protocol version: ${this.protocolVersion}`);
118
+ }
119
+ break;
120
+
121
+ case 'version':
122
+ // TAP version, we can ignore this
123
+ break;
124
+
125
+ case 'plan':
126
+ const plan = message.content as IPlanLine;
127
+ this.expectedTests = plan.end - plan.start + 1;
128
+ if (plan.skipAll) {
129
+ if (this.logger) {
130
+ this.logger.tapOutput(`Skipping all tests: ${plan.skipAll}`);
131
+ }
166
132
  } else {
167
- // Check if we're collecting error details
168
- if (this.collectingErrorDetails) {
169
- // Check if this line is an error detail (starts with Error: or has stack trace characteristics)
170
- if (logLine.trim().startsWith('Error:') || logLine.trim().match(/^\s*at\s/)) {
171
- this.currentTestError.push(logLine);
172
- } else if (this.currentTestError.length > 0) {
173
- // End of error details, show the error
174
- const errorMessage = this.currentTestError.join('\n');
175
- if (this.logger) {
176
- this.logger.testErrorDetails(errorMessage);
177
- }
178
- this.collectingErrorDetails = false;
179
- this.currentTestError = [];
180
- }
133
+ if (this.logger) {
134
+ this.logger.tapOutput(`Expecting ${this.expectedTests} tests!`);
181
135
  }
182
-
183
- // Don't output TAP error details as console output when we're collecting them
184
- if (!this.collectingErrorDetails || (!logLine.trim().startsWith('Error:') && !logLine.trim().match(/^\s*at\s/))) {
185
- if (this.logger) {
186
- // This is console output from the test file, not TAP protocol
187
- this.logger.testConsoleOutput(logLine);
136
+ }
137
+ // Initialize first TapResult
138
+ this._getNewTapTestResult();
139
+ break;
140
+
141
+ case 'test':
142
+ const testResult = message.content as ITestResult;
143
+
144
+ // Update active test result
145
+ this.activeTapTestResult.setTestResult(testResult.ok);
146
+
147
+ // Extract test duration from metadata
148
+ let testDuration = 0;
149
+ if (testResult.metadata?.time) {
150
+ testDuration = testResult.metadata.time;
151
+ }
152
+
153
+ // Log test result
154
+ if (this.logger) {
155
+ if (testResult.ok) {
156
+ this.logger.testResult(testResult.description, true, testDuration);
157
+ } else {
158
+ this.logger.testResult(testResult.description, false, testDuration);
159
+
160
+ // If there's error metadata, show it
161
+ if (testResult.metadata?.error) {
162
+ const error = testResult.metadata.error;
163
+ let errorDetails = error.message;
164
+ if (error.stack) {
165
+ errorDetails = error.stack;
166
+ }
167
+ this.logger.testErrorDetails(errorDetails);
188
168
  }
189
169
  }
190
170
  }
191
- }
192
-
193
- if (this.activeTapTestResult && this.activeTapTestResult.testSettled) {
194
- // Ensure any pending error is shown before settling the test
195
- if (this.collectingErrorDetails && this.currentTestError.length > 0) {
196
- const errorMessage = this.currentTestError.join('\n');
171
+
172
+ // Handle directives (skip/todo)
173
+ if (testResult.directive) {
197
174
  if (this.logger) {
198
- this.logger.testErrorDetails(errorMessage);
175
+ if (testResult.directive.type === 'skip') {
176
+ this.logger.testConsoleOutput(`Test skipped: ${testResult.directive.reason || 'No reason given'}`);
177
+ } else if (testResult.directive.type === 'todo') {
178
+ this.logger.testConsoleOutput(`Test todo: ${testResult.directive.reason || 'No reason given'}`);
179
+ }
199
180
  }
200
- this.collectingErrorDetails = false;
201
- this.currentTestError = [];
202
181
  }
203
182
 
183
+ // Mark test as settled and move to next
184
+ this.activeTapTestResult.testSettled = true;
204
185
  this.testStore.push(this.activeTapTestResult);
205
186
  this._getNewTapTestResult();
187
+ break;
188
+
189
+ case 'comment':
190
+ if (this.logger) {
191
+ // Check if it's a pretask comment
192
+ const pretaskMatch = message.content.match(/^Pretask -> (.+): Success\.$/);
193
+ if (pretaskMatch) {
194
+ this.logger.tapOutput(message.content);
195
+ } else {
196
+ this.logger.testConsoleOutput(message.content);
197
+ }
198
+ }
199
+ break;
200
+
201
+ case 'bailout':
202
+ if (this.logger) {
203
+ this.logger.error(`Bail out! ${message.content}`);
204
+ }
205
+ break;
206
+
207
+ case 'error':
208
+ const errorBlock = message.content as IErrorBlock;
209
+ if (this.logger && errorBlock.error) {
210
+ let errorDetails = errorBlock.error.message;
211
+ if (errorBlock.error.stack) {
212
+ errorDetails = errorBlock.error.stack;
213
+ }
214
+ this.logger.testErrorDetails(errorDetails);
215
+ }
216
+ break;
217
+
218
+ case 'snapshot':
219
+ // Handle new protocol snapshot format
220
+ const snapshot = message.content;
221
+ this.handleSnapshot({
222
+ path: snapshot.name,
223
+ content: typeof snapshot.content === 'string' ? snapshot.content : JSON.stringify(snapshot.content),
224
+ action: 'compare' // Default action
225
+ });
226
+ break;
227
+
228
+ case 'event':
229
+ const event = message.content as ITestEvent;
230
+ this._handleTestEvent(event);
231
+ break;
232
+ }
233
+ }
234
+
235
+ private _handleTestEvent(event: ITestEvent) {
236
+ if (!this.logger) return;
237
+
238
+ switch (event.eventType) {
239
+ case 'test:queued':
240
+ // We can track queued tests if needed
241
+ break;
242
+
243
+ case 'test:started':
244
+ this.logger.testConsoleOutput(cs(`Test starting: ${event.data.description}`, 'cyan'));
245
+ if (event.data.retry) {
246
+ this.logger.testConsoleOutput(cs(` Retry attempt ${event.data.retry}`, 'orange'));
247
+ }
248
+ break;
249
+
250
+ case 'test:progress':
251
+ if (event.data.progress !== undefined) {
252
+ this.logger.testConsoleOutput(cs(` Progress: ${event.data.progress}%`, 'cyan'));
253
+ }
254
+ break;
255
+
256
+ case 'test:completed':
257
+ // Test completion is already handled by the test result
258
+ // This event provides additional timing info if needed
259
+ break;
260
+
261
+ case 'suite:started':
262
+ this.logger.testConsoleOutput(cs(`\nSuite: ${event.data.suiteName}`, 'blue'));
263
+ break;
264
+
265
+ case 'suite:completed':
266
+ this.logger.testConsoleOutput(cs(`Suite completed: ${event.data.suiteName}\n`, 'blue'));
267
+ break;
268
+
269
+ case 'hook:started':
270
+ this.logger.testConsoleOutput(cs(` Hook: ${event.data.hookName}`, 'cyan'));
271
+ break;
272
+
273
+ case 'hook:completed':
274
+ // Silent unless there's an error
275
+ if (event.data.error) {
276
+ this.logger.testConsoleOutput(cs(` Hook failed: ${event.data.hookName}`, 'red'));
277
+ }
278
+ break;
279
+
280
+ case 'assertion:failed':
281
+ // Enhanced assertion failure with diff
282
+ if (event.data.error) {
283
+ this._displayAssertionError(event.data.error);
284
+ }
285
+ break;
286
+ }
287
+ }
288
+
289
+ private _displayAssertionError(error: any) {
290
+ if (!this.logger) return;
291
+
292
+ // Display error message
293
+ if (error.message) {
294
+ this.logger.testErrorDetails(error.message);
295
+ }
296
+
297
+ // Display visual diff if available
298
+ if (error.diff) {
299
+ this._displayDiff(error.diff, error.expected, error.actual);
300
+ }
301
+ }
302
+
303
+ private _displayDiff(diff: any, expected: any, actual: any) {
304
+ if (!this.logger) return;
305
+
306
+ this.logger.testConsoleOutput(cs('\n Diff:', 'cyan'));
307
+
308
+ switch (diff.type) {
309
+ case 'string':
310
+ this._displayStringDiff(diff.changes);
311
+ break;
312
+
313
+ case 'object':
314
+ this._displayObjectDiff(diff.changes, expected, actual);
315
+ break;
316
+
317
+ case 'array':
318
+ this._displayArrayDiff(diff.changes, expected, actual);
319
+ break;
320
+
321
+ case 'primitive':
322
+ this._displayPrimitiveDiff(diff.changes);
323
+ break;
324
+ }
325
+ }
326
+
327
+ private _displayStringDiff(changes: any[]) {
328
+ for (const change of changes) {
329
+ const linePrefix = ` Line ${change.line + 1}: `;
330
+ if (change.type === 'add') {
331
+ this.logger.testConsoleOutput(cs(`${linePrefix}+ ${change.content}`, 'green'));
332
+ } else if (change.type === 'remove') {
333
+ this.logger.testConsoleOutput(cs(`${linePrefix}- ${change.content}`, 'red'));
334
+ }
335
+ }
336
+ }
337
+
338
+ private _displayObjectDiff(changes: any[], expected: any, actual: any) {
339
+ this.logger.testConsoleOutput(cs(' Expected:', 'red'));
340
+ this.logger.testConsoleOutput(` ${JSON.stringify(expected, null, 2)}`);
341
+ this.logger.testConsoleOutput(cs(' Actual:', 'green'));
342
+ this.logger.testConsoleOutput(` ${JSON.stringify(actual, null, 2)}`);
343
+
344
+ this.logger.testConsoleOutput(cs('\n Changes:', 'cyan'));
345
+ for (const change of changes) {
346
+ const path = change.path.join('.');
347
+ if (change.type === 'add') {
348
+ this.logger.testConsoleOutput(cs(` + ${path}: ${JSON.stringify(change.newValue)}`, 'green'));
349
+ } else if (change.type === 'remove') {
350
+ this.logger.testConsoleOutput(cs(` - ${path}: ${JSON.stringify(change.oldValue)}`, 'red'));
351
+ } else if (change.type === 'modify') {
352
+ this.logger.testConsoleOutput(cs(` ~ ${path}:`, 'cyan'));
353
+ this.logger.testConsoleOutput(cs(` - ${JSON.stringify(change.oldValue)}`, 'red'));
354
+ this.logger.testConsoleOutput(cs(` + ${JSON.stringify(change.newValue)}`, 'green'));
206
355
  }
207
356
  }
208
357
  }
358
+
359
+ private _displayArrayDiff(changes: any[], expected: any[], actual: any[]) {
360
+ this._displayObjectDiff(changes, expected, actual);
361
+ }
362
+
363
+ private _displayPrimitiveDiff(changes: any[]) {
364
+ const change = changes[0];
365
+ if (change) {
366
+ this.logger.testConsoleOutput(cs(` Expected: ${JSON.stringify(change.oldValue)}`, 'red'));
367
+ this.logger.testConsoleOutput(cs(` Actual: ${JSON.stringify(change.newValue)}`, 'green'));
368
+ }
369
+ }
209
370
 
210
371
  /**
211
372
  * returns all tests that are not completed
@@ -353,4 +514,4 @@ export class TapParser {
353
514
  }
354
515
  }
355
516
  }
356
- }
517
+ }
@@ -101,6 +101,77 @@ export class TsTest {
101
101
  tapCombinator.evaluate();
102
102
  }
103
103
 
104
+ public async runWatch(ignorePatterns: string[] = []) {
105
+ const smartchokInstance = new plugins.smartchok.Smartchok([this.testDir.cwd]);
106
+
107
+ console.clear();
108
+ this.logger.watchModeStart();
109
+
110
+ // Initial run
111
+ await this.run();
112
+
113
+ // Set up file watcher
114
+ const fileChanges = new Map<string, NodeJS.Timeout>();
115
+ const debounceTime = 300; // 300ms debounce
116
+
117
+ const runTestsAfterChange = async () => {
118
+ console.clear();
119
+ const changedFiles = Array.from(fileChanges.keys());
120
+ fileChanges.clear();
121
+
122
+ this.logger.watchModeRerun(changedFiles);
123
+ await this.run();
124
+ this.logger.watchModeWaiting();
125
+ };
126
+
127
+ // Start watching before subscribing to events
128
+ await smartchokInstance.start();
129
+
130
+ // Subscribe to file change events
131
+ const changeObservable = await smartchokInstance.getObservableFor('change');
132
+ const addObservable = await smartchokInstance.getObservableFor('add');
133
+ const unlinkObservable = await smartchokInstance.getObservableFor('unlink');
134
+
135
+ const handleFileChange = (changedPath: string) => {
136
+ // Skip if path matches ignore patterns
137
+ if (ignorePatterns.some(pattern => changedPath.includes(pattern))) {
138
+ return;
139
+ }
140
+
141
+ // Clear existing timeout for this file if any
142
+ if (fileChanges.has(changedPath)) {
143
+ clearTimeout(fileChanges.get(changedPath));
144
+ }
145
+
146
+ // Set new timeout for this file
147
+ const timeout = setTimeout(() => {
148
+ fileChanges.delete(changedPath);
149
+ if (fileChanges.size === 0) {
150
+ runTestsAfterChange();
151
+ }
152
+ }, debounceTime);
153
+
154
+ fileChanges.set(changedPath, timeout);
155
+ };
156
+
157
+ // Subscribe to all relevant events
158
+ changeObservable.subscribe(([path]) => handleFileChange(path));
159
+ addObservable.subscribe(([path]) => handleFileChange(path));
160
+ unlinkObservable.subscribe(([path]) => handleFileChange(path));
161
+
162
+ this.logger.watchModeWaiting();
163
+
164
+ // Handle Ctrl+C to exit gracefully
165
+ process.on('SIGINT', async () => {
166
+ this.logger.watchModeStop();
167
+ await smartchokInstance.stop();
168
+ process.exit(0);
169
+ });
170
+
171
+ // Keep the process running
172
+ await new Promise(() => {}); // This promise never resolves
173
+ }
174
+
104
175
  private async runSingleTestOrSkip(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
105
176
  // Check if this file should be skipped based on range
106
177
  if (this.startFromFile !== null && fileIndex < this.startFromFile) {
@@ -161,9 +232,45 @@ export class TsTest {
161
232
  process.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
162
233
  }
163
234
 
164
- const execResultStreaming = await this.smartshellInstance.execStreamingSilent(
165
- `tsrun ${fileNameArg}${tsrunOptions}`
166
- );
235
+ // Check for 00init.ts file in test directory
236
+ const testDir = plugins.path.dirname(fileNameArg);
237
+ const initFile = plugins.path.join(testDir, '00init.ts');
238
+ let runCommand = `tsrun ${fileNameArg}${tsrunOptions}`;
239
+
240
+ const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
241
+
242
+ // If 00init.ts exists, run it first
243
+ if (initFileExists) {
244
+ // Create a temporary loader file that imports both 00init.ts and the test file
245
+ const absoluteInitFile = plugins.path.resolve(initFile);
246
+ const absoluteTestFile = plugins.path.resolve(fileNameArg);
247
+ const loaderContent = `
248
+ import '${absoluteInitFile.replace(/\\/g, '/')}';
249
+ import '${absoluteTestFile.replace(/\\/g, '/')}';
250
+ `;
251
+ const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`);
252
+ await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
253
+ runCommand = `tsrun ${loaderPath}${tsrunOptions}`;
254
+ }
255
+
256
+ const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
257
+
258
+ // If we created a loader file, clean it up after test execution
259
+ if (initFileExists) {
260
+ const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`);
261
+ const cleanup = () => {
262
+ try {
263
+ if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
264
+ plugins.smartfile.fs.removeSync(loaderPath);
265
+ }
266
+ } catch (e) {
267
+ // Ignore cleanup errors
268
+ }
269
+ };
270
+
271
+ execResultStreaming.childProcess.on('exit', cleanup);
272
+ execResultStreaming.childProcess.on('error', cleanup);
273
+ }
167
274
 
168
275
  // Handle timeout if specified
169
276
  if (this.timeoutSeconds !== null) {
@@ -382,10 +489,10 @@ export class TsTest {
382
489
  try {
383
490
  // Delete 00err and 00diff directories if they exist
384
491
  if (await plugins.smartfile.fs.isDirectory(errDir)) {
385
- await plugins.smartfile.fs.remove(errDir);
492
+ plugins.smartfile.fs.removeSync(errDir);
386
493
  }
387
494
  if (await plugins.smartfile.fs.isDirectory(diffDir)) {
388
- await plugins.smartfile.fs.remove(diffDir);
495
+ plugins.smartfile.fs.removeSync(diffDir);
389
496
  }
390
497
 
391
498
  // Get all .log files in log directory (not in subdirectories)
@@ -520,4 +520,47 @@ export class TsTestLogger {
520
520
 
521
521
  return diff;
522
522
  }
523
+
524
+ // Watch mode methods
525
+ watchModeStart() {
526
+ if (this.options.json) {
527
+ this.logJson({ event: 'watchModeStart' });
528
+ return;
529
+ }
530
+
531
+ this.log(this.format('\nšŸ‘€ Watch Mode', 'cyan'));
532
+ this.log(this.format(' Running tests in watch mode...', 'dim'));
533
+ this.log(this.format(' Press Ctrl+C to exit\n', 'dim'));
534
+ }
535
+
536
+ watchModeWaiting() {
537
+ if (this.options.json) {
538
+ this.logJson({ event: 'watchModeWaiting' });
539
+ return;
540
+ }
541
+
542
+ this.log(this.format('\n Waiting for file changes...', 'dim'));
543
+ }
544
+
545
+ watchModeRerun(changedFiles: string[]) {
546
+ if (this.options.json) {
547
+ this.logJson({ event: 'watchModeRerun', changedFiles });
548
+ return;
549
+ }
550
+
551
+ this.log(this.format('\nšŸ”„ File changes detected:', 'cyan'));
552
+ changedFiles.forEach(file => {
553
+ this.log(this.format(` • ${file}`, 'yellow'));
554
+ });
555
+ this.log(this.format('\n Re-running tests...\n', 'dim'));
556
+ }
557
+
558
+ watchModeStop() {
559
+ if (this.options.json) {
560
+ this.logJson({ event: 'watchModeStop' });
561
+ return;
562
+ }
563
+
564
+ this.log(this.format('\n\nšŸ‘‹ Stopping watch mode...', 'cyan'));
565
+ }
523
566
  }
@@ -13,6 +13,7 @@ export {
13
13
  // @push.rocks scope
14
14
  import * as consolecolor from '@push.rocks/consolecolor';
15
15
  import * as smartbrowser from '@push.rocks/smartbrowser';
16
+ import * as smartchok from '@push.rocks/smartchok';
16
17
  import * as smartdelay from '@push.rocks/smartdelay';
17
18
  import * as smartfile from '@push.rocks/smartfile';
18
19
  import * as smartlog from '@push.rocks/smartlog';
@@ -23,6 +24,7 @@ import * as tapbundle from '../dist_ts_tapbundle/index.js';
23
24
  export {
24
25
  consolecolor,
25
26
  smartbrowser,
27
+ smartchok,
26
28
  smartdelay,
27
29
  smartfile,
28
30
  smartlog,