UNPKG

@qaflag/core

Version:

Base requirements for the QA Flag library

207 lines (191 loc) 6.54 kB
import { ScenarioConstructor, ScenarioInitOpts, } from '../scenario/scenario.options'; import { KvStore } from '../models/kv-store'; import { Logger } from '../models/logger'; import { ScenarioInterface } from '../scenario/scenario.interface'; import { SuiteEvents, SuiteInterface, SuiteResults, SuiteStep, } from './suite.interface'; import { EventEmitter } from 'events'; import TypedEmitter from 'typed-emitter'; import { PersonaInterface } from '../persona/persona.interface'; import { DefaultUser } from '../persona/persona'; import { BeforeSymbol } from '../decorators/before.decorator'; import { AfterSymbol } from '../decorators/after.decorator'; import { TestCase } from '../case/case'; import { ProjectSettings } from '../types/project-settings.interface'; export const ScenarioDefinitions = Symbol('ScenarioDefinitions'); export const CaseDefinitions = Symbol('CaseDefinitions'); export type SuiteOpts = { title: string; persona?: PersonaInterface; type?: ScenarioConstructor; baseUrl?: string; }; export function Suite(suiteOpts: SuiteOpts) { return class SuiteAbstract implements SuiteInterface { public readonly title = suiteOpts.title; public readonly store = new KvStore(); public readonly logger = new Logger(); public readonly events = new EventEmitter() as TypedEmitter<SuiteEvents>; public readonly scenarios: ScenarioInterface[] = []; public readonly cases: TestCase[] = []; public readonly steps: SuiteStep[] = []; public readonly persona: PersonaInterface = suiteOpts.persona || new DefaultUser(); public readonly baseUrl: string | undefined; public get defaultScenarioOpts() { return { baseUrl: this.baseUrl, }; } public get befores() { return Object.entries<Function>(this[BeforeSymbol] || {}); } public get afters() { return Object.entries<Function>(this[AfterSymbol] || {}); } constructor(public readonly settings: ProjectSettings) { this.baseUrl = suiteOpts.baseUrl || settings.baseUrl; // Add scenarios to this instance const scenarioMethods: { [methodName: string]: ScenarioInitOpts } = this[ScenarioDefinitions]; if (scenarioMethods) { Object.values(scenarioMethods) .sort((a, b) => a.step - b.step) .forEach(scenarioOpts => { const scenarioConstructor = scenarioOpts.type || suiteOpts.type; if (!scenarioConstructor) { throw 'No Scenario Type defined. It must be set either in the Scenario decorator in the Suite decorator, as a default.'; } const scenario = this.addScenarioToStep( new scenarioConstructor( { ...this.defaultScenarioOpts, ...scenarioOpts, }, this, ), ); this.scenarios.push(scenario); }); } // Add Cases const caseMethods: { [methodName: string]: TestCase } = this[CaseDefinitions]; if (caseMethods) { Object.values(caseMethods).forEach(testCase => this.cases.push(testCase), ); } } public async __execute() { this.logger.start(); await this.persona.__startUp(this); await Promise.all( this.scenarios.map(scenario => scenario.persona?.__startUp(this)), ); this.events.emit('beforeAll'); await Promise.all( this.befores.map(([methodName, before]) => before(this, this.defaultScenarioOpts), ), ); for (const step of this.steps) { let fatalError = false; await Promise.all( step.scenarios.map(async scenario => { try { this.events.emit('beforeEach', scenario); await scenario.__startUp(); await scenario.__execute(); try { await scenario.__next(scenario); } catch (ex) { scenario.logger.fail(`${ex}`); } await scenario.__tearDown(); this.events.emit('afterEach', scenario); this.logger.log(scenario.status == 'pass' ? 'pass' : 'fail', { text: scenario.title, }); } catch (ex) { scenario.logger.fail(ex); this.logger.fail(scenario.title); fatalError = true; } }), ); if (fatalError) break; } await Promise.all( this.afters.map(([methodName, after]) => after(this, this.defaultScenarioOpts), ), ); this.logger.end(); this.events.emit('completed'); this.events.emit(this.logger.failed ? 'failed' : 'passed'); } public getStep(stepNumber: number): SuiteStep { // Look for existing step with this number const step = this.steps.find(step => step.stepNumber === stepNumber); if (step) return step; // Create new step const newStep: SuiteStep = { stepNumber, scenarios: [], }; this.steps.push(newStep); this.steps.sort((a, b) => a.stepNumber - b.stepNumber); return newStep; } public addScenarioToStep(scenario: ScenarioInterface) { const step = this.getStep(scenario.step); step.scenarios.push(scenario); return scenario; } public set<T>(key: string, value: T): T { return this.store.set(key, value); } public get<T = any>(key: string): T { return this.store.get(key) as T; } public push<T = any>(key: string, value: T): T[] { return this.store.push(key, value); } public get results() { const results: SuiteResults = { status: 'pass', assertions: { passCount: 0, failCount: 0, optionalFailCount: 0, }, scenarios: { passCount: 0, failCount: 0, }, }; this.scenarios.forEach(scenario => { const scenarioResult = scenario.result; results.assertions.passCount += scenarioResult.passCount; results.assertions.failCount += scenarioResult.failCount; results.assertions.optionalFailCount += scenarioResult.optionalFailCount; if (scenarioResult.failCount > 0) { results.status = 'fail'; results.scenarios.failCount++; } else { results.scenarios.passCount++; } }); return results; } }; }