import _ from 'lodash' import { action, observable } from 'mobx' import AgentModel, { AgentProps } from '../agents/agent-model' import CommandModel, { CommandProps } from '../commands/command-model' import { HookProps } from '../hooks/hook-model' import appState, { AppState } from '../lib/app-state' import scroller, { Scroller } from '../lib/scroller' import RouteModel, { RouteProps } from '../routes/route-model' import TestModel, { TestProps, UpdatableTestProps, UpdateTestCallback } from '../test/test-model' import RunnableModel from './runnable-model' import SuiteModel, { SuiteProps } from './suite-model' import { SessionProps } from '../sessions/sessions-model' const defaults = { hasSingleTest: false, hasTests: false, isReady: false, attemptingShowSnapshot: false, showingSnapshot: false, } interface Props { appState: AppState scroller: Scroller } export type LogProps = AgentProps | CommandProps | RouteProps | SessionProps export type RunnableArray = Array export type Log = AgentModel | CommandModel | RouteModel export interface RootRunnable { hooks?: Array tests?: Array suites?: Array } type RunnableType = 'test' | 'suite' type TestOrSuite = T extends TestProps ? TestProps : SuiteProps export class RunnablesStore { @observable isReady = defaults.isReady @observable runnables: RunnableArray = [] /** * Stores a list of all the runables files where the reporter * has passed without any specific order. * * key: spec FilePath * content: RunableArray */ @observable runnablesHistory: Record = {} runningSpec: string | null = null hasTests: boolean = false hasSingleTest: boolean = false private appState: AppState private scroller: Scroller [key: string]: any _tests: Record = {} _logs: Record = {} _runnablesQueue: Array = [] attemptingShowSnapshot = defaults.attemptingShowSnapshot showingSnapshot = defaults.showingSnapshot constructor ({ appState, scroller }: Props) { this.appState = appState this.scroller = scroller } setRunnables (rootRunnable: RootRunnable) { this.runnables = this._createRunnableChildren(rootRunnable, 0) this.isReady = true const numTests = _.keys(this._tests).length this.hasTests = numTests > 0 this.hasSingleTest = numTests === 1 this._finishedInitialRendering() } _createRunnableChildren (runnableProps: RootRunnable, level: number) { return this._createRunnables('test', runnableProps.tests || [], runnableProps.hooks || [], level).concat( this._createRunnables('suite', runnableProps.suites || [], runnableProps.hooks || [], level), ) } _createRunnables (type: RunnableType, runnables: Array>, hooks: Array, level: number) { return _.map(runnables, (runnableProps) => { return this._createRunnable(type, runnableProps, hooks, level) }) } _createRunnable (type: RunnableType, props: TestOrSuite, hooks: Array, level: number) { props.hooks = _.unionBy(props.hooks, hooks, 'hookId') return type === 'suite' ? this._createSuite(props as SuiteProps, level) : this._createTest(props as TestProps, level) } _createSuite (props: SuiteProps, level: number) { const suite = new SuiteModel(props, level) this._runnablesQueue.push(suite) suite.children = this._createRunnableChildren(props, ++level) return suite } _createTest (props: TestProps, level: number) { const test = new TestModel(props, level, this) this._runnablesQueue.push(test) this._tests[test.id] = test return test } _finishedInitialRendering () { if (this.appState.isRunning) { // have an initial scrollTop set, meaning we reloaded from a domain change // so reset to the saved scrollTop if (this._initialScrollTop) this.scroller.setScrollTop(this._initialScrollTop) } else { // finished running before initial rendering complete (but wasn't manually // stopped), meaning some tests didn't get a chance to get scrolled to // scroll to the end since that's the right place to be if (this.appState.autoScrollingEnabled && !this.appState.isStopped) { this.scroller.scrollToEnd() } } } setInitialScrollTop (initialScrollTop: number) { this._initialScrollTop = initialScrollTop } updateTest (props: UpdatableTestProps, cb: UpdateTestCallback) { this._withTest(props.id, (test) => { test.update(props, cb) }) } runnableStarted (props: TestProps) { this._withTest(props.id, (test) => { test.start(props) }) } runnableFinished (props: TestProps) { this._withTest(props.id, (test) => { test.finish(props) }) } testById (id: string) { return this._tests[id] } addLog (log: LogProps) { this._withTest(log.testId, (test) => { test.addLog(log) }) } _withTest (id: string, cb: ((test: TestModel) => void)) { // we get events for suites and tests, but only tests change during a run, // so if the id isn't found in this._tests, we ignore it b/c it's a suite const test = this._tests[id] if (test) cb(test) } updateLog (props: LogProps) { this._withTest(props.testId, (test) => { test.updateLog(props) }) } removeLog (props: LogProps) { this._withTest(props.testId, (test) => { test.removeLog(props) }) } reset () { _.each(defaults, (value, key) => { this[key] = value }) this.runnables = [] this.runnablesHistory = {} this._tests = {} this._runnablesQueue = [] } @action setRunningSpec (specPath: string) { const previousSpec = this.runningSpec this.runningSpec = specPath if (!previousSpec || previousSpec === specPath) { return } this.runnablesHistory[previousSpec] = this.runnables } } export default new RunnablesStore({ appState, scroller })