QUnit Test Suite Source

You can view the full source code for index.html, cli.js and qunit.js below. The unit tests can be run online in-browser at agjVersionless.agjjQuery.org/tests/index.html.

index.html

<!DOCTYPE html>
<!--/**
 * Copyright (c) 2026 Andrew G. Johnson <andrew@andrewgjohnson.com>
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the “Software”), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * @file The in-browser runner for the agjVersionless plugin test suite.
 * @copyright 2026 Andrew G. Johnson <andrew@andrewgjohnson.com>
 * @license MIT
 * @see {@link https://github.com/andrewgjohnson/agjVersionless GitHub Repository}
 * @see {@link https://agjVersionless.agjjQuery.org/ Online Documentation}
 * @author Andrew G. Johnson <andrew@andrewgjohnson.com>
 * @version 1.0.0
 */-->
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>agjVersionless QUnit Test Suite</title>
    <link rel="canonical" href="https://agjVersionless.agjjQuery.org/tests/index.html" />
    <link rel="shortcut icon" href="https://github.githubassets.com/favicon.ico" />
    <link rel="icon" href="https://github.githubassets.com/favicon.ico" />
    <link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.26.0.css" />
    <script type="text/javascript" src="https://code.jquery.com/qunit/qunit-2.26.0.js"></script>
  </head>
  <body>
    <div id="qunit"></div>
    <div id="qunit-fixture"></div>
    <script type="text/javascript">
      QUnit.config.autostart = false;

      (function() {
        // Define the URLs of the various supported versions of jQuery
        var jQueryUrls = {
          'default': 'https://code.jquery.com/jquery-4.0.0.min.js',
          '1.12.4':  'https://code.jquery.com/jquery-1.12.4.min.js',
          '2.2.4':   'https://code.jquery.com/jquery-2.2.4.min.js',
          '3.7.1':   'https://code.jquery.com/jquery-3.7.1.min.js',
          '4.0.0':   'https://code.jquery.com/jquery-4.0.0.min.js'
        };

        // Define the user-controlled options for the QUnit in-browser experience
        QUnit.config.urlConfig.push({
          id:      'minified',
          label:   'Use minified',
          tooltip: 'Whether or not to use the plugin’s minified Javascript'
        });

        QUnit.config.urlConfig.push({
          id:      'jquery',
          label:   'jQuery version',
          tooltip: 'Which version of jQuery to test with',
          value:   (function() {
            // Generate an array of jQuery versions based on the previously defined jQueryUrls object
            var versions = [];
            for (var version in jQueryUrls) {
              if (Object.prototype.hasOwnProperty.call(jQueryUrls, version) && version !== 'default') {
                versions.push(version);
              }
            }
            return versions;
          })()
        });

        // Function to dynamically load Javascript
        var loadScript = function(src, callback) {
          var script = document.createElement('script');
          script.src = src;
          script.onload = callback;
          document.head.appendChild(script);
        };

        // Load the selected jQuery version first
        var jQueryUrl = jQueryUrls[QUnit.urlParams.jquery || 'default'];
        loadScript(jQueryUrl, function() {
          // Load the agjVersionless plugin after jQuery
          var pluginUrl;
          if (QUnit.urlParams.minified === 'true') {
            pluginUrl = '../distribution/jquery.agjVersionless.min.js';
          } else {
            pluginUrl = '../distribution/jquery.agjVersionless.js';
          }
          loadScript(pluginUrl, function() {
            // Load the QUnit test suite after agjVersionless
            loadScript('qunit.js', function() {
              // Start the QUnit test suite once all the other scripts have loaded
              QUnit.start();
            });
          });
        });
      })();
    </script>
  </body>
</html>

cli.js

/**
 * Copyright (c) 2026 Andrew G. Johnson <andrew@andrewgjohnson.com>
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * @file The CLI runner for the agjVersionless plugin test suite.
 * @copyright 2026 Andrew G. Johnson <andrew@andrewgjohnson.com>
 * @license MIT
 * @see {@link https://github.com/andrewgjohnson/agjVersionless GitHub Repository}
 * @see {@link https://agjVersionless.agjjQuery.org/ Online Documentation}
 * @author Andrew G. Johnson <andrew@andrewgjohnson.com>
 * @version 1.0.0
 */

const {JSDOM} = require('jsdom');
const {window} = new JSDOM(
  '<!DOCTYPE html>' +
    '<html>' +
    '<head>' +
    '<meta charset="utf-8" />' +
    '<title>agjVersionless QUnit Test Suite</title>' +
    '</head>' +
    '<body>' +
    '<div id="qunit"></div>' +
    '<div id="qunit-fixture"></div>' +
    '</body>' +
    '</html>',
);
const {document, navigator} = window;

global.window = window;
global.document = document;
global.navigator = navigator;

const jQuery = require('jquery');
global.jQuery = jQuery;

// The agjVersionless plugin and QUnit test suite
require('../distribution/jquery.agjVersionless.js');
require('./qunit.js');

qunit.js

/**
 * Copyright (c) 2026 Andrew G. Johnson <andrew@andrewgjohnson.com>
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * @file The QUnit test suite for the agjVersionless jQuery plugin.
 * @copyright 2026 Andrew G. Johnson <andrew@andrewgjohnson.com>
 * @license MIT
 * @see {@link https://github.com/andrewgjohnson/agjVersionless GitHub Repository}
 * @see {@link https://agjVersionless.agjjQuery.org/ Online Documentation}
 * @author Andrew G. Johnson <andrew@andrewgjohnson.com>
 * @version 1.0.0
 */

QUnit.on('runEnd', details => {
  const formatNumber = number => new Intl.NumberFormat('en-CA').format(number);

  if (console && console.log) {
    const assertions = {total: 0, passed: 0, skipped: 0, todo: 0};

    if (details.childSuites) {
      for (let i = 0; i < details.childSuites.length; i++) {
        if (details.childSuites[i].tests) {
          for (let j = 0; j < details.childSuites[i].tests.length; j++) {
            if (details.childSuites[i].tests[j].assertions) {
              for (
                let k = 0;
                k < details.childSuites[i].tests[j].assertions.length;
                k++
              ) {
                assertions.total++;

                const assertion = details.childSuites[i].tests[j].assertions[k];
                if (
                  typeof assertion.passed !== 'undefined' &&
                  assertion.passed === true
                ) {
                  assertions.passed++;
                }
                if (
                  typeof assertion.skipped !== 'undefined' &&
                  assertion.skipped === true
                ) {
                  assertions.skipped++;
                }
                if (
                  typeof assertion.todo !== 'undefined' &&
                  assertion.todo === true
                ) {
                  assertions.todo++;
                }
              }
            }
          }
        }
      }
    }

    const colours = {
      reset: '\x1b[0m',
      black: '\x1b[30m',
      red: '\x1b[31m',
      green: '\x1b[32m',
      yellow: '\x1b[33m',
      blue: '\x1b[34m',
      purple: '\x1b[35m',
      cyan: '\x1b[36m',
      white: '\x1b[37m',
    };

    console.log('');
    console.log(colours.green + 'ASSERTIONS' + colours.reset);
    console.log(
      colours.white +
        '# pass ' +
        formatNumber(assertions.passed) +
        colours.reset,
    );
    console.log(
      colours.yellow +
        '# skip ' +
        formatNumber(assertions.skipped) +
        colours.reset,
    );
    console.log(
      colours.cyan + '# todo ' + formatNumber(assertions.todo) + colours.reset,
    );
    console.log(
      colours.red +
        '# fail ' +
        formatNumber(
          assertions.total -
            assertions.passed -
            assertions.skipped -
            assertions.todo,
        ) +
        colours.reset,
    );

    console.log('');
    console.log(colours.green + 'RUNTIME' + colours.reset);
    if (details.runtime > 1000 * 60 * 60) {
      const hours = Math.max(1, Math.round(details.runtime / (1000 * 60 * 60)));
      console.log(
        colours.white +
          '# ' +
          formatNumber(hours) +
          ' ' +
          (hours === 1 ? 'hour' : 'hours') +
          colours.reset,
      );
    } else if (details.runtime > 1000 * 60) {
      const minutes = Math.max(1, Math.round(details.runtime / (1000 * 60)));
      console.log(
        colours.white +
          '# ' +
          minutes +
          ' ' +
          (minutes === 1 ? 'minute' : 'minutes') +
          colours.reset,
      );
    } else if (details.runtime > 1000) {
      const seconds = Math.max(1, Math.round(details.runtime / 1000));
      console.log(
        colours.white +
          '# ' +
          seconds +
          ' ' +
          (seconds === 1 ? 'second' : 'seconds') +
          colours.reset,
      );
    } else {
      const milliseconds = details.runtime;
      console.log(
        colours.white +
          '# ' +
          milliseconds +
          ' ' +
          (milliseconds === 1 ? 'millisecond' : 'milliseconds') +
          colours.reset,
      );
    }
  }
});

