npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

buster-functional

v1.2.2

Published

Functional tests extension for BusterJS. Adds helper functions to simulate basic user interactions

Downloads

12

Readme

buster-functional Build Status Coverage Status

Functional tests extension for BusterJS. Adds helper functions to simulate basic user interactions.

Expects: jQuery to be present on the test target page.

Install

npm install buster-functional --save-dev

Add following snippet to your project's package.json to allow buster-server support proxying without session prefixes.

"scripts":
{
  "postinstall": "test ! -d ./node_modules/buster-functional || ./node_modules/buster-functional/bin/install_wrapper"
},

You might need to run npm run-script postinstall for the first time.

Examples

Config

Just include buster-functional as extension, list proxied paths as resources, see Proxy resources.

'Functional tests':
{
  environment: 'browser',
  tests:
  [
    'tests/functional/**/*.js'
  ],
  resources:
  [
    // provide proxy paths, "/" is occupied by buster itself
    // so provide some alternative, like "/index"
    {path: '/index', backend: 'http://localhost:3030/'},
    {path: '/js', backend: 'http://localhost:3030/js'},
    {path: '/css', backend: 'http://localhost:3030/css'},
    {path: '/api', backend: 'http://localhost:3030/api'},
  ],
  extensions:
  [
    require('buster-functional')
  ],
  'buster-functional':
  {
    timeout: 120 // seconds
  }
}

.load(uri, [callback])

Loads requested page into iframe.

Note: When page is loaded, references to it's window, document and $ are attached to the test's context.

setUp: function(done)
{
  // load new app's homepage for each test
  this.load('/index').waitForVar('$', done);
}

.unload([callback])

Cleans up iframe and proxy-cookie, leaving stage clean for the next test.

tearDown: function(done)
{
  this.unload(done);
}

.touch(selector|element, [callback])

Simulates browser events related to touch, in the right order and with proper pauses.

'Change search type to for-rent': function(done)
{
  // initially loads as For Sale search type
  assert.className(this.$('#searchTypeTabs>[data-tab=for_sale]')[0], 'backgroundLowlight');

  // change tab
  this.touch('#searchTypeTabs>[data-tab=for_rent]', function()
  {
    // for rent tab should be highlighted
    assert.className(this.$('#searchTypeTabs>[data-tab=for_rent]')[0], 'backgroundLowlight');
    // and for sale tab should not
    refute.className(this.$('#searchTypeTabs>[data-tab=for_sale]')[0], 'backgroundLowlight');

    done();
  });
}

.click(selector|element, [callback])

Simulates browser events related to click, in the right order and with proper pauses.

'Change search type to for-sale': function(done)
{
  // initially loads as For Rent search type
  assert.className(this.$('#searchTypeTabs>[data-tab=for_rent]')[0], 'backgroundLowlight');

  // change tab
  this.click('#searchTypeTabs>[data-tab=for_sale]', function()
  {
    // for rent tab should be highlighted
    assert.className(this.$('#searchTypeTabs>[data-tab=for_sale]')[0], 'backgroundLowlight');
    // and for sale tab should not
    refute.className(this.$('#searchTypeTabs>[data-tab=for_rent]')[0], 'backgroundLowlight');

    done();
  });
}

.type(selector|target, text, [callback])

Simulates user's typing, with all related events and proper timing.

'Get autocomplete suggestions': function(done)
{
  this.type('[data-action=searchForm]', 'Palo Al', function()
  {
    // Check first line of autosuggest
    // should contain "Palo Alto, CA"
    assert.contains(this.$('[data-role=autosuggest_list]>li:first-child').text(), 'Palo Alto, CA');

    done();
  });
}

.select(selector|target, option, [callback])

Simulates selecting option within dropdown, with all related events and proper timing.

'Set value for select drop-down': function(done)
{
  var minPrice = this.$('[data-role=selectTag]');

  this.select(minPrice, '1,000', function()
  {
    // Check correct drop-down value is selected
    assert.equals(minPrice[0].options[minPrice[0].selectedIndex].text, '1,000');
    done();
  });
}

.checkbox(selector|target, [callback])

Simulates tapping checkbox input, with all related events and proper timing.

'set property checked on input checkbox': function(done)
{
  var checkboxElement = this.$('input[type=checkbox]');

  this.checkbox(checkboxElement, function()
  {
    // Check input checkbox property checked is set to true
    assert.isTrue(checkboxElement.prop('checked'));
    done();
  });
}

.multipleCheckbox(selector|target, [callback])

Simulates tapping of multiple input checkboxes, with all related events and proper timing.

'set property checked on multiple input checkbox': function(done)
{
  var multipleCheckboxElements = this.$('[data-el=multipleCheckboxes] > input[type=checkbox]');

  this.checkbox(multipleCheckboxElements, function()
  {
    for(var i = 0; i < multipleCheckboxElements.length; i++)
    {
      // Check input checkbox property checked is set to true
      assert.isTrue(multipleCheckboxElements[i].prop('checked'));
    }
    done();
  });
}

