jest-metadata
Version:
🦸♂️ Superhero power for your Jest reporters! 🦸♀️
238 lines (199 loc) • 6.53 kB
text/typescript
import type { TestCaseResult } from '@jest/reporters';
import { JestMetadataError } from '../errors';
import type {
GlobalMetadata,
MetadataEventEmitter,
TestFileMetadata,
TestDoneEvent,
TestEntryMetadata,
TestSkipEvent,
} from '../metadata';
import { Rotator } from '../utils';
export type TestCaseResultArg = Pick<
TestCaseResult,
'status' | 'title' | 'ancestorTitles' | 'invocations'
>;
export type TestFileResultArg = {
testFilePath: string;
testResults: TestCaseResultArg[];
};
export type AggregatedResultArg = {
testResults: TestFileResultArg[];
};
export class FallbackAPI {
private _fallbackModes = new Map<string, boolean>();
private _cache = new Map<string, Rotator<TestEntryInfo>>();
constructor(
private readonly globalMetadata: GlobalMetadata,
private readonly eventEmitter: MetadataEventEmitter,
) {}
public get enabled() {
return this._fallbackModes.size > 0 ? this._fallbackModes.values().next().value : true;
}
reportTestFile(testFilePath: string) {
this.eventEmitter.emit({
type: 'add_test_file',
testFilePath,
});
return this.globalMetadata.getTestFileMetadata(testFilePath);
}
reportTestCase(testFilePath: string, testCaseResult: TestCaseResultArg): TestEntryMetadata {
const file = this.globalMetadata.getTestFileMetadata(testFilePath);
const fallbackMode = this._determineFallbackModeStatus(testFilePath, file);
if (!fallbackMode) {
return file.lastTestEntry!;
}
if (!file.rootDescribeBlock) {
this.eventEmitter.emit({
type: 'start_describe_definition',
testFilePath,
describeId: 'describe_0',
});
}
const rootDescribeBlock = file.rootDescribeBlock!;
const invocations = testCaseResult.invocations ?? 0;
const nameIdentifier = [
testFilePath,
...testCaseResult.ancestorTitles,
testCaseResult.title,
].join('\u001F');
if (invocations <= 1) {
const testId = `test_${rootDescribeBlock.children.length}`;
this.eventEmitter.emit({
type: 'add_test',
testFilePath,
testId,
});
const lastChild = file.lastTestEntry!;
let rotator: Rotator<TestEntryInfo>;
if (this._cache.has(nameIdentifier)) {
rotator = this._cache.get(nameIdentifier)!;
} else {
rotator = new Rotator<TestEntryInfo>();
this._cache.set(nameIdentifier, rotator);
}
rotator.add({
testId,
testFilePath,
testEntryMetadata: lastChild,
testCaseResult: { ...testCaseResult },
});
this.eventEmitter.emit({
type: 'test_start',
testFilePath,
testId,
});
this.eventEmitter.emit({
type: this._getCompletionEventType(testCaseResult),
testFilePath,
testId,
} as TestDoneEvent | TestSkipEvent | TestDoneEvent);
return lastChild;
} else {
const tests = this._cache.get(nameIdentifier)!;
const info = tests.find((t) => t.testCaseResult.status === 'failed')!;
info.testCaseResult = { ...testCaseResult };
this.eventEmitter.emit({
type: 'test_retry',
testFilePath: info.testFilePath,
testId: info.testId,
});
this.eventEmitter.emit({
type: 'test_start',
testFilePath: info.testFilePath,
testId: info.testId,
});
this.eventEmitter.emit({
type: this._getCompletionEventType(testCaseResult),
testFilePath: info.testFilePath,
testId: info.testId,
} as TestDoneEvent | TestSkipEvent | TestDoneEvent);
return info.testEntryMetadata;
}
}
reportTestFileResult(testFileResult: TestFileResultArg): TestEntryMetadata[] {
const { testFilePath, testResults } = testFileResult;
const file = this.globalMetadata.getTestFileMetadata(testFilePath);
const fallbackMode = this._determineFallbackModeStatus(testFilePath, file);
if (!file.rootDescribeBlock) {
this.eventEmitter.emit({
type: 'start_describe_definition',
testFilePath,
describeId: 'describe_0',
});
}
const rootDescribeBlock = file.rootDescribeBlock!;
if (!fallbackMode) {
return [...rootDescribeBlock.allTestEntries()];
}
for (const rotator of this._cache.values()) {
rotator.reset();
}
const result: TestEntryMetadata[] = [];
for (const testCaseResult of testResults) {
const nameId = this._getNameIdentifier(testFilePath, testCaseResult);
const tests = this._cache.get(nameId);
const info = tests?.peek();
if (info && info.testCaseResult.status === testCaseResult.status) {
result.push(info.testEntryMetadata);
tests!.next();
} else {
const testId = `test_${rootDescribeBlock.children.length}`;
this.eventEmitter.emit({
type: 'add_test',
testFilePath,
testId,
});
this.eventEmitter.emit({
type: 'test_start',
testFilePath,
testId,
});
this.eventEmitter.emit({
type: this._getCompletionEventType(testCaseResult),
testFilePath,
testId,
} as TestDoneEvent | TestSkipEvent | TestDoneEvent);
result.push(file.lastTestEntry!);
}
}
return result;
}
private _getNameIdentifier(testFilePath: string, testCaseResult: TestCaseResultArg) {
return [testFilePath, ...testCaseResult.ancestorTitles, testCaseResult.title].join('\u001F');
}
private _getCompletionEventType(
testCaseResult: TestCaseResultArg,
): 'test_done' | 'test_skip' | 'test_todo' {
switch (testCaseResult.status) {
case 'passed':
case 'failed': {
return 'test_done';
}
case 'todo': {
return 'test_todo';
}
case 'skipped':
case 'pending':
case 'disabled': {
return 'test_skip';
}
default: {
throw new JestMetadataError(`Unexpected test case result status: ${testCaseResult.status}`);
}
}
}
private _determineFallbackModeStatus(testFilePath: string, file: TestFileMetadata): boolean {
if (!this._fallbackModes.has(testFilePath)) {
this._fallbackModes.set(testFilePath, !file.rootDescribeBlock);
}
return this._fallbackModes.get(testFilePath)!;
}
}
type TestEntryInfo = {
testId: string;
testFilePath: string;
testEntryMetadata: TestEntryMetadata;
/** Only or the last invocation */
testCaseResult: TestCaseResultArg;
};