@bigtest/interactor

new Interactor (scope)

import Interactor from '@bigtest/interactor';

In biology, an interactor is defined as part of an organism that natural selection acts upon. A @bigtest/interactor interactor defines part of an app that tests act upon.

let input = new Interactor('input');

await input
  .focus()
  .fill('some value')
  .blur();

expect(input.value).to.equal('some value');

Interactors are Convergences. They directly extend the Convergence class and as such are immutable, reusable, and composable.

let input = new Interactor('input');
let submit = new Interactor('button[type="submit"]');

let fillAndSubmit = value => {
  return input.fill(value)
    .append(submit.click());
}

await fillAndSubmit('some value');

Interactors don’t have to be narrowly scoped either. The various interaction methods support passing a selector as the first argument.

new Interactor('#some-form')
  .fill('input[type="email"]', 'email@domain.tld')
  .click('button[type="submit"]');

You can create custom interactors by extending the class…

class FormInteractor extends Interactor {
  fillEmail(email) {
    // return an instance of this interactor to allow chaining
    return this.fill('input[type="email"]', email);
  }

  submit() {
    return this.click('button[type="submit"]');
  }

  fillAndSubmit(email) {
    return this
      .fillEmail(email)
      .submit();
  }
}

… or use the interactor class decorator in conjuction with the various interaction helpers.

import { interactor, fillable, clickable } from '@bigtest/interactor';

@interactor class FormInteractor {
  fillEmail = fillable('input[type="email"]');
  submit = clickable('button[type="submit"]');

  fillAndSubmit(email) {
    return this
      .fillEmail(email)
      .submit();
  }
}

Interactors also have a static from method to create custom interactors. You can even extend from custom interactors.

const FormInteractor = Interactor.from({
  fillEmail: fillable('input[type="email"]'),
  submit: clickable('button[type="submit"]'),

  fillAndSubmit(email) {
    return this
      .fillEmail(email)
      .submit();
  }
});

Custom interactors also have a static extend decorator available that you can use to extend from custom interactors while still using the class syntax.

@FieldInteractor.extend
class PasswordInteractor {
  // ...
}

Properties

Methods

defaultScope String|Element

The default selector or element an interactor is scoped to when a scope is not provided during initialization.

new Interactor().$root //=> document.body

When extending the Interactor class, this static property may be overridden to define a new default scope.

class CustomInteractor extends Interactor {
  static defaultScope = '#some-element';
}

new CustomInteractor().$root //=> <div id="some-element">...</div>

isHidden Boolean

  • throws Error When the interactor scope cannot be found

Returns true when the interactor scope is visually hidden, otherwise returns false. When the interactor scope cannot be found, an error will be thrown.

<div id="foo">
  ...
</div>

<div id="bar" style="display: none">
  ...
</div>
new Interactor('#foo').isHidden //=> false
new Interactor('#bar').isHidden //=> true

The element is considered NOT visible for HTML <area> elements, SVG elements that do not render anything themselves, display: none elements, and generally any elements that are not rendered.

isPresent Boolean

Returns true when the interactor scope exists in the DOM, otherwise returns false.

<div id="foo">
  ...
</div>
new Interactor('#foo').isPresent //=> true
new Interactor('#bar').isPresent //=> false

isVisible Boolean

  • throws Error When the interactor scope cannot be found

Returns true when the interactor scope is NOT visually hidden, otherwise returns false. When the interactor scope cannot be found, an error will be thrown.

<div id="foo">
  ...
</div>

<div id="bar" style="display: none">
  ...
</div>
new Interactor('#foo').isVisible //=> true
new Interactor('#bar').isVisible //=> false

The element is considered NOT visible for HTML <area> elements, SVG elements that do not render anything themselves, display: none elements, and generally any elements that are not rendered.

text Boolean

  • throws Error When the interactor scope cannot be found

Returns the trimmed textContent property of an element.

<p>
  Hello World!
</p>
new Interactor('p').text //=> "Hello World!"

value Boolean

  • throws Error When the interactor scope cannot be found

Returns the value of an input element.

<input value="Hello World!" />
new Interactor('input').value //=> "Hello World!"

extend classDescriptor Class

  • classDescriptor Object|Class A class descriptor or constructor
  • returns Class Custom interactor class

