UNPKG

@hayspec/spec

Version:

Core logic for Hayspec framework.

316 lines (276 loc) 5.98 kB
import { Stage } from './stage'; import { Context } from './context'; import { Reporter } from './reporter'; /** * */ type StageHandler<Data> = (stage: Stage<Data>) => (void | Promise<void>); /** * */ type ContextHandler<Data> = (context: Context<Data>, stage: Stage<Data>) => (void | Promise<void>); /** * */ interface PerformRecipes<Data> { message: string; handler?: ContextHandler<Data> spec?: Spec<Data>; } /** * */ export class Spec<Data = {}> { protected beforeHandlers: StageHandler<Data>[] = []; protected beforeEachHandlers: ContextHandler<Data>[] = []; protected afterHandlers: StageHandler<Data>[] = []; protected afterEachHandlers: ContextHandler<Data>[] = []; protected performRecipes: PerformRecipes<Data>[] = []; protected onlyEnabled: boolean = false; protected _stage: Stage<Data>; public parent: Spec<Data>; /** * */ public constructor(stage?: Stage<Data>, parent?: Spec<Data>) { this.parent = parent || null; this.stage = stage || this.createStage(); } /** * */ public set stage(s: Stage<Data>) { if (this.parent) { this.parent.stage = s; } else { this._stage = s; } } /** * */ public get stage() { if (this.parent) { return this.parent.stage; } else { return this._stage; } } /** * */ public hasOnly() { return this.onlyEnabled; } /** * */ public isRoot() { return !this.parent; } /** * */ public before(handler: StageHandler<Data>, append: boolean = true) { if (append) { this.beforeHandlers.push(handler); } else { this.beforeHandlers.unshift(handler); } return this; } /** * */ public beforeEach(handler: ContextHandler<Data>, append: boolean = true) { if (append) { this.beforeEachHandlers.push(handler); } else { this.beforeEachHandlers.unshift(handler); } return this; } /** * */ public after(handler: StageHandler<Data>, append: boolean = true) { if (append) { this.afterHandlers.push(handler); } else { this.afterHandlers.unshift(handler); } return this; } /** * */ public afterEach(handler: ContextHandler<Data>, append: boolean = true) { if (append) { this.afterEachHandlers.push(handler); } else { this.afterEachHandlers.unshift(handler); } return this; } /** * */ public spec(message: string, spec: Spec<Data>) { const known = this.performRecipes.filter((r) => r.spec === spec).length > 0; if (!known) { spec.parent = this; spec.stage = this.stage; [].concat(this.beforeEachHandlers).reverse().forEach((h) => spec.beforeEach(h, false)); this.afterEachHandlers.forEach((h) => spec.afterEach(h)); } this.performRecipes.push({ message, spec }); return this; } /** * */ public test(message: string, handler: ContextHandler<Data>) { if (!this.onlyEnabled) { this.performRecipes.push({ message, handler }); } return this; } /** * */ public skip(message: string, handler?: ContextHandler<Data>) { this.performRecipes.push({ message, handler: null }); return this; } /** * */ public only(message: string, handler: ContextHandler<Data>) { if (!this.onlyEnabled) { this.performRecipes = this.performRecipes.filter((r) => !r.handler); this.onlyEnabled = true; } this.performRecipes.push({ message, handler }); return this; } /** * */ public async perform() { await this.performBegin(); await this.performBefore(); for (const recipe of this.performRecipes) { if (recipe.spec) { await this.performSpec(recipe); } else { await this.performTest(recipe); } } await this.performAfter(); await this.performEnd(); } /** * */ protected async performBegin() { if (this.isRoot()) { this.stage.reporter.begin(); } } /** * */ protected async performEnd() { if (this.isRoot()) { this.stage.reporter.end(); } } /** * */ protected async performSpec(recipe: PerformRecipes<Data>) { const start = Date.now(); this.stage.reporter.note({ type: 'SpecStartNote', message: recipe.message, }); await recipe.spec.perform(); this.stage.reporter.note({ type: 'SpecEndNote', duration: Date.now() - start, }); } /** * */ protected async performTest(recipe: PerformRecipes<Data>) { const start = Date.now(); this.stage.reporter.note({ type: 'TestStartNote', message: recipe.message, perform: !!recipe.handler, }); if (recipe.handler) { const context = this.createContext(); await this.performBeforeEach(context); await recipe.handler(context, this.stage); await this.performAfterEach(context); } this.stage.reporter.note({ type: 'TestEndNote', duration: Date.now() - start, }); } /** * */ protected async performBefore() { for (const handler of this.beforeHandlers) { await handler(this.stage); } } /** * */ protected async performAfter() { for (const handler of this.afterHandlers) { await handler(this.stage); } } /** * */ protected async performBeforeEach(context: Context<Data>) { for (const handler of this.beforeEachHandlers) { await handler(context, this.stage); } } /** * */ protected async performAfterEach(context: Context<Data>) { for (const handler of this.afterEachHandlers) { await handler(context, this.stage); } } /** * */ protected createStage() { const reporter = new Reporter(); return new Stage<Data>(reporter); } /** * */ protected createContext() { return new Context<Data>(this.stage); } };