Description
File locations
Path | Use | Default import must return |
---|---|---|
/tests/yadda/steps.js |
Step library to be used for feature files | A hash of opinionated steps |
/tests/yadda/steps/<my-steps>.js |
Custom step implementations | A hash of opinionated steps, to be composed into main steps.js |
/tests/yadda/dictionary.js |
Converters aka macros | An hash of string | Regexp | [string | RegExp, (string[]) => Promise<any> ] |
/tests/yadda/labels.js |
Custom labels for third-party selectors | An hash of string , e. g. {'Bootstrap-Primary-Button', '.btn-primary') |
/tests/yadda/annotations.js |
Annotations to setup features and scenarios | TBD |
Populating the dictionary
Yadda has a weird API for defining the dictionary:
new Yadda.Dictionary()
.define('address', '$street, $postcode')
.define('street', /(\d+) (\w+))
.define('postcode', /((GIR &0AA)|((([A-PR-UWYZ][A-HK-Y]?[0-9][0-9]?)|(([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRV-Y]))) &[0-9][ABD-HJLNP-UW-Z]{2}))/)
.define('num', /(\d+)/, Yadda.converters.integer)
.define('quantity', /(\d+) (\w+)/, (amount, units, cb) => {
cb(null, { amount: amount, units: units })
});
.define('user', '$user', (userName, cb) => {
fetch(`https://api.example.com/users/${userName}`).then(
(response) => cb(null, response.json()),
(e) => cb(e)
);
});
This Node-style callback is inconvenient and hard to type. Basically, you pass a callback that accepts a callback.
ember-yadda
uses a simplified way of defining the dictionary:
{
address: '$street, $postcode',
street: /(\d+) (\w+),
postcode: /((GIR &0AA)|((([A-PR-UWYZ][A-HK-Y]?[0-9][0-9]?)|(([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRV-Y]))) &[0-9][ABD-HJLNP-UW-Z]{2}))/,
num: Yadda.converters.integer,
quantity: [
/(\d+) (\w+)/,
(amount, units) => ({ amount: amount, units: units })
],
user: [
'$user',
async (userName) => (await fetch(`https://api.example.com/users/${userName}`)).json()
]
}
Under the hood, ember-yadda
will convert these straightforward callbacks into Node-style supported by Yadda like this:
import opinionatedDictionary from '<my-app>/tests/yadda/dictionary';
function instantiateYaddaDictionary(opinionatedDictionary) {
const yaddaDictionary = new Yadda.Dictionary();
Object.entries(opinionatedDictionary).forEach(([macroName, macroDefinition]) => {
if (Array.isArray(macroDefinition)) {
const [pattern, converter] = macroDefinition;
yaddaDictionary.define(macroName, pattern, wrapConverterWithNodeStyle(converter));
} else {
yaddaDictionary.define(macroName, macroDefinition);
}
});
return yaddaDictionary;
}
function wrapConverterWithNodeStyle(converter) {
return (...args) => {
const cb = args.pop();
try {
const result = await converter(...args);
cb(null, result);
} catch (e) {
cb(e);
}
}
}
export default instantiateYaddaDictionary(opinionatedDictionary);
Now it is very convenient to define converter macros, but existing converters from Yadda.converters
are Node-style. I see three ways to resolve this:
-
When defining a macro using a
Yadda.converters
converter, use an util on it so that it is wrapped with async/await that the addon supports.E. g. instead of:
num: Yadda.converters.integer,
you would do:
num: converter(Yadda.converters.integer),
The downside of this approach is that a Node-style converter will be wrapped with async/await by the util and then wrapped again with Node-style by the addon.
-
Have
wrapConverterWithNodeStyle
detect whether the converter is Node-style or async/await.One way to do this would be to use introspection:
import introspect from 'introspect-fun'; // https://github.com/NicolasVargas/introspect-fun function maybeWrapConverterWithNodeStyle(converter) { return introspect(coverter).pop() === 'next' ? converter : wrapConverterWithNodeStyle(converter); }
The downside of this approach is that it's magical and unreliable, since it assumes that a node-style callback has its last argument called
next
. It will fail both when aYadda.converters
converter has the last argument called differently (currently not the case) and when an async/await converter has its last argument callednext
.There's also a performance implication, but it should be negligible.
Maybe there's another way to distinguish Node-style
Yadda.converters
converters from async/await converters? -
Simply port all six
Yadda.converters
converters, so that either theember-yadda
addon or a separate content-oriented addon offers async/await equivalents.Downside: more work to do. I was gonna write "more maintenance", but after ensuring test coverage the code will not need to be revisited.
We'll also need to maintain compatibility with
Yadda.converters
, but I doubt that they will stray far from what they currently are.
Supporting legacy ember-cli-yadda
In the legacy ember-cli-yadda
, a test file generated from a feature file imports a matching steps file like this.
We could make this import conditional. If the steps file exists, run it using the legacy setup. Otherwise, use the new setup described above.
Within the steps file, the user has complete freedom. They can:
- Keep using the new setup, but override some of the opinionated steps. This could be useful to resolve step name conflicts between opinionated step libraries.
- Use the legacy approach just like in
ember-cli-yadda
. - Use a mixed approach via tricks from
ember-cli-yadda-opinionated
. This lets you use legacyember-cli-yadda
steps and opinionated steps in the same feature.
Things not covered
Localization
Previously, localizaton setup was happening in the app.
Due to drastically simplified design of addon's in-app files, the addon will have yadda.localisation.default
hardcoded.
I believe internationalized steps are a bad practice, so we should not bother making it configurable.
In future, if there's a feature request for supporting non-English locales, we'll be able to think about a way of making it configurable.