Skip to content

Commit

Permalink
Remove global testState
Browse files Browse the repository at this point in the history
This entails creating step- & hook-registries *per test* when running in
all-mode and with non-global step definitions. This will become
important for the next step.
  • Loading branch information
badeball committed May 28, 2020
1 parent e010dea commit bf9ef17
Show file tree
Hide file tree
Showing 18 changed files with 456 additions and 521 deletions.
4 changes: 3 additions & 1 deletion cypress-tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ const glob = require("glob");
const fs = require("fs");
const { execFileSync } = require("child_process");

const { shouldProceedCurrentStep } = require("./lib/tagsHelper");
const {
shouldProceedCurrentStep
} = require("./lib/browser-runtime/tagsHelper");

const debug = (message, ...rest) =>
process.env.DEBUG
Expand Down
88 changes: 88 additions & 0 deletions lib/browser-runtime/createRegistries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const {
CucumberExpression,
RegularExpression,
ParameterTypeRegistry
} = require("cucumber-expressions");

const { shouldProceedCurrentStep } = require("./tagsHelper");

class StepDefinitionRegistry {
constructor() {
this.definitions = {};
this.runtime = {};
this.options = {
parameterTypeRegistry: new ParameterTypeRegistry()
};

this.definitions = [];
this.runtime = (matcher, implementation) => {
let expression;
if (matcher instanceof RegExp) {
expression = new RegularExpression(
matcher,
this.options.parameterTypeRegistry
);
} else {
expression = new CucumberExpression(
matcher,
this.options.parameterTypeRegistry
);
}

this.definitions.push({
implementation,
expression
});
};

this.resolve = text => {
const matchingSteps = this.definitions.filter(({ expression }) =>
expression.match(text)
);

if (matchingSteps.length === 0) {
throw new Error(`Step implementation missing for: ${text}`);
} else if (matchingSteps.length > 1) {
throw new Error(`Multiple implementations exists for: ${text}`);
} else {
return matchingSteps[0];
}
};
}
}

class HookRegistry {
constructor() {
this.definitions = [];
this.runtime = {};

this.runtime = (tags, implementation) => {
this.definitions.push({
tags,
implementation
});
};

this.resolve = scenarioTags =>
this.definitions.filter(
({ tags }) =>
!tags ||
tags.length === 0 ||
shouldProceedCurrentStep(scenarioTags, tags)
);
}
}

function createRegistries() {
const stepDefinitionRegistry = new StepDefinitionRegistry();
const beforeHookRegistry = new HookRegistry();
const afterHookRegistry = new HookRegistry();

return {
stepDefinitionRegistry,
beforeHookRegistry,
afterHookRegistry
};
}

module.exports = createRegistries;
208 changes: 208 additions & 0 deletions lib/browser-runtime/createTestsFromFeatures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
const DataTable = require("cucumber/lib/models/data_table").default;
const statuses = require("cucumber/lib/status").default;
const { CucumberDataCollector } = require("./cukejson/cucumberDataCollector");
const { generateCucumberJson } = require("./cukejson/generateCucumberJson");
const { shouldProceedCurrentStep } = require("./tagsHelper");
const createRegitries = require("./createRegistries");
const populateGlobalMethods = require("./populateGlobalMethods");

function resolveAndRunHooks(hookRegistry, scenarioTags, featureName) {
return window.Cypress.Promise.each(
hookRegistry.resolve(scenarioTags, featureName),
({ implementation }) => implementation.call(this)
);
}

function resolveStepArgument(args) {
const [argument] = args;
if (!argument) {
return argument;
}
if (argument.rows) {
return new DataTable(argument);
}
if (argument.content) {
return argument.content;
}
return argument;
}

function resolveAndRunStepDefinition(
stepDefinitionRegistry,
step,
featureName
) {
const stepText = step.text;
const { expression, implementation } = stepDefinitionRegistry.resolve(
stepText,
featureName
);
const argument = resolveStepArgument(step.arguments);
return implementation.call(
this,
...expression.match(stepText).map(match => match.getValue()),
argument
);
}

const cleanupFilename = s => s.split(".")[0];

const writeCucumberJsonFile = (json, preprocessorConfig) => {
const outputFolder =
preprocessorConfig.cucumberJson.outputFolder || "cypress/cucumber-json";
const outputPrefix = preprocessorConfig.cucumberJson.filePrefix || "";
const outputSuffix =
preprocessorConfig.cucumberJson.fileSuffix || ".cucumber";
const fileName = json[0] ? cleanupFilename(json[0].uri) : "empty";
const outFile = `${outputFolder}/${outputPrefix}${fileName}${outputSuffix}.json`;
cy.writeFile(outFile, json, { log: false });
};

