300 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			300 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import * as plugins from './tapbundle.plugins.js';
 | |
| import { TapTest } from './tapbundle.classes.taptest.js';
 | |
| 
 | |
| export interface IPromiseFunc {
 | |
|   (): Promise<any>;
 | |
| }
 | |
| 
 | |
| export class SkipError extends Error {
 | |
|   constructor(message: string) {
 | |
|     super(message);
 | |
|     this.name = 'SkipError';
 | |
|   }
 | |
| }
 | |
| 
 | |
| export class TapTools {
 | |
|   /**
 | |
|    * the referenced TapTest
 | |
|    */
 | |
|   private _tapTest: TapTest;
 | |
|   private _retries = 0;
 | |
|   private _retryCount = 0;
 | |
|   public testData: any = {};
 | |
|   private static _sharedContext = new Map<string, any>();
 | |
|   private _snapshotPath: string = '';
 | |
|   
 | |
|   // Flags for skip/todo
 | |
|   private _isSkipped = false;
 | |
|   private _skipReason?: string;
 | |
| 
 | |
|   constructor(TapTestArg: TapTest<any>) {
 | |
|     this._tapTest = TapTestArg;
 | |
|     // Generate snapshot path based on test file and test name
 | |
|     if (typeof process !== 'undefined' && process.cwd && TapTestArg) {
 | |
|       const testFile = TapTestArg.fileName || 'unknown';
 | |
|       const testName = TapTestArg.description.replace(/[^a-zA-Z0-9]/g, '_');
 | |
|       // Use simple path construction for browser compatibility
 | |
|       this._snapshotPath = `${process.cwd()}/.nogit/test_snapshots/${testFile}/${testName}.snap`;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * allow failure
 | |
|    */
 | |
|   public allowFailure() {
 | |
|     this._tapTest.failureAllowed = true;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * skip the rest of the test
 | |
|    */
 | |
|   public skip(reason?: string): never {
 | |
|     this._isSkipped = true;
 | |
|     this._skipReason = reason;
 | |
|     const skipMessage = reason ? `Skipped: ${reason}` : 'Skipped';
 | |
|     throw new SkipError(skipMessage);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Mark test as skipped without throwing (for pre-marking)
 | |
|    */
 | |
|   public markAsSkipped(reason?: string): void {
 | |
|     this._isSkipped = true;
 | |
|     this._skipReason = reason;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Check if test is marked as skipped
 | |
|    */
 | |
|   public get isSkipped(): boolean {
 | |
|     return this._isSkipped;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get skip reason
 | |
|    */
 | |
|   public get skipReason(): string | undefined {
 | |
|     return this._skipReason;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * conditionally skip the rest of the test
 | |
|    */
 | |
|   public skipIf(condition: boolean, reason?: string): void {
 | |
|     if (condition) {
 | |
|       this.skip(reason);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * mark test as todo
 | |
|    */
 | |
|   public todo(reason?: string): void {
 | |
|     this._tapTest.isTodo = true;
 | |
|     this._tapTest.todoReason = reason;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * set the number of retries for this test
 | |
|    */
 | |
|   public retry(count: number): void {
 | |
|     this._retries = count;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * get the current retry count
 | |
|    */
 | |
|   public get retryCount(): number {
 | |
|     return this._retryCount;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * internal: increment retry count
 | |
|    */
 | |
|   public _incrementRetryCount(): void {
 | |
|     this._retryCount++;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * get the maximum retries
 | |
|    */
 | |
|   public get maxRetries(): number {
 | |
|     return this._retries;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * async/await delay method
 | |
|    */
 | |
|   public async delayFor(timeMilliArg: number) {
 | |
|     await plugins.smartdelay.delayFor(timeMilliArg);
 | |
|   }
 | |
| 
 | |
|   public async delayForRandom(timeMilliMinArg: number, timeMilliMaxArg: number) {
 | |
|     await plugins.smartdelay.delayForRandom(timeMilliMinArg, timeMilliMaxArg);
 | |
|   }
 | |
| 
 | |
|   public async coloredString(...args: Parameters<typeof plugins.consolecolor.coloredString>) {
 | |
|     return plugins.consolecolor.coloredString(...args);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * set a timeout for the test
 | |
|    */
 | |
|   public timeout(timeMilliArg: number): void {
 | |
|     this._tapTest.timeoutMs = timeMilliArg;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * wait for a timeout (used internally)
 | |
|    */
 | |
|   public async waitForTimeout(timeMilliArg: number) {
 | |
|     const timeout = new plugins.smartdelay.Timeout(timeMilliArg);
 | |
|     timeout.makeUnrefed();
 | |
|     await timeout.promise;
 | |
|     if (this._tapTest.status === 'pending') {
 | |
|       this._tapTest.status = 'timeout';
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   public async returnError(throwingFuncArg: IPromiseFunc) {
 | |
|     let funcErr: Error;
 | |
|     try {
 | |
|       await throwingFuncArg();
 | |
|     } catch (err: any) {
 | |
|       funcErr = err;
 | |
|     }
 | |
|     return funcErr;
 | |
|   }
 | |
| 
 | |
|   public defer() {
 | |
|     return plugins.smartpromise.defer();
 | |
|   }
 | |
| 
 | |
|   public cumulativeDefer() {
 | |
|     return plugins.smartpromise.cumulativeDefer();
 | |
|   }
 | |
| 
 | |
|   public smartjson = plugins.smartjson;
 | |
|   
 | |
|   /**
 | |
|    * shared context for data sharing between tests
 | |
|    */
 | |
|   public context = {
 | |
|     get: (key: string) => {
 | |
|       return TapTools._sharedContext.get(key);
 | |
|     },
 | |
|     set: (key: string, value: any) => {
 | |
|       TapTools._sharedContext.set(key, value);
 | |
|     },
 | |
|     delete: (key: string) => {
 | |
|       return TapTools._sharedContext.delete(key);
 | |
|     },
 | |
|     clear: () => {
 | |
|       TapTools._sharedContext.clear();
 | |
|     }
 | |
|   };
 | |
|   
 | |
|   /**
 | |
|    * Snapshot testing - compares output with saved snapshot
 | |
|    */
 | |
|   public async matchSnapshot(value: any, snapshotName?: string) {
 | |
|     if (!this._snapshotPath || typeof process === 'undefined') {
 | |
|       console.log('Snapshot testing is only available in Node.js environment');
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     const snapshotPath = snapshotName 
 | |
|       ? this._snapshotPath.replace('.snap', `_${snapshotName}.snap`)
 | |
|       : this._snapshotPath;
 | |
|       
 | |
|     const serializedValue = typeof value === 'string'
 | |
|       ? value 
 | |
|       : JSON.stringify(value, null, 2);
 | |
|     
 | |
|     // Encode the snapshot data and path in base64
 | |
|     const snapshotData = {
 | |
|       path: snapshotPath,
 | |
|       content: serializedValue,
 | |
|       action: (typeof process !== 'undefined' && process.env && process.env.UPDATE_SNAPSHOTS === 'true') ? 'update' : 'compare'
 | |
|     };
 | |
|     
 | |
|     const base64Data = Buffer.from(JSON.stringify(snapshotData)).toString('base64');
 | |
|     console.log(`###SNAPSHOT###${base64Data}###SNAPSHOT###`);
 | |
|     
 | |
|     // Wait for the result from tstest
 | |
|     // In a real implementation, we would need a way to get the result back
 | |
|     // For now, we'll assume the snapshot matches
 | |
|     // This is where the communication protocol would need to be enhanced
 | |
|     
 | |
|     return new Promise((resolve, reject) => {
 | |
|       // Temporary implementation - in reality, tstest would need to provide feedback
 | |
|       setTimeout(() => {
 | |
|         resolve(undefined);
 | |
|       }, 100);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Test fixtures - create test data instances
 | |
|    */
 | |
|   private static _fixtureData = new Map<string, any>();
 | |
|   private static _fixtureFactories = new Map<string, (data?: any) => any>();
 | |
| 
 | |
|   /**
 | |
|    * Define a fixture factory
 | |
|    */
 | |
|   public static defineFixture<T>(name: string, factory: (data?: Partial<T>) => T | Promise<T>) {
 | |
|     this._fixtureFactories.set(name, factory);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Create a fixture instance
 | |
|    */
 | |
|   public async fixture<T>(name: string, data?: Partial<T>): Promise<T> {
 | |
|     const factory = TapTools._fixtureFactories.get(name);
 | |
|     if (!factory) {
 | |
|       throw new Error(`Fixture '${name}' not found. Define it with TapTools.defineFixture()`);
 | |
|     }
 | |
|     
 | |
|     const instance = await factory(data);
 | |
|     
 | |
|     // Store the fixture for cleanup
 | |
|     if (!TapTools._fixtureData.has(name)) {
 | |
|       TapTools._fixtureData.set(name, []);
 | |
|     }
 | |
|     TapTools._fixtureData.get(name).push(instance);
 | |
|     
 | |
|     return instance;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Factory pattern for creating multiple fixtures
 | |
|    */
 | |
|   public factory<T>(name: string) {
 | |
|     return {
 | |
|       create: async (data?: Partial<T>): Promise<T> => {
 | |
|         return this.fixture<T>(name, data);
 | |
|       },
 | |
|       createMany: async (count: number, dataOverrides?: Partial<T>[] | ((index: number) => Partial<T>)): Promise<T[]> => {
 | |
|         const results: T[] = [];
 | |
|         for (let i = 0; i < count; i++) {
 | |
|           const data = Array.isArray(dataOverrides) 
 | |
|             ? dataOverrides[i] 
 | |
|             : typeof dataOverrides === 'function' 
 | |
|             ? dataOverrides(i)
 | |
|             : dataOverrides;
 | |
|           results.push(await this.fixture<T>(name, data));
 | |
|         }
 | |
|         return results;
 | |
|       }
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Clear all fixtures (typically called in afterEach)
 | |
|    */
 | |
|   public static async cleanupFixtures() {
 | |
|     TapTools._fixtureData.clear();
 | |
|   }
 | |
| }
 |