diff --git a/Netdisco/t/11-portsort.t b/Netdisco/t/11-portsort.t new file mode 100755 index 00000000..3563f3b6 --- /dev/null +++ b/Netdisco/t/11-portsort.t @@ -0,0 +1,18 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use Test::More; +use Env::Path; +use FindBin qw( $Bin ); + +my @phantomjs = Env::Path->PATH->Whence('phantomjs'); +my $phantomjs = scalar @phantomjs ? $phantomjs[0] : $ENV{ND_PHANTOMJS}; + +if ( !-x $phantomjs ) { + plan skip_all => + "phantomjs not found, please set ND_PHANTOMJS or install phantomjs to the default location"; +} +else { + exec( $phantomjs, "$Bin/js/run_qunit.js", "$Bin/html/portsort.html" ); +} diff --git a/Netdisco/t/html/portsort.html b/Netdisco/t/html/portsort.html new file mode 100644 index 00000000..cfa2cd99 --- /dev/null +++ b/Netdisco/t/html/portsort.html @@ -0,0 +1,243 @@ + + + + + portsort.js test suite + + + + + + + + + + +
+
+ + diff --git a/Netdisco/t/js/qunit-tap.js b/Netdisco/t/js/qunit-tap.js new file mode 100644 index 00000000..7031e6c1 --- /dev/null +++ b/Netdisco/t/js/qunit-tap.js @@ -0,0 +1,378 @@ +/** + * QUnit-TAP - A TAP Output Producer Plugin for QUnit + * + * https://github.com/twada/qunit-tap + * version: 1.4.2 + * + * Copyright (c) 2010-2014 Takuto Wada + * Dual licensed under the MIT and GPLv2 licenses. + * https://raw.github.com/twada/qunit-tap/master/MIT-LICENSE.txt + * https://raw.github.com/twada/qunit-tap/master/GPL-LICENSE.txt + * + * A part of extend function is: + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * A part of stripTags function is: + * Copyright (c) 2005-2010 Sam Stephenson + * Released under the MIT license. + * http://prototypejs.org + */ +(function (root, factory) { + 'use strict'; + + // using returnExports UMD pattern + if (typeof define === 'function' && define.amd) { + define(factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + root.qunitTap = factory(); + } +}(this, function () { + 'use strict'; + + var qunitTapVersion = '1.4.2', + slice = Array.prototype.slice; + + // borrowed from qunit.js + function extend (a, b) { + var prop; + for (prop in b) { + if (b.hasOwnProperty(prop)) { + if (typeof b[prop] === 'undefined') { + delete a[prop]; + } else { + a[prop] = b[prop]; + } + } + } + return a; + } + + function indexOf (ary, element) { + var i; + for (i = 0; i < ary.length; i += 1) { + if (ary[i] === element) { + return i; + } + } + return -1; + } + + function removeElement (ary, element) { + var index = indexOf(ary, element); + if (index !== -1) { + return ary.splice(index, 1); + } else { + return []; + } + } + + function isPlanRequired (conf) { + return (typeof conf !== 'undefined' && typeof conf.requireExpects !== 'undefined' && conf.requireExpects); + } + + function isPassed (details) { + return !!(details.result); + } + + function isFailed (details) { + return !(isPassed(details)); + } + + function isAssertOkFailed (details) { + return isFailed(details) && typeof details.expected === 'undefined' && typeof details.actual === 'undefined'; + } + + // borrowed from prototype.js + // not required since QUnit.log receives raw data (details). see jquery/qunit@c2cde34 + function stripTags (str) { + if (!str) { + return str; + } + return str.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi, ''); + } + + function escapeLineEndings (str) { + return str.replace(/(\r?\n)/g, '$&# '); + } + + function ltrim (str) { + return str.replace(/^\s+/, ''); + } + + function noop (obj) { + return obj; + } + + function render (desc, fieldName, fieldValue, formatter) { + desc.push(fieldName + ': ' + formatter(fieldValue)); + } + + function renderIf (shouldRender, desc, fieldName, fieldValue, formatter) { + if (!shouldRender || typeof fieldValue === 'undefined') { + return; + } + render(desc, fieldName, fieldValue, formatter); + } + + function formatTestLine (testLine, rest) { + if (!rest) { + return testLine; + } + return testLine + ' - ' + escapeLineEndings(rest); + } + + var extractDetailsFrom = (function () { + var detailsExtractor; + + function setupExtractor (logArguments) { + switch (logArguments.length) { + case 1: // details + detailsExtractor = function (args) { return args[0]; }; + break; + case 2: // result, message(with tags) + detailsExtractor = function (args) { return {result: args[0], message: stripTags(args[1])}; }; + break; + case 3: // result, message, details + detailsExtractor = function (args) { return args[2]; }; + break; + default: + throw new Error('QUnit-TAP does not support QUnit#log arguments like this.'); + } + } + + return function (logArguments) { + if (detailsExtractor) { + return detailsExtractor(logArguments); + } + setupExtractor(logArguments); + return detailsExtractor(logArguments); + }; + })(); + + var createCallbackAppenderFor = function (qu) { + // detect QUnit's multipleCallbacks feature. see jquery/qunit@34f6bc1 + var isMultipleLoggingCallbacksSupported = + (typeof qu.config !== 'undefined' && + typeof qu.config.log !== 'undefined' && + typeof qu.config.done !== 'undefined' && + typeof qu.config.testDone !== 'undefined' && + typeof qu.config.moduleStart !== 'undefined' && + typeof qu.config.testStart !== 'undefined'); + return function (subject, observer, event) { + var originalLoggingCallback = subject[event], + callback; + if (isMultipleLoggingCallbacksSupported) { + callback = function () { + // make listener methods (moduleStart,testStart,log, ...) overridable. + observer[event].apply(observer, slice.apply(arguments)); + }; + originalLoggingCallback(callback); + } else if (typeof originalLoggingCallback === 'function') { + // do not overwrite old-style logging callbacks + callback = function () { + var args = slice.apply(arguments); + originalLoggingCallback.apply(subject, args); + observer[event].apply(observer, args); + }; + subject[event] = callback; + } + return callback; + }; + }; + + + /** + * QUnit-TAP - A TAP Output Producer Plugin for QUnit + * @param qunitObject QUnit object reference. + * @param printLikeFunction print-like function for TAP output (assumes line-separator is added by this function for each call). + * @param options configuration options to customize default behavior. + * @return object to provide QUnit-TAP API and customization subject. + */ + function qunitTap(qunitObject, printLikeFunction, options) { + if (!qunitObject) { + throw new Error('should pass QUnit object reference. Please check QUnit\'s "require" path if you are using Node.js (or any CommonJS env).'); + } else if (typeof printLikeFunction !== 'function') { + throw new Error('should pass print-like function'); + } + + var qu = qunitObject, + tap = {}, + jsDumpExists = (typeof qu.jsDump !== 'undefined' && typeof qu.jsDump.parse === 'function'), + explain = (jsDumpExists ? function explain (obj) { return qu.jsDump.parse(obj); } : noop), + deprecateOption = function deprecateOption (optionName, fallback) { + // option deprecation and fallback function + if (!options || typeof options !== 'object') { + return; + } + if (typeof options[optionName] === 'undefined') { + return; + } + printLikeFunction('# WARNING: Option "' + optionName + '" is deprecated and will be removed in future version.'); + fallback(options[optionName]); + }, + targetEvents = [ + 'moduleStart', + 'testStart', + 'log', + 'testDone', + 'done' + ], + registeredCallbacks = {}; + + + tap.config = extend( + { + initialCount: 1, + showModuleNameOnFailure: true, + showTestNameOnFailure: true, + showExpectationOnFailure: true, + showSourceOnFailure: true + }, + options + ); + deprecateOption('noPlan', function (flag) { + printLikeFunction('# Now QUnit-TAP works as with "noPlan: true" by default. If you want to delare plan explicitly, please use "QUnit.config.requireExpects" option instead.'); + tap.config.noPlan = flag; + }); + deprecateOption('count', function (count) { + tap.config.initialCount = (count + 1); + }); + deprecateOption('showDetailsOnFailure', function (flag) { + tap.config.showModuleNameOnFailure = flag; + tap.config.showTestNameOnFailure = flag; + tap.config.showExpectationOnFailure = flag; + tap.config.showSourceOnFailure = flag; + }); + tap.VERSION = qunitTapVersion; + tap.puts = printLikeFunction; + tap.count = tap.config.initialCount - 1; + tap.expectedCount = tap.config.initialCount - 1; + + function isEnabled (configName) { + return tap.config[configName]; + } + + function formatDetails (details) { + if (isPassed(details)) { + return details.message; + } + var desc = []; + if (details.message) { + desc.push(details.message); + } + if (isEnabled('showExpectationOnFailure') && !(isAssertOkFailed(details))) { + render(desc, 'expected', details.expected, explain); + render(desc, 'got', details.actual, explain); + } + renderIf(isEnabled('showTestNameOnFailure'), desc, 'test', details.name, noop); + renderIf(isEnabled('showModuleNameOnFailure'), desc, 'module', details.module, noop); + renderIf(isEnabled('showSourceOnFailure'), desc, 'source', details.source, ltrim); + return desc.join(', '); + } + + function printPlanLine (toCount) { + tap.puts(tap.config.initialCount + '..' + toCount); + } + + function unsubscribeEvent (eventName) { + var listeners; + if (indexOf(targetEvents, eventName) === -1) { + return; + } + listeners = qu.config[eventName]; + if (typeof listeners === 'undefined') { + return; + } + removeElement(listeners, registeredCallbacks[eventName]); + } + + function unsubscribeEvents (eventNames) { + var i; + for (i = 0; i < eventNames.length; i += 1) { + unsubscribeEvent(eventNames[i]); + } + } + + tap.explain = explain; + + tap.note = function note (obj) { + tap.puts(escapeLineEndings('# ' + obj)); + }; + + tap.diag = function diag (obj) { + tap.note(obj); + return false; + }; + + tap.moduleStart = function moduleStart (arg) { + var name = (typeof arg === 'string') ? arg : arg.name; + tap.note('module: ' + name); + }; + + tap.testStart = function testStart (arg) { + var name = (typeof arg === 'string') ? arg : arg.name; + tap.note('test: ' + name); + }; + + tap.log = function log () { + var details = extractDetailsFrom(arguments), + testLine = ''; + tap.count += 1; + if (isFailed(details)) { + testLine += 'not '; + } + testLine += ('ok ' + tap.count); + tap.puts(formatTestLine(testLine, formatDetails(details))); + }; + + tap.testDone = function testDone () { + if (isPlanRequired(qu.config)) { + tap.expectedCount += qu.config.current.expected; + } + }; + + tap.done = function done () { + if (typeof tap.config.noPlan !== 'undefined' && !(tap.config.noPlan)) { + // Do nothing until removal of 'noPlan' option. + } else if (isPlanRequired(qu.config)) { + printPlanLine(tap.expectedCount); + } else { + printPlanLine(tap.count); + } + }; + + tap.unsubscribe = function unsubscribe () { + if (typeof qu.config === 'undefined') { + return; + } + if (arguments.length === 0) { + unsubscribeEvents(targetEvents); + } else { + unsubscribeEvents(slice.apply(arguments)); + } + }; + + (function () { + var appendCallback = createCallbackAppenderFor(qu), + eventName, i, callback; + for (i = 0; i < targetEvents.length; i += 1) { + eventName = targetEvents[i]; + callback = appendCallback(qu, tap, eventName); + registeredCallbacks[eventName] = callback; + } + })(); + + return tap; + } + + qunitTap.qunitTap = function () { + throw new Error('[BC BREAK] Since 1.4.0, QUnit-TAP exports single qunitTap function as module.exports. Therefore, require("qunit-tap") returns qunitTap function itself. Please fix your code if you are using Node.js (or any CommonJS env).'); + }; + + // using substack pattern (export single function) + return qunitTap; +})); diff --git a/Netdisco/t/js/run_qunit.js b/Netdisco/t/js/run_qunit.js new file mode 100644 index 00000000..c6d236f5 --- /dev/null +++ b/Netdisco/t/js/run_qunit.js @@ -0,0 +1,74 @@ +/** + * Wait until the test condition is true or a timeout occurs. Useful for waiting + * on a server response or for a ui change (fadeIn, etc.) to occur. + * + * @param testFx javascript condition that evaluates to a boolean, + * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or + * as a callback function. + * @param onReady what to do when testFx condition is fulfilled, + * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or + * as a callback function. + * @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used. + */ +function waitFor(testFx, onReady, timeOutMillis) { + var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001, //< Default Max Timout is 3s + start = new Date().getTime(), + condition = false, + interval = setInterval(function() { + if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) { + // If not time-out yet and condition not yet fulfilled + condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code + } else { + if(!condition) { + // If condition still not fulfilled (timeout but condition is 'false') + console.log("# 'waitFor()' timeout"); + phantom.exit(1); + } else { + // Condition fulfilled (timeout and/or condition is 'true') + // console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms."); + typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled + clearInterval(interval); //< Stop this interval + } + } + }, 100); //< repeat check every 250ms +}; + + +if (phantom.args.length === 0 || phantom.args.length > 2) { + console.log('Usage: run-qunit.js URL'); + phantom.exit(1); +} + +var page = new WebPage(); + +// Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this") +page.onConsoleMessage = function(msg) { + console.log(msg); +}; + +page.open(phantom.args[0], function(status){ + if (status !== "success") { + console.log("Unable to access network"); + phantom.exit(1); + } else { + waitFor(function(){ + return page.evaluate(function(){ + var el = document.getElementById('qunit-testresult'); + if (el && el.innerText.match('completed')) { + return true; + } + return false; + }); + }, function(){ + var failedNum = page.evaluate(function(){ + var el = document.getElementById('qunit-testresult'); + // console.log(el.innerText); + try { + return el.getElementsByClassName('failed')[0].innerHTML; + } catch (e) { } + return 10000; + }); + phantom.exit((parseInt(failedNum, 10) > 0) ? 1 : 0); + }); + } +});