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();
|
|
}
|
|
}
|