Skip to content

[RFC] Initial design #1

Open
Open
@lolmaus

Description

@lolmaus

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:

  1. 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.

  2. 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 a Yadda.converters converter has the last argument called differently (currently not the case) and when an async/await converter has its last argument called next.

    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?

  3. Simply port all six Yadda.converters converters, so that either the ember-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:

  1. 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.
  2. Use the legacy approach just like in ember-cli-yadda.
  3. Use a mixed approach via tricks from ember-cli-yadda-opinionated. This lets you use legacy ember-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.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions