testeranto
Version:
the AI powered BDD test framework for typescript projects
206 lines (183 loc) • 6.23 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unused-vars */
// Do not add logging to this file as it is used by the pure runtime.
import { ITTestResourceConfiguration, ITestArtifactory } from ".";
import { Ibdd_in_any, Ibdd_out_any } from "../CoreTypes";
import { IGivens } from "./BaseGiven";
import { beforeAllProxy, afterAllProxy } from "./pmProxy";
import { IPM } from "./types";
/**
* Represents a collection of test suites keyed by their names.
* Suites are organized as named collections because:
* - Tests are typically grouped into logical suites (e.g., by feature, component)
* - Suites may have different configurations or setup requirements
* - Named suites allow for selective test execution and better reporting
* - This supports the hierarchical structure of test organization
*/
export type ISuites<I extends Ibdd_in_any, O extends Ibdd_out_any> = Record<
string,
BaseSuite<I, O>
>;
export abstract class BaseSuite<I extends Ibdd_in_any, O extends Ibdd_out_any> {
name: string;
givens: IGivens<I>;
store: I["istore"];
testResourceConfiguration: ITTestResourceConfiguration;
index: number;
failed: boolean;
fails: number;
artifacts: string[] = [];
addArtifact(path: string) {
if (typeof path !== "string") {
throw new Error(
`[ARTIFACT ERROR] Expected string, got ${typeof path}: ${JSON.stringify(
path
)}`
);
}
const normalizedPath = path.replace(/\\/g, "/"); // Normalize path separators
this.artifacts.push(normalizedPath);
}
constructor(name: string, index: number, givens: IGivens<I> = {}) {
const suiteName = name || "testSuite"; // Ensure name is never undefined
if (!suiteName) {
throw new Error("BaseSuite requires a non-empty name");
}
this.name = suiteName;
this.index = index;
this.givens = givens;
this.fails = 0;
}
public features() {
try {
const features = Object.keys(this.givens)
.map((k) => this.givens[k].features)
.flat()
.filter((value, index, array) => {
return array.indexOf(value) === index;
});
// Convert all features to strings
const stringFeatures = features.map((feature) => {
if (typeof feature === "string") {
return feature;
} else if (feature && typeof feature === "object") {
return feature.name || JSON.stringify(feature);
} else {
return String(feature);
}
});
return stringFeatures || [];
} catch (e) {
console.error("[ERROR] Failed to extract features:", JSON.stringify(e));
return [];
}
}
public toObj() {
const givens = Object.keys(this.givens).map((k) => {
const givenObj = this.givens[k].toObj();
// Ensure given features are strings
// if (givenObj.features) {
// givenObj.features = givenObj.features.map(feature => {
// return feature;
// // if (typeof feature === 'string') {
// // return feature;
// // } else if (feature && typeof feature === 'object') {
// // return feature.name || JSON.stringify(feature);
// // } else {
// // return String(feature);
// // }
// });
// }
return givenObj;
});
return {
name: this.name,
givens,
fails: this.fails,
failed: this.failed,
features: this.features(),
artifacts: this.artifacts
? this.artifacts.filter((art) => typeof art === "string")
: [],
};
}
setup(
s: I["iinput"],
artifactory: ITestArtifactory,
tr: ITTestResourceConfiguration,
pm: IPM
): Promise<I["isubject"]> {
return new Promise((res) => res(s as unknown as I["isubject"]));
}
assertThat(t: Awaited<I["then"]> | undefined): boolean {
return !!t;
}
afterAll(store: I["istore"], artifactory: ITestArtifactory, pm: IPM) {
return store;
}
async run(
input: I["iinput"],
testResourceConfiguration: ITTestResourceConfiguration,
artifactory: (fPath: string, value: unknown) => void,
tLog: (...string) => void,
pm: IPM
): Promise<BaseSuite<I, O>> {
this.testResourceConfiguration = testResourceConfiguration;
// tLog("test resources: ", JSON.stringify(testResourceConfiguration));
const suiteArtifactory = (fPath: string, value: unknown) =>
artifactory(`suite-${this.index}-${this.name}/${fPath}`, value);
// console.log("\nSuite:", this.index, this.name);
// tLog("\nSuite:", this.index, this.name);
const sNdx = this.index;
// Ensure addArtifact is properly bound to 'this'
const addArtifact = this.addArtifact.bind(this);
const proxiedPm = beforeAllProxy(pm, sNdx.toString(), addArtifact);
const subject = await this.setup(
input,
suiteArtifactory,
testResourceConfiguration,
proxiedPm
);
for (const [gKey, g] of Object.entries(this.givens)) {
const giver = this.givens[gKey];
try {
this.store = await giver.give(
subject,
gKey,
testResourceConfiguration,
this.assertThat,
suiteArtifactory,
tLog,
pm,
sNdx
);
// Add the number of failures from this given to the suite's total
this.fails += giver.fails || 0;
} catch (e) {
this.failed = true;
// Add 1 to fails for the caught error
this.fails += 1;
// Also add any failures from the given itself
if (giver.fails) {
this.fails += giver.fails;
}
console.error(`Error in given ${gKey}:`, e);
// Don't re-throw to continue with other givens
}
}
// Mark the suite as failed if there are any failures
if (this.fails > 0) {
this.failed = true;
}
try {
// Ensure addArtifact is properly bound to 'this'
const addArtifact = this.addArtifact.bind(this);
const afterAllPm = afterAllProxy(pm, sNdx.toString(), addArtifact);
this.afterAll(this.store, artifactory, afterAllPm);
} catch (e) {
console.error(JSON.stringify(e));
// this.fails.push(this);
// return this;
}
return this;
}
}