UNPKG

good-vibes

Version:

Good Vibes is a Node.js testing library inspired by Alicia Keys Tiny Desk Concert

204 lines (193 loc) 6.06 kB
import * as isEqual from "lodash.isequal"; import * as fs from "fs/promises"; import * as Diff from "diff"; import Context, { Resolve } from "./Context"; import log, { LogLevel } from "./log"; enum TestStatus { PASSTHROUGH, FAILED, PASSED, } // assert 4 failed // 4/5 assertions passed export default class TestContext extends Context { #groupName: string; #testStatus: TestStatus; readonly snapshotsDirectory: string; #snapshotsDirectoryInitCompleted: boolean; #snapshotsDirectoryInitFailed: boolean; #passedAssertions: number; #failedAssertions: number; constructor( testName: string, groupName: string, resolve: Resolve, snapshotsDirectory?: string ) { super(testName, resolve); this.#groupName = groupName; this.snapshotsDirectory = snapshotsDirectory ? `${snapshotsDirectory}/${this.#groupName}` : `./test/__snapshots__/${this.#groupName}`; this.#snapshotsDirectoryInitCompleted = false; this.#snapshotsDirectoryInitFailed = false; this.#testStatus = TestStatus.PASSTHROUGH; this.#passedAssertions = 0; this.#failedAssertions = 0; } #setTestStatus(status: TestStatus.PASSED | TestStatus.FAILED) { if ( this.#testStatus === TestStatus.FAILED && status === TestStatus.PASSED ) { this.#testStatus = TestStatus.FAILED; } else { this.#testStatus = status; } } check<Type>(expectedValue: Type, actualValue: Type): TestContext { const result = isEqual(expectedValue, actualValue); if (!result) { this.#failedAssertions++; this.logger( `Assertion ${this.#failedAssertions}: Expected ${JSON.stringify( expectedValue )} to match ${JSON.stringify(actualValue)}`, LogLevel.ERROR ); this.#setTestStatus(TestStatus.FAILED); } else { this.#passedAssertions++; this.#setTestStatus(TestStatus.PASSED); } return this; } async #initSnapshotsDirectory() { if (this.#snapshotsDirectoryInitCompleted) { return; // no need to re-init } if (this.#snapshotsDirectoryInitFailed) { this.logger( `Failed to create snapshots directory in a previous attempt, will not retry` ); return; // no need to re-init } this.logger( `Checking if specified directory for snapshots ${this.snapshotsDirectory} exists` ); let stat; try { stat = await fs.stat(this.snapshotsDirectory); } catch (e) { this.logger(`Snapshots directory does not exist, trying to create now`); try { await fs.mkdir(this.snapshotsDirectory, { recursive: true }); this.logger(`Snapshots directory created, lets go!`); this.#snapshotsDirectoryInitCompleted = true; } catch (e) { this.logger(`Failed to create snapshots directory due to error: ${e}`); this.#snapshotsDirectoryInitFailed = true; } } if (stat) { if (!stat.isDirectory()) { this.logger( `Specified path for snapshots (${this.snapshotsDirectory}) exists but is not a directory` ); this.#snapshotsDirectoryInitFailed = true; } else { log(`Directory already exists, lets go!`); this.#snapshotsDirectoryInitCompleted = true; } } } #getSnapshotPath(assertionName: string): string { return `${this.snapshotsDirectory}/${this.name}_${assertionName}.json`; } async snapshot<Type>( assertionName: string, actualValue: Type, rebase?: boolean ): Promise<TestContext> { if (rebase) { await this.#initSnapshotsDirectory(); this.logger( `Writing snapshot baseline for ${assertionName} to ${this.#getSnapshotPath( assertionName )}` ); try { await fs.writeFile( this.#getSnapshotPath(assertionName), JSON.stringify(actualValue) ); } catch (e) { this.logger( `Failed to write snapshot baseline file due to error, ${e}` ); } this.logger(`Failing test in case rebase flag was set by mistake`); this.#setTestStatus(TestStatus.FAILED); } else { const snapshotPath = this.#getSnapshotPath(assertionName); let data; try { data = await fs.readFile(snapshotPath, { encoding: "utf8", }); } catch (e) { this.logger(`Could not find snapshot file at path ${snapshotPath}`); this.#setTestStatus(TestStatus.FAILED); return Promise.resolve(this); } const existingValue = JSON.parse(data); const diff = Diff.diffJson(actualValue, existingValue); if (diff?.length) { const failed = diff.reduce((failed, part) => { failed = failed || part.added || part.removed; return failed; }, false); if (failed) { this.logger( `Snapshot ${assertionName}: FAILED, please see diff below for details\n`, LogLevel.ERROR ); diff.forEach((part) => { log( part.value, part.added ? LogLevel.ERROR : part.removed ? LogLevel.SUCCESS : LogLevel.INFO ); }); this.#setTestStatus(TestStatus.FAILED); } else { this.logger(`Snapshot ${assertionName}: PASSED`, LogLevel.SUCCESS); this.#setTestStatus(TestStatus.PASSED); } } else { this.logger( "Test failed as baseline was empty, run with rebase flag set to true to generate file" ); this.#setTestStatus(TestStatus.FAILED); } } return Promise.resolve(this); } done() { if (this.#passedAssertions) { this.logger( `${this.#passedAssertions}/${ this.#failedAssertions + this.#passedAssertions } Assertions PASSED`, LogLevel.SUCCESS ); } this.resolve( this.#testStatus === TestStatus.PASSTHROUGH || this.#testStatus === TestStatus.PASSED ); } }