An async / await Configuration Adventure

Ember's new integration testing relies heavily on async / await. Learn how to use it in your app, how to make developer ergonomics even better, and help us make the right choice for new projects by default.

An async / await Configuration Adventure

async / await is quite possibly the best feature to land in JavaScript in recent years (yes, I think its better than class 😱). Based on surprisingly good browser support matrix (last 2 versions of Chrome on desktop and mobile, Firefox, Safari on desktop and mobile, and Edge) and massively better developer ergonomics the new testing system (see my previous post) uses async / await by default.

First a quick side-track to look into just how much better things are with async / await, lets look at the following test using promises:

test('can fill out sauce rankings', function(assert) {
  return render(hbs`{{sauce-rankings}}`)
    .then(() => {
      return fillIn('.best', 'polynesian');
    })
    .then(() => {
      return fillIn('.second-best', 'honey');
    })
    .then(() => {
      return fillIn('.third-best', 'barbeque');
    })
    .then(() => {
      return fillIn('.worst', 'honey mustard');
    })
    .then(() => {
      return click('.submit');
    })
    .then(() => {
      assert.equal(this.element.textContent, 'results submitted!');
    })  
});

Compare that with the following which is the same test but using async / await:

test('can fill out sauce rankings', async function(assert) {
  await render(hbs`{{sauce-rankings}}`);
  
  await fillIn('.best', 'polynesian');
  await fillIn('.second-best', 'honey');
  await fillIn('.third-best', 'barbeque');
  await fillIn('.worst', 'honey mustard');
               
  await click('.submit');
  
  assert.equal(this.element.textContent, 'results submitted!');
});

This is awesome (I know), but it poses a somewhat tricky question: if the default Ember application's config/targets.js (see prior article on targets) file includes ie 9 by default (since this is the oldest browser supported by Ember 2.x), how do we actually use async / await in our test suites? The answer (as with most things in development πŸ˜‰) is: "it depends πŸ’ƒπŸ•Ί".

The goal of this little adventure is to explore the various possibilities during the beta testing phase of the new testing API so that we can settle in on the best defaults for new Ember apps and addons.

Lets begin the choose your own adventure portion of the post 😎.


Does your application support browsers other than the following (note: this includes whatever browser you use for testing)?

  • last 2 chrome (desktop and mobile)
  • last 2 safari (desktop and mobile)
  • last 2 edge
  • last 2 firefox

If you said "yes, my application supports older browsers like IE 11" proceed to section 38.

If you said "nope, my app is in that sweet sweet 'evergreen browser' only promised land" proceed to section 93.

93. Supports only browsers with native async / await

Congratulations, this scenario is the easiest πŸŽ‚. The only change needed to make things "just work" is to update your config/targets.js to match your actual browser support matrix.

For example, you might update your config/targets.js to the following contents:

module.exports = {
  browsers: [
    'last 2 Chrome versions',
    'last 2 Firefox versions',
    'last 2 Safari versions',
    'last 2 Edge versions',
  ],
};

Thats it, you are done! Wasn't that easy? Go to the end. 😺

38. Supports browsers without native async / await

Now here is where things start getting fun 😈. In order to use async / await in "legacy" (😎) browsers we have to ensure that async / await is transpiled to something that they can understand. In general, this means that async / await usages will be modified into ES5 compatible JavaScript by using a helper utility called regenerator which has special runtime code that must be loaded before executing the transpiled async/await code. This runtime code is not terribly large (only about 2.5kb after gzip) and is already a dependency of some very popular addons like ember-concurrency or ember-power-select so you may have it already.

Its time for the next branch in our adventure, I await your choice below (πŸ˜‚)...

Do you use ember-concurrency or ember-power-select already?

If you said "yes, of course I use the best addons already!", then proceed to section 30.

If you said "no, I'm not using these addons just yet but I'll check that out after I get my fancy new tests working" then please continue below.

Would you like to use async / await in your application code, or only in your test suite?

If you said "duh, I want to use the best feature in JavaScript all over my app", then proceed to section 30.

If you said "hmm, lets stick with just using it for testing for now", then continue to section 25.

30. Install regenerator for general use

Awesome, I'm glad you see the power of async / await and are looking forward to using it (and possibly some generator functions) even though you are stuck supporting older browsers. πŸ‘πŸ‘

The setup for your project in this case is also very easy:

npm install --save-dev ember-maybe-import-regenerator

I know what you are thinking: "this is a weird name for an addon". I agree! However, the name perfectly suits the addons purpose. Specifically, it adds the regenerator runtime to your projects vendor.js only if needed (and only one copy of it regardless of how many times it has been included).