Similar to the @interactor decorator; creates a custom interactor class from methods and properties of another class. However, this static method is available on all interactor classes, which makes any interactor extendable.

import Interactor, {
  text,
  property,
  value,
  clickable
} from '@bigtest/interactor';

@Interactor.extend
class FieldInteractor {
  label = text('label');
  name = property('input', 'name');
  type = property('input', 'type');
  placeholder = property('input', 'placeholder');
  value = value('input');
}

@FieldInteractor.extend
class PasswordInteractor {
  toggleVisibility = clickable('.visibility-toggle');
}

from properties Class

  • properties Object Used to create a custom interactor
  • returns Class Custom interactor class

Creates a custom interactor class from methods and properties of an object. Methods and getters are added to the custom class’s prototype and all other properties are defined during instance initialization to support custom property creators.

import Interactor, {
  text,
  property,
  value,
  clickable
} from '@bigtest/interactor';

const FieldInteractor = Interactor.from({
  label: text('label'),
  name: property('input', 'name'),
  type: property('input', 'type'),
  placeholder: property('input', 'placeholder'),
  value: value('input')
});

const PasswordInteractor = FieldInteractor.from({
  toggleVisibility: clickable('.visibility-toggle')
});

isInteractor obj Boolean

  • obj Object A possible interactor object
  • returns Boolean

Returns true if the object has common interactor properties

let result = maybeInteractor()

if (isInteractor(result)) {
  await result.login(user)
} else {
  something(result)
}

$ selector Element

  • selector String Selector string
  • returns Element Element found via querySelector
  • throws Error When the element or scope cannot be found

A querySelector-like method that is scoped to the current interactor. Unlike querySelector, this method will throw an error when the element cannot be found.

let page = new Interactor('#page-scope');

// returns an element matching `#page-scope .some-element`, and
// throws an error if it cannot be found
page.$('.some-element');

$$ selector Array

  • selector String Selector string
  • returns Array Array of elements found via querySelectorAll
  • throws Error When the interactor scope cannot be found

A querySelectorAll-like method that is scoped to the current interactor and returns an array instead of a nodelist. If selector cannot be found, an empty array is returned. If the current scope cannot be found, an error is thrown.

let list = new Interactor('ul.some-list');

// returns an array of elements matching `ul.some-list li`; only
// throws an error when `ul.some-list` cannot be found
page.$$('li');

blur selector Interactor

  • selector String Nested element query selector
  • returns Interactor A new instance with additional convergences

Converges on an element first existing in the DOM, then triggers a blur event on that element.

<form ...>
  <input type="email" />
  ...
</form>
await new Interactor('input').blur();
await new Interactor('form').blur('input[type="email"]');

click selector Interactor

  • selector String Nested element query selector
  • returns Interactor A new instance with additional convergences

Converges on an element first existing in the DOM, then triggers a click on that element.

<form ...>
  <button type="submit">
    ...
  </button>
  ...
</form>
await new Interactor('button').click();
await new Interactor('form').click('[type="submit"]');

fill selector, value Interactor

  • selector String Nested element query selector
  • value String Value to set
  • returns Interactor A new instance with additional convergences

Converges on an element first existing in the DOM, then sets its value property to the passed value, and triggers both input and change events for the element.

<form ...>
  <input id="name" />
  ...
</form>
await new Interactor('input').fill('value');
await new Interactor('form').fill('input#name', 'value');

find selector Interactor

  • selector String Element query selector
  • returns Interactor A new instance with additional convergences

Converges on an element existing in the DOM.

let $el = await new Interactor().find('.some-element');

findAll selector Interactor

  • selector String Element query selector
  • returns Interactor A new instance with additional convergences

Converges on the scope existing in DOM, then returns an instance of this interactor which will converge with an array of elements matching the provided selector.

let $listItems = await new Interactor('ul').findAll('li');

focus selector Interactor

  • selector String Nested element query selector
  • returns Interactor A new instance with additional convergences

Converges on an element first existing in the DOM, then triggers a focus event on that element.

<form ...>
  <input type="email" />
  ...
</form>
await new Interactor('input').focus();
await new Interactor('form').focus('input[type="email"]');

pause Interactor

  • returns Interactor An instance of this interactor which will halt when it encounters this method in the convergence stack