QUnit.module('agjVersionless');

QUnit.test('$.agjVersionless exists', assert => {
  assert.strictEqual(
    typeof jQuery.agjVersionless,
    'object',
    '$.agjVersionless should be an object',
  );
});

QUnit.test('$.agjVersionless.bind exists', assert => {
  assert.strictEqual(
    typeof jQuery.agjVersionless.bind,
    'function',
    '$.agjVersionless.bind should be a function',
  );
});

QUnit.test('$.agjVersionless.unbind exists', assert => {
  assert.strictEqual(
    typeof jQuery.agjVersionless.unbind,
    'function',
    '$.agjVersionless.unbind should be a function',
  );
});

QUnit.test('$.agjVersionless.bind binds an event handler', assert => {
  const element = jQuery('<div></div>');
  let triggered = false;

  jQuery.agjVersionless.bind(element, 'click', () => {
    triggered = true;
  });

  element.trigger('click');

  assert.true(triggered, 'Event handler should have been called after trigger');
});

QUnit.test('$.agjVersionless.bind binds an event handler with data', assert => {
  const element = jQuery('<div></div>');
  let receivedData = null;

  const data = {key: 'value'};
  jQuery.agjVersionless.bind(element, 'click', data, event => {
    receivedData = event.data;
  });

  element.trigger('click');

  assert.deepEqual(
    receivedData,
    {key: 'value'},
    'Event handler should have received the data object',
  );
});

QUnit.test('$.agjVersionless.unbind unbinds an event handler', assert => {
  const element = jQuery('<div></div>');
  let callCount = 0;

  const handler = function () {
    callCount++;
  };

  element.on('click', handler);
  element.trigger('click');

  jQuery.agjVersionless.unbind(element, 'click');
  element.trigger('click');

  assert.strictEqual(
    callCount,
    1,
    'Event handler should have been called exactly once before unbind',
  );
});

QUnit.test('$.agjVersionless.bind returns the element for chaining', assert => {
  const element = jQuery('<div></div>');

  const result = jQuery.agjVersionless.bind(element, 'click', () => {});

  assert.ok(
    result instanceof jQuery,
    'bind should return a jQuery object for chaining',
  );
});

QUnit.test(
  '$.agjVersionless.unbind returns the element for chaining',
  assert => {
    const element = jQuery('<div></div>');

    const result = jQuery.agjVersionless.unbind(element, 'click');

    assert.ok(
      result instanceof jQuery,
      'unbind should return a jQuery object for chaining',
    );
  },
);

QUnit.test('$.fn.agjVersionless exists', assert => {
  const element = jQuery('<div></div>');
  assert.strictEqual(
    typeof element.agjVersionless,
    'object',
    '$.fn.agjVersionless should be an object',
  );
});

QUnit.test('$.fn.agjVersionless.bind exists', assert => {
  const element = jQuery('<div></div>');
  assert.strictEqual(
    typeof element.agjVersionless.bind,
    'function',
    '$.fn.agjVersionless.bind should be a function',
  );
});

QUnit.test('$.fn.agjVersionless.unbind exists', assert => {
  const element = jQuery('<div></div>');
  assert.strictEqual(
    typeof element.agjVersionless.unbind,
    'function',
    '$.fn.agjVersionless.unbind should be a function',
  );
});

