feat(core): Implement Protocol V2 with enhanced settings and lifecycle hooks

This commit is contained in:
2025-05-26 04:02:32 +00:00
parent 91880f8d42
commit 33d2ff1d4f
24 changed files with 2356 additions and 441 deletions

View File

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