function createTestsFromFeatures(options) {
const { features, preprocessorConfig, globalFilesToRequireFn } = options;

const tagsUsedInTests = features
.flatMap(feature => feature.pickles)
.flatMap(pickle => pickle.tags)
.map(tag => tag.name);

const envTags = Cypress.env("TAGS");

let tagFilter = null;

if (tagsUsedInTests.includes("@focus")) {
tagFilter = "@focus";
} else if (envTags) {
tagFilter = envTags;
}

let stepDefinitionRegistry;
let beforeHookRegistry;
let afterHookRegistry;

if (globalFilesToRequireFn) {
({
stepDefinitionRegistry,
beforeHookRegistry,
afterHookRegistry
} = createRegitries());

populateGlobalMethods({
stepDefinitionRegistry,
beforeHookRegistry,
afterHookRegistry
});

globalFilesToRequireFn();
}

// eslint-disable-next-line no-restricted-syntax
for (const {
filePath,
source,
feature,
pickles,
localFilesToRequireFn
} of features) {
const testState = new CucumberDataCollector(filePath, source, feature);

if (localFilesToRequireFn) {
({
stepDefinitionRegistry,
beforeHookRegistry,
afterHookRegistry
} = createRegitries());

populateGlobalMethods({
stepDefinitionRegistry,
beforeHookRegistry,
afterHookRegistry
});

localFilesToRequireFn();
}

// eslint-disable-next-line no-loop-func
describe(feature.name, () => {
before(() => {
cy.then(() => testState.onStartTest());
});

beforeEach(() => {
/**
* Left for legacy support, but it's not something we rely on (nor should you).
*/
window.testState = testState;

const failHandler = err => {
Cypress.off("fail", failHandler);
testState.onFail(err);
throw err;
};

Cypress.on("fail", failHandler);
});

const picklesToRun = pickles.filter(
pickle => !tagFilter || shouldProceedCurrentStep(pickle.tags, tagFilter)
);

picklesToRun.forEach(pickle => {
const indexedSteps = pickle.steps.map((step, index) =>
Object.assign({}, step, { index })
);

it(pickle.name, function() {
return cy
.then(() => testState.onStartScenario(pickle, indexedSteps))
.then(() =>
resolveAndRunHooks.call(
this,
beforeHookRegistry,
pickle.tags,
feature.name
)
)
.then(() =>
indexedSteps.forEach(step => {
cy.then(() => testState.onStartStep(step))
.then(() =>
resolveAndRunStepDefinition.call(
this,
stepDefinitionRegistry,
step,
testState.feature.name
)
)
.then(() => testState.onFinishStep(step, statuses.PASSED));
})
)
.then(() =>
resolveAndRunHooks.call(
this,
afterHookRegistry,
pickle.tags,
feature.name
)
)
.then(() => testState.onFinishScenario(pickle));
});
});

after(() => {
cy.then(() => testState.onFinishTest()).then(() => {
if (
preprocessorConfig &&
preprocessorConfig.cucumberJson &&
preprocessorConfig.cucumberJson.generate
) {
const json = generateCucumberJson(testState);
writeCucumberJsonFile(json, preprocessorConfig);
}
});
});
});
}
}

module.exports = createTestsFromFeatures;
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { EventEmitter } = require("events");
const { generateEvents } = require("../parserHelpers");
const JsonFormatter = require("cucumber/lib/formatter/json_formatter").default;
const formatterHelpers = require("cucumber/lib/formatter/helpers");
const { generateEvents } = require("../../parserHelpers");

function last(collection) {
return collection[collection.length - 1];
Expand Down
3 changes: 3 additions & 0 deletions lib/browser-runtime/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const createTestsFromFeatures = require("./createTestsFromFeatures");

module.exports = { createTestsFromFeatures };
62 changes: 62 additions & 0 deletions lib/browser-runtime/populateGlobalMethods.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const {
defineParameterType
} = require("cucumber/lib/support_code_library_builder/define_helpers");

function parseHookArgs(args) {
if (args.length === 2) {
if (typeof args[0] !== "object" || typeof args[0].tags !== "string") {
throw new Error(
"Hook definitions with two arguments should have an object containing tags (string) as the first argument."
);
}
if (typeof args[1] !== "function") {
throw new Error(
"Hook definitions with two arguments must have a function as the second argument."
);
}
return {
tags: args[0].tags,
implementation: args[1]
};
}
if (typeof args[0] !== "function") {
throw new Error(
"Hook definitions with one argument must have a function as the first argument."
);
}
return {
implementation: args[0]
};
}

/* eslint-disable no-param-reassign, no-multi-assign */
function populateGlobalMethods(
{ stepDefinitionRegistry, beforeHookRegistry, afterHookRegistry },
instance = window
) {
const defineStep = (expression, implementation) => {
stepDefinitionRegistry.runtime(expression, implementation);
};

instance.defineStep = defineStep;
instance.Given = instance.given = defineStep;
instance.When = instance.when = defineStep;
instance.Then = instance.then = defineStep;
instance.And = instance.and = defineStep;
instance.But = instance.but = defineStep;

instance.defineParameterType = defineParameterType(stepDefinitionRegistry);

instance.Before = (...args) => {
const { tags, implementation } = parseHookArgs(args);
beforeHookRegistry.runtime(tags, implementation);
};

instance.After = (...args) => {
const { tags, implementation } = parseHookArgs(args);
afterHookRegistry.runtime(tags, implementation);
};
}
/* eslint-enable no-param-reassign, no-multi-assign */

module.exports = populateGlobalMethods;
File renamed without changes.
Loading

0 comments on commit bf9ef17

Please sign in to comment.