Pauses an interactor by halting the convergence while it is running with an unresolving promise.

This is a hack which causes the event loop to hang and in some situations become unresponsive. Consider moving any teardown code to execute before setup. This way, when a test is finished, the DOM and state is preserved for interacting with and inspecting.

scoped selector, properties Interactor

  • selector String Nested element query selector
  • properties Object Interaction descriptors
  • returns Interactor A new nested interactor instance

Returns a nested interactor scoped to the selector within the current interactor’s scope.

<form ...>
  <button type="submit">
    ...
  </button>
  ...
</form>
await new Interactor('form').scoped('[type="submit"]').click();

This is especially useful for returning nested interactors from custom methods.

@interactor class RadioGroupInteractor {
  radio(value) {
    return this.scoped(`[type="radio"][value="${value}"]`, {
      isDisabled: property('disabled')
    });
  }
}
radioGroup.radio('option-1').isDisabled //=> Boolean
radioGroup.radio('option-1').click() //=> RadioGroupInteractor

scroll selector, scrollTo Interactor

  • selector String Nested element query selector
  • scrollTo.top Number Number of pixels to scroll the top-offset
  • scrollTo.left Number Number of pixels to scroll the left-offset
  • returns Interactor A new instance with additional convergences

Converges on an element first existing in the DOM, then sets the scrollTop and/or scrollLeft properties of the element, and then finally triggers a scroll event on the element.

await new Interactor('#page').scroll({ top: 100 });
await new Interactor('#page').scroll('.nested-view', { left: 100 });

select selector, options Interactor

  • selector String Nested element query selector
  • options String|Array. Option or array of options text to select
  • returns Interactor A new instance with additional convergences

Converges on an element first existing in the DOM, then selects a matching option based on the text content, and triggers change and input events for the select element.

<form ...>
  <select id="month">
    <option value="1">January</option>
    <option value="2">February</option>
    <option value="3">March</option>
    ...
  </select>
  ...
</form>
await new Interactor('select').select('February');
await new Interactor('form').fill('select#month', 'March');

For multiple selects you can pass an array of options you would like to select.

<form ...>
  <select id="month" multiple>
    <option value="1">January</option>
    <option value="2">February</option>
    <option value="3">March</option>
    ...
  </select>
  ...
</form>
await new Interactor('select').select(['February', 'March']);
await new Interactor('form').select('select#month', ['February', 'March']);

trigger selector, eventName, options Interactor

  • selector String Nested element query selector
  • eventName String Event name or options object
  • options Object Event init options
  • returns Interactor A new instance with additional convergences

Converges on an element first existing in the DOM, then triggers a specified event with optional event init options.

await new Interactor('#foo').trigger('customEvent');
await new Interactor('#foo').trigger('customEvent', { ... });
await new Interactor('#foo').trigger('#bar', 'customEvent');
await new Interactor('#foo').trigger('#bar', 'customEvent', { ... });

action method Object

  • method Function Function body for the interaction method
  • returns Object page-object property descriptor

Creates a property descriptor for interaction methods.

function check(selector) {
  return action(function(name) {
    return this.click(`${selector}[name="${name}"]`);
  })
}
@interactor class CheckboxGroupInteractor {
  check = check('input[type="checkbox"]');
}
new CheckboxGroupinteractor('.checkboxes').check('option-1');

attribute selector, attr Object

  • selector String Nested element query selector
  • attr String Attribute name
  • returns Object Property descriptor

Property creator for returning an attribute of an element.

<div class="card" id="foo">
  ...
  <a class="card-link" href="https://example.com">
    ...
  </a>
</div>
@interactor class CardInteractor {
  id = attribute('id');
  url = attribute('.card-link', 'href');
}
new CardInteractor('.card').id //=> "foo"
new CardInteractor('.card').url //=> "https://example.com"

blurrable selector Object

  • selector String Element query selector
  • returns Object Property descriptor

Interaction creator for blurring a specific element within a custom interactor class.

<form ...>
  <input type="email" />
  ...
</form>
@interactor class FormInteractor {
  blurEmail = blurrable('input[type="email"]');
}
await new FormInteractor('form').blurEmail();

clickable selector Object

  • selector String Element query selector
  • returns Object Property descriptor

Interaction creator for clicking a specific element within a custom interactor class.