.wait(event, callback)

Waits for the application level events ($(document) by default), if no window.$ is available, waits until it's resolved (via this._eventRoot).

'Go to the property page': function(done)
{
  // no property page yet
  refute.contains(this.$('[data-action=pdpDesc]').text(), 'Home Details');

  // tap on the property
  this.touch('[data-action=property] .propertyPhoto');

  // wait for the new page to activate
  this.wait('currentViewActive', function()
  {
    // property page is here
    assert.contains(this.$('[data-action=pdpDesc]').text(), 'Home Details');

    done();
  });
}

.waitForTransition(selector|element, callback)

Waits for CSS Transition to finish, triggers provided callback after that.

'Open side menu': function(done)
{
  // define target element
  var target = this.$('body>[data-view*=side_nav]');

  // when menu closed it has position absolute
  assert.equals(target.css('position'), 'absolute');

  // Toggle menu
  this.touch('#navToggle');

  // there is animation delay for sliding in, wait till animation is done
  this.waitForTransition(target, function()
  {
    // when open, side menu has position fixed
    assert.equals(target.css('position'), 'fixed');

    done();
  });
}

.waitForText(selector|element, text, callback)

Waits for element to show up on the page and contain provided text. Better to use with selectors rather than elements, to allow it to catch newly created elements.

Another autocomplete example, this time much closer to the real life.

'Get autocomplete suggestions': function(done)
{
  this.type('[data-action=searchForm]', 'Palo Al', function()
  {
    // need to wait for all the autocomplete requests to be resolved
    this.waitForText('[data-role=autosuggest_list]>li:first-child b', 'Palo Al', function()
    {
      // Check first line of autosuggest
      // should contain "Palo Alto, CA"
      assert.contains(this.$('[data-role=autosuggest_list]>li:first-child').text(), 'Palo Alto, CA');

      done();
    });
  });
}

.waitForElement(selector, callback)

Waits for element to show up on the page.

'Lazy loads property information': function(done)
{
  this.click('[data-action=loadInfo]', function()
  {
    // need to wait for all the autocomplete requests to be resolved
    this.waitForElement('[data-role=propertyInformation]', function()
    {
      // Check first line of autosuggest
      // should contain "Palo Alto, CA"
      assert.contains(this.$('[data-role=propertyInformation]').text(), 'Home Details');

      done();
    });
  });
}

.waitForElementToContain(selector|element, callback)

Waits for the element to have some content.

'Has the google ad loaded': function(done)
{
  this.waitForElementToContain('[data-role="page_first_ad"]', function() {
    assert.contains(this.$('[data-role="page_first_ad"]').find('div:first').attr('id'), 'google_ads_iframe_');
    done();
  });
}

.waitForVar(variable, callback)

Waits for variable to be defined.

// wait for App to be initialized on page load
setUp: function(done)
{
  // load new homepage for each test
  this.load(url)
    .enhance(common.es5Shim)
    .waitForVar('App', function()
    {
      // expose App to the tests
      this.App = this.window.App;
      // set App as event root
      this._setEventRoot(this.window.App);
      // get back to the tests
      done();
    }.bind(this));
}

.enhance(callback)

Provides means to enhance testable environment by enhancing in-iframe window object.

Note: Enhance handler function will called bound to the test object.

setUp: function(done)
{
  // load new app's homepage for each test
  this.load('/index').wait('postPageRender', done);

  this.enhance(function(iframeWindow)
  {
    // channel App's console.log output into test reporting
    iframeWindow.console.log = console.log.bind(console);
  });
}

Rendr

For Rendr based application you need to wait for the App object to be instantiated, before proceeding to the tests.

Following example shows possible setUp function to use with Rendr-based sites.


setUp: function(done)
{
  this.load(url).waitForVar('App', function()
  {
    // expose App to the tests
    this.App = this.window.App;
    // set App as event root
    this._setEventRoot(this.window.App);
    // proceed with the tests
    done();

  }.bind(this));  
}

PhantomJS

At the moment PhantomJS uses older version of webkit that doesn't support Function.prototype.bind. As workaround you can add es5-shim to the list of libraries:

'Functional tests':
{
  environment: 'browser',
  libs:
  [
    // PhantomJS - https://github.com/ariya/phantomjs/issues/10522
    'assets/js/vendor/es5-shim.js'
  ],
  tests:
  [
    'tests/functional/**/*.js'
  ]
}

And add bind to the test page loaded in iframe:

setUp: function(done)
{
  // load new app's homepage for each test
  this.load('/index').wait('postPageRender', done);

  this.enhance(function(iframeWindow)
  {
    iframeWindow.Function.prototype.bind = Function.prototype.bind;
  });
}

Real Life Example

Functional testing of Trulia Mobile website using Buster.JS with buster-functional module in real browsers on mobile devices, including iPhones 6+ and 5S with iOS8/Safari, Androids 4.4 with Stock and Chrome browsers.

TODO

  • Integrate resource proxy changes into buster-server.
  • Improve documentation.

License

MIT