LevelbossMike / ember-statecharts

Statecharts for Ember.js applications
https://ember-statecharts.com/
MIT License
72 stars 13 forks source link

[Testing Helpers] Pull in helpers from qunit-xstate-test, providing better type definitions for ember projects #239

Open NullVoxPopuli opened 4 years ago

NullVoxPopuli commented 4 years ago

@sukima's project here: https://github.com/sukima/qunit-xstate-test is great in that it eliminates testing boilerplate from the most comment way of testing using statecharts.

However, it's framework-agnostic (which is great!), but that means that the type definitions don't quite match for what we need in an Ember context (things like assigning the correct 'this', etc).

I've made the following changes here to support Ember w/ TypeScript:

import { module, test } from 'qunit';
/* global QUnit */
import { TestModel } from '@xstate/test';
import { TestPlan } from '@xstate/test/lib/types';
import { TestContext as EmberTestContext } from 'ember-test-helpers';

type TestCallbackFn<TestContext, TContext, TReturn> = (
  this: EmberTestContext,
  assert: Assert,
  path: TestPlan<TestContext, TContext>['paths'][0]
) => TReturn;

export const testShortestPaths = <TestContext, TContext, TReturn>(
  testModel: TestModel<TestContext, TContext>,
  testCallback: TestCallbackFn<TestContext, TContext, TReturn>
) => {
  testModel.getShortestPathPlans().forEach((plan) => {
    module(plan.description, function () {
      plan.paths.forEach((path) => {
        test(path.description, function (assert) {
          return testCallback.bind(this)(assert, path);
        });
      });
    });
  });
};

Only 2 notable changes for testShortestPaths (originally here):

So what does this change get us? Auto-completion of all our helpers: image

For this to work well, what does the testModel look like?

interface TestMachineContext {
  assert: Assert;
  owner: ApplicationInstance;
}

const testModel = createModel<TestMachineContext, {}>(
  Machine({
    id: 'login-test',
    initial: 'begin',
    states: {
      begin: {
        on: {
          SCAN_LOGIN_VALID: 'scannedValidLoginQR',
          SCAN_LOGIN_INVALID: 'begin',
          SCAN_INVALID: 'begin',
        },
        meta: {
          async test({ assert }) {
            // assertions
          },
        },
      },
      scannedValidLoginQR: {},
    },
  })
).withEvents({
  SCAN_LOGIN_VALID: {
    async exec({ owner }) => {
      scanQR(owner, ['login', { pub: 'abcdef123', name: 'General Kenobi' }]);
    },
  },
  // ...
});

Note, I think that maybe the args to meta.test are invalid (the reason why type inference doesn't work here). This'd be upstream in XState though.

cc @davidkpiano re: meta.test types

davidkpiano commented 4 years ago

This is a planned addition (re: adding types for meta), and I might make it so that @xstate/test version 1.0 would look more like:

const testModel = createModel({
  // ... test machine definition goes directly in here
  states: {
    loading: {
      // test is a direct property of the state
      async test() {
        // assertion
      }
    }
  }
})