diff --git a/src/index.ts b/src/index.ts index e8dc3c138..4f4c6ce3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,12 +22,14 @@ export { default as UsageFormatter } from './formatter/usage_formatter' export { default as UsageJsonFormatter } from './formatter/usage_json_formatter' export { formatterHelpers } -// Support Code Fuctions +// Support Code Functions const { methods } = supportCodeLibraryBuilder export const After = methods.After export const AfterAll = methods.AfterAll +export const AfterStep = methods.AfterStep export const Before = methods.Before export const BeforeAll = methods.BeforeAll +export const BeforeStep = methods.BeforeStep export const defineParameterType = methods.defineParameterType export const defineStep = methods.defineStep export const Given = methods.Given diff --git a/src/models/test_step_hook_definition.ts b/src/models/test_step_hook_definition.ts new file mode 100644 index 000000000..1832d1bb8 --- /dev/null +++ b/src/models/test_step_hook_definition.ts @@ -0,0 +1,34 @@ +import { PickleTagFilter } from '../pickle_filter' +import Definition, { + IDefinition, + IGetInvocationDataResponse, + IGetInvocationDataRequest, + IDefinitionParameters, + IHookDefinitionOptions, +} from './definition' +import { messages } from 'cucumber-messages' + +export default class TestStepHookDefinition extends Definition + implements IDefinition { + private readonly pickleTagFilter: PickleTagFilter + + constructor(data: IDefinitionParameters) { + super(data) + this.pickleTagFilter = new PickleTagFilter(data.options.tags) + } + + appliesToTestCase(pickle: messages.IPickle): boolean { + return this.pickleTagFilter.matchesAllTagExpressions(pickle) + } + + async getInvocationParameters({ + hookParameter, + }: IGetInvocationDataRequest): Promise { + return Promise.resolve({ + getInvalidCodeLengthMessage: () => + this.buildInvalidCodeLengthMessage('0 or 1', '2'), + parameters: [hookParameter], + validCodeLengths: [0, 1, 2], + }) + } +} diff --git a/src/runtime/pickle_runner.ts b/src/runtime/pickle_runner.ts index 6ed57bc48..0d29a1d00 100644 --- a/src/runtime/pickle_runner.ts +++ b/src/runtime/pickle_runner.ts @@ -2,7 +2,7 @@ import _ from 'lodash' import { getAmbiguousStepException } from './helpers' import AttachmentManager from './attachment_manager' import StepRunner from './step_runner' -import { messages, IdGenerator } from 'cucumber-messages' +import { IdGenerator, messages } from 'cucumber-messages' import { addDurations, getZeroDuration } from '../time' import { EventEmitter } from 'events' import { @@ -10,6 +10,7 @@ import { ITestCaseHookParameter, } from '../support_code_library_builder/types' import TestCaseHookDefinition from '../models/test_case_hook_definition' +import TestStepHookDefinition from '../models/test_step_hook_definition' import StepDefinition from '../models/step_definition' import { IDefinition } from '../models/definition' import { doesNotHaveValue } from '../value_checker' @@ -21,6 +22,7 @@ interface ITestStep { isBeforeHook?: boolean isHook: boolean hookDefinition?: TestCaseHookDefinition + stepHookDefinition?: TestStepHookDefinition pickleStep?: messages.Pickle.IPickleStep stepDefinitions?: StepDefinition[] } @@ -171,6 +173,18 @@ export default class PickleRunner { ) } + getBeforeStepHookDefinitions(): TestStepHookDefinition[] { + return this.supportCodeLibrary.beforeTestStepHookDefinitions.filter( + hookDefinition => hookDefinition.appliesToTestCase(this.pickle) + ) + } + + getAfterStepHookDefinitions(): TestStepHookDefinition[] { + return this.supportCodeLibrary.afterTestStepHookDefinitions.filter( + hookDefinition => hookDefinition.appliesToTestCase(this.pickle) + ) + } + getStepDefinitions( pickleStep: messages.Pickle.IPickleStep ): StepDefinition[] { @@ -323,6 +337,21 @@ export default class PickleRunner { return this.invokeStep(null, hookDefinition, hookParameter) } + async runStepHook( + stepHookDefinition: TestStepHookDefinition + ): Promise { + if (this.isSkippingSteps()) { + return messages.TestResult.fromObject({ status: Status.SKIPPED }) + } + const hookParameter: ITestCaseHookParameter = { + gherkinDocument: this.gherkinDocument, + pickle: this.pickle, + testCaseStartedId: this.currentTestCaseStartedId, + } + + return this.invokeStep(null, stepHookDefinition, hookParameter) + } + async runStep(testStep: ITestStep): Promise { if (testStep.stepDefinitions.length === 0) { return messages.TestResult.fromObject({ status: Status.UNDEFINED }) @@ -334,6 +363,78 @@ export default class PickleRunner { } else if (this.isSkippingSteps()) { return messages.TestResult.fromObject({ status: Status.SKIPPED }) } - return this.invokeStep(testStep.pickleStep, testStep.stepDefinitions[0]) + let stepResult + let afterStepHooksResult + const beforeStepHooksResult = await this.runStepHooks( + this.getBeforeStepHookDefinitions() + ) + + if (beforeStepHooksResult.status !== Status.FAILED) { + stepResult = await this.invokeStep( + testStep.pickleStep, + testStep.stepDefinitions[0] + ) + if (stepResult.status === Status.PASSED) { + afterStepHooksResult = await this.runStepHooks( + this.getAfterStepHookDefinitions() + ) + } + } + let cumulatedStepResult = beforeStepHooksResult + + if (stepResult !== undefined) { + cumulatedStepResult = stepResult + if (beforeStepHooksResult.duration !== null) { + cumulatedStepResult.duration = addDurations( + cumulatedStepResult.duration, + beforeStepHooksResult.duration + ) + } + if (afterStepHooksResult !== undefined) { + if (afterStepHooksResult.duration !== null) { + cumulatedStepResult.duration = addDurations( + cumulatedStepResult.duration, + afterStepHooksResult.duration + ) + } + if (this.shouldUpdateStatus(afterStepHooksResult)) { + cumulatedStepResult.status = afterStepHooksResult.status + } + if (afterStepHooksResult.message !== null) { + cumulatedStepResult.message = afterStepHooksResult.message + } + } + } + + return cumulatedStepResult + } + + async runStepHooks( + stepHooks: TestStepHookDefinition[] + ): Promise { + const stepHooksResult = messages.TestResult.fromObject({ + status: + this.result.status === Status.FAILED + ? Status.SKIPPED + : this.result.status, + }) + + for (const stepHookDefinition of stepHooks) { + const stepHookResult = await this.runStepHook(stepHookDefinition) + if (this.shouldUpdateStatus(stepHookResult)) { + stepHooksResult.status = stepHookResult.status + this.result.status = stepHookResult.status + } + if (stepHookResult.message !== null) { + stepHooksResult.message = stepHookResult.message + } + if (stepHookResult.duration !== null) { + stepHooksResult.duration = + stepHooksResult.duration !== null + ? addDurations(stepHooksResult.duration, stepHookResult.duration) + : stepHookResult.duration + } + } + return stepHooksResult } } diff --git a/src/support_code_library_builder/index.ts b/src/support_code_library_builder/index.ts index 5b141d269..9ad5d63e7 100644 --- a/src/support_code_library_builder/index.ts +++ b/src/support_code_library_builder/index.ts @@ -2,6 +2,7 @@ import _ from 'lodash' import { buildParameterType, getDefinitionLineAndUri } from './build_helpers' import { IdGenerator } from 'cucumber-messages' import TestCaseHookDefinition from '../models/test_case_hook_definition' +import TestStepHookDefinition from '../models/test_step_hook_definition' import TestRunHookDefinition from '../models/test_run_hook_definition' import StepDefinition from '../models/step_definition' import { formatLocation } from '../formatter/helpers' @@ -19,7 +20,9 @@ import { DefineStepPattern, IDefineStepOptions, IDefineTestCaseHookOptions, + IDefineTestStepHookOptions, TestCaseHookFunction, + TestStepHookFunction, IDefineTestRunHookOptions, ISupportCodeLibrary, IParameterTypeDefinition, @@ -41,6 +44,13 @@ interface ITestCaseHookDefinitionConfig { uri: string } +interface ITestStepHookDefinitionConfig { + code: any + line: number + options: any + uri: string +} + interface ITestRunHookDefinitionConfig { code: any line: number @@ -53,8 +63,10 @@ export class SupportCodeLibraryBuilder { private afterTestCaseHookDefinitionConfigs: ITestCaseHookDefinitionConfig[] private afterTestRunHookDefinitionConfigs: ITestRunHookDefinitionConfig[] + private afterTestStepHookDefinitionConfigs: ITestStepHookDefinitionConfig[] private beforeTestCaseHookDefinitionConfigs: ITestCaseHookDefinitionConfig[] private beforeTestRunHookDefinitionConfigs: ITestRunHookDefinitionConfig[] + private beforeTestStepHookDefinitionConfigs: ITestStepHookDefinitionConfig[] private cwd: string private defaultTimeout: number private definitionFunctionWrapper: any @@ -72,12 +84,18 @@ export class SupportCodeLibraryBuilder { AfterAll: this.defineTestRunHook( () => this.afterTestRunHookDefinitionConfigs ), + AfterStep: this.defineTestStepHook( + () => this.afterTestStepHookDefinitionConfigs + ), Before: this.defineTestCaseHook( () => this.beforeTestCaseHookDefinitionConfigs ), BeforeAll: this.defineTestRunHook( () => this.beforeTestRunHookDefinitionConfigs ), + BeforeStep: this.defineTestStepHook( + () => this.beforeTestStepHookDefinitionConfigs + ), defineParameterType: this.defineParameterType.bind(this), defineStep, Given: defineStep, @@ -155,6 +173,37 @@ export class SupportCodeLibraryBuilder { } } + defineTestStepHook( + getCollection: () => ITestStepHookDefinitionConfig[] + ): ( + options: string | IDefineTestStepHookOptions | TestStepHookFunction, + code?: TestStepHookFunction + ) => void { + return ( + options: string | IDefineTestStepHookOptions | TestStepHookFunction, + code?: TestStepHookFunction + ) => { + if (typeof options === 'string') { + options = { tags: options } + } else if (typeof options === 'function') { + code = options + options = {} + } + const { line, uri } = getDefinitionLineAndUri(this.cwd) + validateArguments({ + args: { code, options }, + fnName: 'defineTestStepHook', + location: formatLocation({ line, uri }), + }) + getCollection().push({ + code, + line, + options, + uri, + }) + } + } + defineTestRunHook( getCollection: () => ITestRunHookDefinitionConfig[] ): (options: IDefineTestRunHookOptions | Function, code?: Function) => void { @@ -215,6 +264,25 @@ export class SupportCodeLibraryBuilder { }) } + buildTestStepHookDefinitions( + configs: ITestStepHookDefinitionConfig[] + ): TestStepHookDefinition[] { + return configs.map(({ code, line, options, uri }) => { + const wrappedCode = this.wrapCode({ + code, + wrapperOptions: options.wrapperOptions, + }) + return new TestStepHookDefinition({ + code: wrappedCode, + id: this.newId(), + line, + options, + unwrappedCode: code, + uri, + }) + }) + } + buildTestRunHookDefinitions( configs: ITestRunHookDefinitionConfig[] ): TestRunHookDefinition[] { @@ -279,12 +347,18 @@ export class SupportCodeLibraryBuilder { afterTestRunHookDefinitions: this.buildTestRunHookDefinitions( this.afterTestRunHookDefinitionConfigs ).reverse(), + afterTestStepHookDefinitions: this.buildTestStepHookDefinitions( + this.afterTestStepHookDefinitionConfigs + ).reverse(), beforeTestCaseHookDefinitions: this.buildTestCaseHookDefinitions( this.beforeTestCaseHookDefinitionConfigs ), beforeTestRunHookDefinitions: this.buildTestRunHookDefinitions( this.beforeTestRunHookDefinitionConfigs ), + beforeTestStepHookDefinitions: this.buildTestStepHookDefinitions( + this.beforeTestStepHookDefinitionConfigs + ), defaultTimeout: this.defaultTimeout, parameterTypeRegistry: this.parameterTypeRegistry, stepDefinitions: this.buildStepDefinitions(), @@ -297,8 +371,10 @@ export class SupportCodeLibraryBuilder { this.newId = newId this.afterTestCaseHookDefinitionConfigs = [] this.afterTestRunHookDefinitionConfigs = [] + this.afterTestStepHookDefinitionConfigs = [] this.beforeTestCaseHookDefinitionConfigs = [] this.beforeTestRunHookDefinitionConfigs = [] + this.beforeTestStepHookDefinitionConfigs = [] this.definitionFunctionWrapper = null this.defaultTimeout = 5000 this.parameterTypeRegistry = new ParameterTypeRegistry() diff --git a/src/support_code_library_builder/types.ts b/src/support_code_library_builder/types.ts index b5783e2f9..ec2f88119 100644 --- a/src/support_code_library_builder/types.ts +++ b/src/support_code_library_builder/types.ts @@ -1,5 +1,6 @@ import { messages } from 'cucumber-messages' import TestCaseHookDefinition from '../models/test_case_hook_definition' +import TestStepHookDefinition from '../models/test_step_hook_definition' import TestRunHookDefinition from '../models/test_run_hook_definition' import StepDefinition from '../models/step_definition' import { ParameterTypeRegistry } from 'cucumber-expressions' @@ -13,6 +14,13 @@ export interface ITestCaseHookParameter { testCaseStartedId: string } +export interface ITestStepHookParameter { + gherkinDocument: messages.IGherkinDocument + pickle: messages.IPickle + result?: messages.ITestResult + testCaseStartedId: string +} + export type TestCaseHookFunctionWithoutParameter = () => void export type TestCaseHookFunctionWithParameter = ( arg: ITestCaseHookParameter @@ -21,6 +29,14 @@ export type TestCaseHookFunction = | TestCaseHookFunctionWithoutParameter | TestCaseHookFunctionWithParameter +export type TestStepHookFunctionWithoutParameter = () => void +export type TestStepHookFunctionWithParameter = ( + arg: ITestStepHookParameter +) => void +export type TestStepHookFunction = + | TestStepHookFunctionWithoutParameter + | TestStepHookFunctionWithParameter + export interface IDefineStepOptions { timeout?: number wrapperOptions?: any @@ -31,6 +47,11 @@ export interface IDefineTestCaseHookOptions { timeout?: number } +export interface IDefineTestStepHookOptions { + tags?: string + timeout?: number +} + export interface IDefineTestRunHookOptions { timeout?: number } @@ -57,11 +78,23 @@ export interface IDefineSupportCodeMethods { After(code: TestCaseHookFunction): void After(tags: string, code: TestCaseHookFunction): void After(options: IDefineTestCaseHookOptions, code: TestCaseHookFunction): void + AfterStep(code: TestStepHookFunction): void + AfterStep(tags: string, code: TestStepHookFunction): void + AfterStep( + options: IDefineTestStepHookOptions, + code: TestStepHookFunction + ): void AfterAll(code: Function): void AfterAll(options: IDefineTestRunHookOptions, code: Function): void Before(code: TestCaseHookFunction): void Before(tags: string, code: TestCaseHookFunction): void Before(options: IDefineTestCaseHookOptions, code: TestCaseHookFunction): void + BeforeStep(code: TestStepHookFunction): void + BeforeStep(tags: string, code: TestStepHookFunction): void + BeforeStep( + options: IDefineTestStepHookOptions, + code: TestStepHookFunction + ): void BeforeAll(code: Function): void BeforeAll(options: IDefineTestRunHookOptions, code: Function): void Given(pattern: DefineStepPattern, code: Function): void @@ -86,8 +119,10 @@ export interface IDefineSupportCodeMethods { export interface ISupportCodeLibrary { readonly afterTestCaseHookDefinitions: TestCaseHookDefinition[] + readonly afterTestStepHookDefinitions: TestStepHookDefinition[] readonly afterTestRunHookDefinitions: TestRunHookDefinition[] readonly beforeTestCaseHookDefinitions: TestCaseHookDefinition[] + readonly beforeTestStepHookDefinitions: TestStepHookDefinition[] readonly beforeTestRunHookDefinitions: TestRunHookDefinition[] readonly defaultTimeout: number readonly stepDefinitions: StepDefinition[] diff --git a/src/support_code_library_builder/validate_arguments.ts b/src/support_code_library_builder/validate_arguments.ts index 1984b9e80..70317c8d2 100644 --- a/src/support_code_library_builder/validate_arguments.ts +++ b/src/support_code_library_builder/validate_arguments.ts @@ -54,6 +54,18 @@ const validations: Dictionary = { optionsTimeoutValidation, { identifier: 'second argument', ...fnValidation }, ], + defineTestStepHook: [ + { identifier: 'first argument', ...optionsValidation }, + { + identifier: '"options.tags"', + expectedType: 'string', + predicate({ options }) { + return doesNotHaveValue(options.tags) || _.isString(options.tags) + }, + }, + optionsTimeoutValidation, + { identifier: 'second argument', ...fnValidation }, + ], defineStep: [ { identifier: 'first argument',