feat(core): Implement Protocol V2 with enhanced settings and lifecycle hooks
This commit is contained in:
@@ -2,6 +2,9 @@ import * as plugins from './tapbundle.plugins.js';
|
||||
|
||||
import { type IPreTaskFunction, PreTask } from './tapbundle.classes.pretask.js';
|
||||
import { TapTest, type ITestFunction } from './tapbundle.classes.taptest.js';
|
||||
import { ProtocolEmitter, type ITestEvent } from '../dist_ts_tapbundle_protocol/index.js';
|
||||
import type { ITapSettings } from './tapbundle.interfaces.js';
|
||||
import { SettingsManager } from './tapbundle.classes.settingsmanager.js';
|
||||
|
||||
export interface ITestSuite {
|
||||
description: string;
|
||||
@@ -102,6 +105,8 @@ class TestBuilder<T> {
|
||||
}
|
||||
|
||||
export class Tap<T> {
|
||||
private protocolEmitter = new ProtocolEmitter();
|
||||
private settingsManager = new SettingsManager();
|
||||
private _skipCount = 0;
|
||||
private _filterTags: string[] = [];
|
||||
|
||||
@@ -139,12 +144,27 @@ export class Tap<T> {
|
||||
*/
|
||||
public skip = {
|
||||
test: (descriptionArg: string, functionArg: ITestFunction<T>) => {
|
||||
console.log(`skipped test: ${descriptionArg}`);
|
||||
this._skipCount++;
|
||||
const skippedTest = this.test(descriptionArg, functionArg, 'skip');
|
||||
return skippedTest;
|
||||
},
|
||||
testParallel: (descriptionArg: string, functionArg: ITestFunction<T>) => {
|
||||
console.log(`skipped test: ${descriptionArg}`);
|
||||
this._skipCount++;
|
||||
const skippedTest = new TapTest<T>({
|
||||
description: descriptionArg,
|
||||
testFunction: functionArg,
|
||||
parallel: true,
|
||||
});
|
||||
|
||||
// Mark as skip mode
|
||||
skippedTest.tapTools.markAsSkipped('Marked as skip');
|
||||
|
||||
// Add to appropriate test list
|
||||
if (this._currentSuite) {
|
||||
this._currentSuite.tests.push(skippedTest);
|
||||
} else {
|
||||
this._tapTests.push(skippedTest);
|
||||
}
|
||||
|
||||
return skippedTest;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -153,7 +173,65 @@ export class Tap<T> {
|
||||
*/
|
||||
public only = {
|
||||
test: (descriptionArg: string, testFunctionArg: ITestFunction<T>) => {
|
||||
this.test(descriptionArg, testFunctionArg, 'only');
|
||||
return this.test(descriptionArg, testFunctionArg, 'only');
|
||||
},
|
||||
testParallel: (descriptionArg: string, testFunctionArg: ITestFunction<T>) => {
|
||||
const onlyTest = new TapTest<T>({
|
||||
description: descriptionArg,
|
||||
testFunction: testFunctionArg,
|
||||
parallel: true,
|
||||
});
|
||||
|
||||
// Add to only tests list
|
||||
this._tapTestsOnly.push(onlyTest);
|
||||
|
||||
return onlyTest;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* mark a test as todo (not yet implemented)
|
||||
*/
|
||||
public todo = {
|
||||
test: (descriptionArg: string, functionArg?: ITestFunction<T>) => {
|
||||
const defaultFunc = (async () => {}) as ITestFunction<T>;
|
||||
const todoTest = new TapTest<T>({
|
||||
description: descriptionArg,
|
||||
testFunction: functionArg || defaultFunc,
|
||||
parallel: false,
|
||||
});
|
||||
|
||||
// Mark as todo
|
||||
todoTest.tapTools.todo('Marked as todo');
|
||||
|
||||
// Add to appropriate test list
|
||||
if (this._currentSuite) {
|
||||
this._currentSuite.tests.push(todoTest);
|
||||
} else {
|
||||
this._tapTests.push(todoTest);
|
||||
}
|
||||
|
||||
return todoTest;
|
||||
},
|
||||
testParallel: (descriptionArg: string, functionArg?: ITestFunction<T>) => {
|
||||
const defaultFunc = (async () => {}) as ITestFunction<T>;
|
||||
const todoTest = new TapTest<T>({
|
||||
description: descriptionArg,
|
||||
testFunction: functionArg || defaultFunc,
|
||||
parallel: true,
|
||||
});
|
||||
|
||||
// Mark as todo
|
||||
todoTest.tapTools.todo('Marked as todo');
|
||||
|
||||
// Add to appropriate test list
|
||||
if (this._currentSuite) {
|
||||
this._currentSuite.tests.push(todoTest);
|
||||
} else {
|
||||
this._tapTests.push(todoTest);
|
||||
}
|
||||
|
||||
return todoTest;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -163,6 +241,21 @@ export class Tap<T> {
|
||||
private _currentSuite: ITestSuite | null = null;
|
||||
private _rootSuites: ITestSuite[] = [];
|
||||
|
||||
/**
|
||||
* Configure global test settings
|
||||
*/
|
||||
public settings(settings: ITapSettings): this {
|
||||
this.settingsManager.setGlobalSettings(settings);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current test settings
|
||||
*/
|
||||
public getSettings(): ITapSettings {
|
||||
return this.settingsManager.getSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normal test function, will run one by one
|
||||
* @param testDescription - A description of what the test does
|
||||
@@ -179,14 +272,26 @@ export class Tap<T> {
|
||||
parallel: false,
|
||||
});
|
||||
|
||||
// No options applied here - use the fluent builder syntax instead
|
||||
// Apply default settings from settings manager
|
||||
const settings = this.settingsManager.getSettings();
|
||||
if (settings.timeout !== undefined) {
|
||||
localTest.timeoutMs = settings.timeout;
|
||||
}
|
||||
if (settings.retries !== undefined) {
|
||||
localTest.tapTools.retry(settings.retries);
|
||||
}
|
||||
|
||||
// Handle skip mode
|
||||
if (modeArg === 'skip') {
|
||||
localTest.tapTools.markAsSkipped('Marked as skip');
|
||||
}
|
||||
|
||||
// If we're in a suite, add test to the suite
|
||||
if (this._currentSuite) {
|
||||
this._currentSuite.tests.push(localTest);
|
||||
} else {
|
||||
// Otherwise add to global test list
|
||||
if (modeArg === 'normal') {
|
||||
if (modeArg === 'normal' || modeArg === 'skip') {
|
||||
this._tapTests.push(localTest);
|
||||
} else if (modeArg === 'only') {
|
||||
this._tapTestsOnly.push(localTest);
|
||||
@@ -211,6 +316,15 @@ export class Tap<T> {
|
||||
parallel: true,
|
||||
});
|
||||
|
||||
// Apply default settings from settings manager
|
||||
const settings = this.settingsManager.getSettings();
|
||||
if (settings.timeout !== undefined) {
|
||||
localTest.timeoutMs = settings.timeout;
|
||||
}
|
||||
if (settings.retries !== undefined) {
|
||||
localTest.tapTools.retry(settings.retries);
|
||||
}
|
||||
|
||||
if (this._currentSuite) {
|
||||
this._currentSuite.tests.push(localTest);
|
||||
} else {
|
||||
@@ -336,8 +450,27 @@ export class Tap<T> {
|
||||
await preTask.run();
|
||||
}
|
||||
|
||||
// Count actual tests that will be run
|
||||
console.log(`1..${concerningTests.length}`);
|
||||
// Emit protocol header and TAP version
|
||||
console.log(this.protocolEmitter.emitProtocolHeader());
|
||||
console.log(this.protocolEmitter.emitTapVersion(13));
|
||||
|
||||
// Emit test plan
|
||||
const plan = {
|
||||
start: 1,
|
||||
end: concerningTests.length
|
||||
};
|
||||
console.log(this.protocolEmitter.emitPlan(plan));
|
||||
|
||||
// Run global beforeAll hook if configured
|
||||
const settings = this.settingsManager.getSettings();
|
||||
if (settings.beforeAll) {
|
||||
try {
|
||||
await settings.beforeAll();
|
||||
} catch (error) {
|
||||
console.error('Error in beforeAll hook:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests from suites with lifecycle hooks
|
||||
let testKey = 0;
|
||||
@@ -365,6 +498,33 @@ export class Tap<T> {
|
||||
});
|
||||
|
||||
for (const currentTest of nonSuiteTests) {
|
||||
// Wrap test function with global lifecycle hooks
|
||||
const originalFunction = currentTest.testFunction;
|
||||
const testName = currentTest.description;
|
||||
currentTest.testFunction = async (tapTools) => {
|
||||
// Run global beforeEach if configured
|
||||
if (settings.beforeEach) {
|
||||
await settings.beforeEach(testName);
|
||||
}
|
||||
|
||||
// Run the actual test
|
||||
let testPassed = true;
|
||||
let result: any;
|
||||
try {
|
||||
result = await originalFunction(tapTools);
|
||||
} catch (error) {
|
||||
testPassed = false;
|
||||
throw error;
|
||||
} finally {
|
||||
// Run global afterEach if configured
|
||||
if (settings.afterEach) {
|
||||
await settings.afterEach(testName, testPassed);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const testPromise = currentTest.run(testKey++);
|
||||
if (currentTest.parallel) {
|
||||
promiseArray.push(testPromise);
|
||||
@@ -394,6 +554,16 @@ export class Tap<T> {
|
||||
console.log(failReason);
|
||||
}
|
||||
|
||||
// Run global afterAll hook if configured
|
||||
if (settings.afterAll) {
|
||||
try {
|
||||
await settings.afterAll();
|
||||
} catch (error) {
|
||||
console.error('Error in afterAll hook:', error);
|
||||
// Don't throw here, we want to complete the test run
|
||||
}
|
||||
}
|
||||
|
||||
if (optionsArg && optionsArg.throwOnError && failReasons.length > 0) {
|
||||
if (!smartenvInstance.isBrowser && typeof process !== 'undefined') process.exit(1);
|
||||
}
|
||||
@@ -402,6 +572,13 @@ export class Tap<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event
|
||||
*/
|
||||
private emitEvent(event: ITestEvent) {
|
||||
console.log(this.protocolEmitter.emitEvent(event));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run tests in a suite with lifecycle hooks
|
||||
*/
|
||||
@@ -412,6 +589,14 @@ export class Tap<T> {
|
||||
context: { testKey: number }
|
||||
) {
|
||||
for (const suite of suites) {
|
||||
// Emit suite:started event
|
||||
this.emitEvent({
|
||||
eventType: 'suite:started',
|
||||
timestamp: Date.now(),
|
||||
data: {
|
||||
suiteName: suite.description
|
||||
}
|
||||
});
|
||||
// Run beforeEach from parent suites
|
||||
const beforeEachFunctions: ITestFunction<any>[] = [];
|
||||
let currentSuite: ITestSuite | null = suite;
|
||||
@@ -426,27 +611,46 @@ export class Tap<T> {
|
||||
for (const test of suite.tests) {
|
||||
// Create wrapper test function that includes lifecycle hooks
|
||||
const originalFunction = test.testFunction;
|
||||
const testName = test.description;
|
||||
test.testFunction = async (tapTools) => {
|
||||
// Run all beforeEach hooks
|
||||
// Run global beforeEach if configured
|
||||
const settings = this.settingsManager.getSettings();
|
||||
if (settings.beforeEach) {
|
||||
await settings.beforeEach(testName);
|
||||
}
|
||||
|
||||
// Run all suite beforeEach hooks
|
||||
for (const beforeEach of beforeEachFunctions) {
|
||||
await beforeEach(tapTools);
|
||||
}
|
||||
|
||||
// Run the actual test
|
||||
const result = await originalFunction(tapTools);
|
||||
|
||||
// Run afterEach hooks in reverse order
|
||||
const afterEachFunctions: ITestFunction<any>[] = [];
|
||||
currentSuite = suite;
|
||||
while (currentSuite) {
|
||||
if (currentSuite.afterEach) {
|
||||
afterEachFunctions.push(currentSuite.afterEach);
|
||||
let testPassed = true;
|
||||
let result: any;
|
||||
try {
|
||||
result = await originalFunction(tapTools);
|
||||
} catch (error) {
|
||||
testPassed = false;
|
||||
throw error;
|
||||
} finally {
|
||||
// Run afterEach hooks in reverse order
|
||||
const afterEachFunctions: ITestFunction<any>[] = [];
|
||||
currentSuite = suite;
|
||||
while (currentSuite) {
|
||||
if (currentSuite.afterEach) {
|
||||
afterEachFunctions.push(currentSuite.afterEach);
|
||||
}
|
||||
currentSuite = currentSuite.parent || null;
|
||||
}
|
||||
|
||||
for (const afterEach of afterEachFunctions) {
|
||||
await afterEach(tapTools);
|
||||
}
|
||||
|
||||
// Run global afterEach if configured
|
||||
if (settings.afterEach) {
|
||||
await settings.afterEach(testName, testPassed);
|
||||
}
|
||||
currentSuite = currentSuite.parent || null;
|
||||
}
|
||||
|
||||
for (const afterEach of afterEachFunctions) {
|
||||
await afterEach(tapTools);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -462,6 +666,15 @@ export class Tap<T> {
|
||||
|
||||
// Recursively run child suites
|
||||
await this._runSuite(suite, suite.children, promiseArray, context);
|
||||
|
||||
// Emit suite:completed event
|
||||
this.emitEvent({
|
||||
eventType: 'suite:completed',
|
||||
timestamp: Date.now(),
|
||||
data: {
|
||||
suiteName: suite.description
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user