<div class="card">
  ...
  <a class="card-link" href="https://example.com">
    ...
  </a>
</div>
@interactor class CardInteractor {
  clickThrough = clickable('.card-link');
}
await new CardInteractor('.card').clickThrough()

collection selector, properties Object

  • selector String|Function Element query selector or function that returns a selector
  • properties Object Interaction descriptors
  • returns Object Property descriptor

Interaction creator for a collection of nested interactors. A collection interaction takes an index as it’s argument and returns a nested interactor scoped to that element.

<ul class="checkboxes">
  <li><input type="checkbox" .../></li>
  <li><input type="checkbox" .../></li>
  <li><input type="checkbox" .../></li>
</ul>
@interactor class CheckboxGroupInteractor {
  items = collection('input[type="checkbox"]');
}

Nested interactions return instances of the topmost interactor so that the initial chain is never broken.

await checkboxGroup
  .items(0).click()
  .items(1).click();

Nested interactors also have an additional method, #only(), which disables the default nested chaining behavior, but retains any previous interactions.

await checkboxGroup
  .items(0).click()
  .items(1).only()
    .focus()
    .trigger('keydown', { which: 32 })

When calling a collection method without an index, an array of un-nested interactors are returned, each corresponding to an element in the DOM at the time the method was invoked.

checkboxGroup.items().length // => 3

// checks all checkboxes
await checkboxGroup.do(function() {
  return this.items().reduce((group, item) => {
    return group.append(item.click());
  }, this);
})

With the second argument, you can define additional interactions using the various interaction helpers.

<ul class="cards">
  <li class="card">
    ...
    <a class="card-link" ...>
      ...
    </a>
  </li>
</ul>
@interactor class CardsListInteractor {
  cards = collection('.card', {
    clickThrough: clickable('.card-link')
  });
}

You can also use another interactor class.

@interactor class CardInteractor {
  clickThrough = clickable('.card-link');
}

@interactor class CardsListInteractor {
  cards = collection('.card', CardInteractor);
}
await new CardListinteractor('.cards')
  .cards(0).clickThrough();

The collection interaction creator also accepts a function instead of a selector. This function is invoked with any arguments given to the resulting collection method and must return a new selector string. When no arguments are provided, the selector should match multiple elements within the current scope; when arguments are given, the selector should match only one element within the current scope.

@interactor class CheckboxGroupInteractor {
  items = collection(value => {
    return `[type="radio"]${value ? '[value="${value}"]' : ''}`;
  })
}
await checkBoxGroup
  .items('green').click()
  .items('red').click();

computed getter Object

  • getter Function Property getter
  • returns Object Property descriptor

Creates a property descriptor for interaction property getters.

function data(key, selector) {
  return computed(function() {
    return this.$(selector).dataset[key];
  })
}
@interactor class PageInteractor {
  username = data('user', '#user-info');
}

count selector Object

  • selector String Element query selector
  • returns Object Property descriptor
  • throws Error When the interactor scope cannot be found

Property creator for returning the number of elements found via a query selector. Will throw an error if the interactor scope cannot be found.

<ul >
  <li>...</li>
  <li>...</li>
  <li>...</li>
</ul>
@interactor class ListInteractor {
  size = count('li');
}
new ListInteractor('ul').size //=> 3

fillable selector Object

  • selector String Element query selector
  • returns Object Property descriptor

Interaction creator for setting the value of a specific element within a custom interactor class.

<form ...>
  <input id="name" />
  ...
</form>
@interactor class FormInteractor {
  fillName = fillable('input#name');
}
await new FormInteractor('form').fillName('value');

find selector Object

  • selector String Element query selector
  • returns Object Property descriptor

Interaction creator for finding a specific element within a custom interactor class.

@interactor class PageInteractor {
  heading = find('h1.heading');
}
let $heading = new PageInteractor().heading;

findAll selector Object

  • selector String Element query selector
  • returns Object Property descriptor

Interaction creator for finding a specific set of elements within a custom interactor class.

@interactor class ListInteractor {
  items = findAll('li');
}
let $listItems = new ListInteractor('ul').items;

focusable selector Object

  • selector String Element query selector
  • returns Object Property descriptor

Interaction creator for focusing a specific element within a custom interactor class.

<form ...>
  <input type="email" />
  ...
