Interactor.js

Custom Interactors

Composition

Interactors were built to be composable. Using the class decorator, you can create your own custom interactors by composing other interactors, actions, and special property creators. The class decorator maps these properties into getters and methods that are wrapped to return immutable instances of the parent class. All custom interactors inherit from the base Interactor class.

    Built-in property creators can be found here, and built-in actions can be found here.

    It's highly recommended that you build custom component interactors for unit tests, and then use those component interactors to build page interactors for acceptance tests.

    Custom assertions

    Custom properties created using the built-in property creators automatically create custom assertions as well.

    In the example above, we defined a few custom interactor properties for the field interactor. Then in our tests, we asserted against those properties both by referencing the property, and by using the auto-defined assertion.

    import interactor, {
      matches,
      text
    } from 'interactor.js';
    
    @interactor class FieldInteractor {
      label = text('.label');
      hasError = matches('.has-error');
      errorMessage = text('.error-message');
    }
    
    // ...
    
    // use any assertion library
    expect(field.label).toBe('Password');
    expect(field.hasError).toBe(true);
    expect(field.errorMessage).toBe('Incorrect password');
    
    // or use interactor's async assertions
    await field
      .assert.label('Password')
      .assert.hasError()
      .assert.errorMessage('Incorrect password');

    Nested assertions work the same way as nested actions and return the top-most parent interactor. Like other assertions, they are also grouped with neighboring assertions until an action is called. Assertions can only be started at the top level interactor; nested interactors do not contain an assert property.

    import interactor, {
      collection
      property,
      scoped
    } from 'interactor.js';
    
    @interactor class FormInteractor {
      name = scoped('input');
    
      options = collection('[type=radio]', {
        checked: property('checked')
      });
    
      submit = scoped('submit');
    }
    
    // ...
    
    new FormInteractor()
      // the following assertions are grouped together
      .assert.name.value('Name Namerson')
      .assert.options(2).checked()
      .assert.submit.not.disabled()
      // assertion after actions are grouped separately
      .submit.click()
      .assert.submit.matches('.loading')
      .assert.submit.disabled()

    All built-in assertions, and assertions auto-defined from custom properties, can be passed a custom matcher function which is given the result of the property as it's only argument. Assertions that test properties which return strings can also be passed a regular expression to test against. Providing no arguments to an assertion will assert the property's truthiness.

    // asserting with a custom matcher
    await new FormInteractor()
      .assert.options().count(len => len > 1 && len <= 3)
    
    // asserting against a regexp
    await new FormInteractor()
      .assert.name.value(/namerson/i);
    
    // asserting truthiness
    await new FormInteractor()
      .assert.submit.disabled();

    Advanced assertions

    Assertions can also be added to custom interactors using the static assertions property. An assertion defined this way can be a function that throws an error or returns a boolean value. To control the error message, return an object consisting of the result of the validation and a message function called when an assertion fails. This is especially recommended when negating custom assertions, otherwise a generic error message will be thrown.

      Extending custom interactors

      All interactors extend from the base Interactor class, and so inherit methods, properties, and assertions from that class. To extend from custom interactors, just use the extends keyword in conjunction with the @interactor decorator.

      import interactor from 'interactor.js';
      
      @interactor class FieldInteractor { /* ... */ }
      @interactor class PasswordInteractor extends FieldInteractor { /* ... */ }
      
      expect(new PasswordInteractor()).toBeInstanceOf(PasswordInteractor);
      expect(new PasswordInteractor()).toBeInstanceOf(FieldInteractor);
      expect(new PasswordInteractor()).toBeInstanceOf(Interactor);

      Without class properties or decorators

      Some environments may not be able to transpile class properties or decorators, or maybe you just have an aversion to classes. Interactor.js provides a static from method to create custom interactors from plain old JavaScript objects. All custom interactors also have a static from method.

      import { Interactor, type, click } from 'interactor.js';
      
      const LoginInteractor = Interactor.from({
        // static properties are handled via the `static` keyword
        static: {
          // the class name is used in error messages when a scope
          // cannot be inferred; since pojos do not have names, a
          // static name property is recommended
          name: 'LoginInteractor',
          // the defaultScope is used when invoked without a scope
          defaultScope: '.login-form',
          // custom assertions are also defined by a static property
          assertions: { /* ... */ }
        },
      
        typeEmail: email => type('.email', email),
        typePassword: pass => type('.password', pass),
        submit: click('.submit')
      });

      New interactor instances can also be created using a static scoped method, or by passing an custom interactor class to the scoped helper.

      import { scoped } from 'interactor.js';
      
      // new keyword
      let loginForm = new LoginInteractor('.login-form');
      // static scoped method
      let loginForm = LoginInteractor.scoped('.login-form');
      // using the interactor creator
      let loginForm = scoped('.login-form', LoginInteractor);

      In both examples, the selector is optional since LoginInteractor declares a static defaultScope property.