QUnit.test('$.fn.agjVersionless.bind binds an event handler', assert => {
  const element = jQuery('<div></div>');
  let triggered = false;

  element.agjVersionless.bind('click', () => {
    triggered = true;
  });

  element.trigger('click');

  assert.true(triggered, 'Event handler should have been called after trigger');
});

QUnit.test(
  '$.fn.agjVersionless.bind binds an event handler with data',
  assert => {
    const element = jQuery('<div></div>');
    let receivedData = null;

    const data = {key: 'value'};
    element.agjVersionless.bind('click', data, event => {
      receivedData = event.data;
    });

    element.trigger('click');

    assert.deepEqual(
      receivedData,
      {key: 'value'},
      'Event handler should have received the data object',
    );
  },
);

QUnit.test('$.fn.agjVersionless.unbind unbinds an event handler', assert => {
  const element = jQuery('<div></div>');
  let callCount = 0;

  const handler = function () {
    callCount++;
  };

  element.on('click', handler);
  element.trigger('click');

  element.agjVersionless.unbind('click');
  element.trigger('click');

  assert.strictEqual(
    callCount,
    1,
    'Event handler should have been called exactly once before unbind',
  );
});

QUnit.test(
  '$.fn.agjVersionless.bind returns the element for chaining',
  assert => {
    const element = jQuery('<div></div>');

    const result = element.agjVersionless.bind('click', () => {});

    assert.ok(
      result instanceof jQuery,
      'bind should return a jQuery object for chaining',
    );
  },
);

QUnit.test(
  '$.fn.agjVersionless.unbind returns the element for chaining',
  assert => {
    const element = jQuery('<div></div>');

    const result = element.agjVersionless.unbind('click');

    assert.ok(
      result instanceof jQuery,
      'unbind should return a jQuery object for chaining',
    );
  },
);

QUnit.test('$.fn.agjVersionless.on exists', assert => {
  const element = jQuery('<div></div>');
  assert.strictEqual(
    typeof element.agjVersionless.on,
    'function',
    '$.fn.agjVersionless.on should be a function',
  );
});

QUnit.test('$.fn.agjVersionless.off exists', assert => {
  const element = jQuery('<div></div>');
  assert.strictEqual(
    typeof element.agjVersionless.off,
    'function',
    '$.fn.agjVersionless.off should be a function',
  );
});

QUnit.test('$.fn.agjVersionless.on binds an event handler', assert => {
  const element = jQuery('<div></div>');
  let triggered = false;

  element.agjVersionless.on('click', () => {
    triggered = true;
  });

  element.trigger('click');

  assert.true(triggered, 'Event handler should have been called after trigger');
});

QUnit.test(
  '$.fn.agjVersionless.on binds an event handler with data',
  assert => {
    const element = jQuery('<div></div>');
    let receivedData = null;

    const data = {key: 'value'};
    element.agjVersionless.on('click', data, event => {
      receivedData = event.data;
    });

    element.trigger('click');

    assert.deepEqual(
      receivedData,
      {key: 'value'},
      'Event handler should have received the data object',
    );
  },
);

QUnit.test('$.fn.agjVersionless.off unbinds an event handler', assert => {
  const element = jQuery('<div></div>');
  let callCount = 0;

  const handler = function () {
    callCount++;
  };

  element.on('click', handler);
  element.trigger('click');

  element.agjVersionless.off('click');
  element.trigger('click');

  assert.strictEqual(
    callCount,
    1,
    'Event handler should have been called exactly once before off',
  );
});

QUnit.test(
  '$.fn.agjVersionless.on returns the element for chaining',
  assert => {
    const element = jQuery('<div></div>');

    const result = element.agjVersionless.on('click', () => {});

    assert.ok(
      result instanceof jQuery,
      'on should return a jQuery object for chaining',
    );
  },
);

QUnit.test(
  '$.fn.agjVersionless.off returns the element for chaining',
  assert => {
    const element = jQuery('<div></div>');

    const result = element.agjVersionless.off('click');

    assert.ok(
      result instanceof jQuery,
      'off should return a jQuery object for chaining',
    );
  },
);