</form>
@interactor class FormInteractor {
  focusEmail = focusable('input[type="email"]');
}
await new FormInteractor('form').focusEmail();

hasClass selector, className Object

  • selector String Nested element query selector
  • className String Classname to check for
  • returns Object Property descriptor

Property creator for returning true or false when an element has a specific class.

<form class="error" ...>
  <input id="name" class="error" />
  <input type="email" id="email" />
</form>
@interactor class FormInteractor {
  hasErrors = hasClass('error'); // applies to the root
  hasNameError = hasClass('input#name', 'error');
  hasEmailError = hasClass('input#email', 'error');
}
new FormInteractor('form').hasErrors //=> true
new FormInteractor('form').hasNameError //=> true
new FormInteractor('form').hasEmailError //=> false

interactor classDescriptor Class

  • classDescriptor Object|Class A class descriptor or constructor
  • returns Class Custom interactor class
import { interactor } from '@bigtest/interactor';

Creates a custom interactor class from methods and properties of another class. Instance initializers that define property descriptors will have their descriptors added to the custom class’s prototype.

import {
  interactor,
  isPresent,
  clickable
} from '@bigtest/interactor';

@interactor class CustomInteractor {
  // optional default scope for this interactor
  static defaultScope = '#some-element';

  // `isPresent` returns a getter descriptor
  hasError = isPresent('div.error');

  // `*able` helpers return method descriptors
  submit = clickable('button[type="submit"]');

  // normal getters and methods work as well
  fillForm(name, email) {
    return this
      .fill('input#name', name)
      .fill('input#email', email)
      .submit();
  }
}

is selector, match Object

  • selector String Nested element query selector
  • match String Matching query selector
  • returns Object Property descriptor

Property creator for returning true or false within a custom interactor class depending on if the element matches the provided query selector.

<ul class="list">
  <li id="foo">...</li>
  <li id="bar">...</li>
  <li id="baz">...</li>
</ul>
@interactor class ListInteractor {
  isList = is('.list');
  isFooFirst = is('#foo', ':first-child');
  isBarLast = is('#bar', ':last-child');
}
new ListInteractor('ul').isList //=> true
new ListInteractor('ul').isFooFirst //=> true
new ListInteractor('ul').isBarLast //=> false

isHidden selector Object

  • selector String Element query selector
  • returns Object Property descriptor

Property creator for returning true or false within a custom interactor class when a specific element is visually hidden or not.

<div id="foo">
  ...
</div>

<div id="bar" style="display: none">
  ...
</div>
@interactor class PageInteractor {
  isFooHidden = isHidden('#foo');
  isBarHidden = isHidden('#bar');
}
new PageInteractor().isFooHidden // => false
new PageInteractor().isBarHidden // => true

The element is considered NOT visible for HTML <area> elements, SVG elements that do not render anything themselves, display: none elements, and generally any elements that are not rendered.

isPresent selector Object

  • selector String Element query selector
  • returns Object Property descriptor

Interaction creator for returning true or false within a custom interactor class if the element exists in the DOM or not.

<div id="foo">
  ...
</div>
@interactor class PageInteractor {
  isFooPresent = isPresent('#foo');
  isBarPresent = isPresent('#bar');
}
new PageInteractor().isFooPresent // => true
new PageInteractor().isBarPresent // => false

isVisible selector Object

  • selector String Element query selector
  • returns Object Property descriptor

Property creator for returning true or false within a custom interactor class depending on if the element is visible in the DOM.

<div id="foo">
  ...
</div>

<div id="bar" style="display: none">
  ...
</div>
@interactor class PageInteractor {
  isFooVisible = isVisible('#foo');
  isBarVisible = isVisible('#bar');
}
new PageInteractor().isFooVisible // => true
new PageInteractor().isBarVisible // => false

The element is considered NOT visible for HTML <area> elements, SVG elements that do not render anything themselves, display: none elements, and generally any elements that are not rendered.

property selector, prop Object

  • selector String Nested element query selector
  • prop String Property name
  • returns Object Property descriptor

Property creator for returning a property of an element.

<div class="card" style="height: 100px">
  ...
  <button class="card-cta" disabled>
    ...
  </button>