With the setup described here, you can now use async / await in your test suite as well as your application. Congratulations!! πŸ™ŒπŸ‘

As you tool around in your newly found async freedom, you will likely notice that debugging through the regenerator runtime code is somewhat annoying and hard to reason about.

Would you like to do local development and testing with native async/await and only use regenerator in CI?

If you said, "of course, what took you so long!?!?!?!?" then follow along at section 10.

If you said, "nah, I like it when my head hurts πŸ€•" then good luck to you! Check out the end.

25. Install regenerator for test usage only

This begins one of our trickier setups. The main thing we want to do is ensure that:

  1. We can use async / await in our tests (e.g. tests/**/*.js files).
  2. We do not accidentally start using async / await (or generators) in our application code.

The second point is probably not quite as obvious, but is absolutely just as important as the first. If we do not do something to address this point it is very likely that you will accidentally ship some async / await usage to production. Since the test environment will have regenerator present, your tests will all continue to pass even though things are very broken in production.

OK, enough yammering, lets get this working!

For this setup, you will need to install two new packages:

npm install --save-dev ember-maybe-import-regenerator-for-testing
npm install --save-dev eslint-plugin-disable-features

In case you were wondering, the first package (ember-maybe-import-regenerator-for-testing) adds the regenerator runtime to your projects assets/test-support.js file (only when required). The second package is an eslint plugin that will be used to ensure you don't accidentally start using async / await in your application code.


Now, on to the linting configuration! 🎳

I've recently been really loving the new way to configure overrides in ESLint (which was added starting with eslint@4.1.0) so that is how I'll describe the changes needed. Please feel free to translate this to the older (IMHO harder to reason about) cascading configuration system if you prefer that.

A newly generated ember-cli@2.16 apps .eslintrc.js looks like:

module.exports = {
  root: true,
  parserOptions: {
    ecmaVersion: 2017,
    sourceType: 'module'
  },
  extends: 'eslint:recommended',
  env: {
    browser: true
  },
  rules: {
  }
};

To prevent the app/ directory from containing usage of async / await and generator functions you will need to update your projects .eslintrc.js file to look like:

module.exports = {
  root: true,
  parserOptions: {
    ecmaVersion: 2017,
    sourceType: 'module'
  },
  extends: 'eslint:recommended',
  env: {
    browser: true
  },
  rules: {
  },
  overrides: [
    {
      files: ['app/**/*.js'],
      plugins: [
        'disable-features',
      ],
      rules: {
        'disable-features/disable-async-await': 'error',
        'disable-features/disable-generator-functions': 'error',
      }
    }
  ],
};

With the setup described here, you can now use async / await in your test suite, and be certain not to break your application. Congratulations!! πŸ™ŒπŸ‘

As you tool around in your newly found async freedom, you will likely notice that debugging through the regenerator runtime code is somewhat annoying and hard to reason about.

Would you like to do local development and testing with native async/await and only use regenerator in CI?

If you said, "hell yes, what took you so long!?!?!?!?" then follow along to section 10.

If you said, "nah, I like it when my head hurts πŸ€•" then good luck to you! Check out the end.

10. Limit targets during local development/testing

It is really nice to be able to debug code that is much closer to what you actually wrote. Stepping over native async / await usage is a truly amazing experience. Even if your application ultimately supports older browsers, you can still have a nicer developer experience by limiting your applications targets during development and testing while still having production do a full transpilation to ES5.

All it takes is some small edits to config/targets.js. The default config.targets.js of an ember-cli@2.16 app looks like:

module.exports = {
  browsers: [
    'ie 9',
    'last 1 Chrome versions',
    'last 1 Firefox versions',
    'last 1 Safari versions'
  ]
};

Here is the setup I would recommend (assuming your CI system sets process.env.CI like Travis, CircleCI, CodeShip, etc do):

/* eslint-env node */
const browsers = [
  'last 1 Chrome versions',
  'last 1 Firefox versions',
  'last 1 Safari versions'
];

const isCI = !!process.env.CI;
const isProduction = process.env.EMBER_ENV === 'production';

if (isCI || isProduction) {
  browsers.push('ie 9');
}

module.exports = {
  browsers
};

Now, when running ember serve or ember test --server locally, you will see class, template strings, async / await, and everything else that modern browsers support out of the box instead of having to debug the "transpiled to ES5" mess.

πŸ”š

Hopefully you enjoyed my little choose your adventure game (feel free to let me know what you thought @rwjblue) and were able to set your application up for some exciting new async / await testing goodness.

The ember-cli blueprints will almost certainly be updated in 2.17 or 2.18 to support async / await in tests out of the box (I'm personally leaning towards the method in section 30 along with the local development ergonomics of section 10), what do you think the defaults should be? Why?