import * as plugins from './tapbundle.plugins.js'; import { TapTest } from './tapbundle.classes.taptest.js'; export interface IPromiseFunc { (): Promise; } 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(); private _snapshotPath: string = ''; // Flags for skip/todo private _isSkipped = false; private _skipReason?: string; constructor(TapTestArg: TapTest) { 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) { 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(); private static _fixtureFactories = new Map any>(); /** * Define a fixture factory */ public static defineFixture(name: string, factory: (data?: Partial) => T | Promise) { this._fixtureFactories.set(name, factory); } /** * Create a fixture instance */ public async fixture(name: string, data?: Partial): Promise { 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(name: string) { return { create: async (data?: Partial): Promise => { return this.fixture(name, data); }, createMany: async (count: number, dataOverrides?: Partial[] | ((index: number) => Partial)): Promise => { 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(name, data)); } return results; } }; } /** * Clear all fixtures (typically called in afterEach) */ public static async cleanupFixtures() { TapTools._fixtureData.clear(); } }