</div>
@interactor class CardInteractor {
  height = property('offsetHeight');
  isDisabled = property('button.card-cta', 'disabled');
}
new CardInteractor('.card').height //=> 100
new CardInteractor('.card').isDisabled //=> true

scoped selector, descriptors Object

  • selector String Element query selector
  • descriptors Object Interaction descriptors
  • returns Object Property descriptor

Interaction creator for a single nested interactor.

<form class="login-form">
  <input type="text" name="username" />
  <input type="email" name="email" />
  <button type="submit">Login</button>
</form>
@interactor class LoginFormInteractor {
  username = scoped('input[name="username"]')
  email = scoped('input[name="email"]')
  submit = clickable('button[type="submit"]')
}

Nested interactions return instances of the topmost interactor so that the initial chain is never broken.

await loginForm
  .username.fill('darklord1926')
  .email.fill('tom.riddle@hogwarts.edu')
  .email.blur()
  .submit()

Nested interactors also have an additional method, #only(), which disables the default nested chaining behavior, but retains any previous interactions.

await loginForm
  .username.fill('h4x0r')
  .email.only()
    .fill('not@an@email')
    .blur()

With the second argument, you can define additional interactions using the various interaction helpers.

<label class="field username-field">
  <span class="field-label">Username:</span>
  <input type="text" name="username" />
</label>
@interactor class FormInteractor {
  username = scoped('.username-field', {
    label: text('.field-label'),
    fillIn: fillable('input')
  })
}

You can also use another interactor class.

@interactor class FieldInteractor {
  label = text('.field-label')

  fillIn(value) {
    return this.scoped('input')
     .focus().fill(value).blur()
  }
}

@interactor class LoginFormInteractor {
  username = scoped('.username-field', FieldInteractor)
  email = scoped('.email-field', FieldInteractor)
  submit = clickable('button[type="submit"]')
}
await loginForm
  .username.fillIn('darklord1926')
  .email.fillIn('tom.riddle@hogwarts.edu')
  .submit()

scrollable selector Object

  • selector String Element query selector
  • returns Object Property descriptor

Interaction creator for scrollilng a specific element within a custom interactor class.

@interactor class PageInteractor {
  scrollSection = scrollable('.scrollview')
}
await new PageInteractor('#page').scrollSection({ top: 100 })

selectable selector Object

  • selector String Element query selector
  • returns Object Property descriptor

Interaction creator for selecting an option of a specific select element within a custom interactor class.

<form ...>
  <select id="month">
    <option value="1">January</option>
    <option value="2">February</option>
    <option value="3">March</option>
    ...
  </select>
  ...
</form>
@interactor class FormInteractor {
  selectMonth = selectable('select#month');
}
await new FormInteractor('form').selectMonth('February');

For multiple selects you can pass an array of options you would like to select.

<form ...>
  <select id="month" multiple>
    <option value="1">January</option>
    <option value="2">February</option>
    <option value="3">March</option>
    ...
  </select>
  ...
</form>
@interactor class FormInteractor {
  selectMonth = selectable('select#month');
}
await new FormInteractor('form').selectMonth(['February', 'March']);

text selector Object

  • selector String Element query selector
  • returns Object Property descriptor

Property creator for returning the trimmed textContent property of an element within a custom interactor class.

<h1>
  Hello World!
</h1>
@interactor class PageInteractor {
  heading = text('h1');
}
new PageInteractor().heading //=> "Hello World!"

triggerable selector, eventName, options Object

  • selector String Element query selector
  • eventName String Event name or options object
  • options Object Event init options
  • returns Object Property descriptor

Interaction creator for triggering an event on a specific element within a custom interactor class.

@interactor class PageInteractor {
  triggerEvent = triggerable('customEvent', { ... });
  triggerFooEvent = triggerable('#foo', 'customEvent');
}
await new PageInteractor().triggerEvent();
await new PageInteractor().triggerEvent({ ... });
await new PageInteractor().triggerFooEvent();
await new PageInteractor().triggerFooEvent({ ... });

value selector Object

  • selector String Element query selector
  • returns Object Property descriptor

Property creator for returning the value of an input element within a custom interactor class.

<form>
  <input id="name" value="Foo Bar" />
</form>
@interactor class FormInteractor {
  name = value('input#name');
}
new FormInteractor('form').name //=> "Foo Bar"