diff --git a/Gruntfile.js b/Gruntfile.js
index 15d5d861..fbf49a7b 100755
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -10,13 +10,13 @@ module.exports = function(grunt) {
["clean:dev", "concat:css", "concat:js", "copy:htmlDev", "copy:staticDev", "chmod:build", "watch"]);
grunt.registerTask("test",
- "A persistent task which creates a test build whenever source files are modified.",
- ["clean:dev", "concat:cssTest", "concat:jsTest", "copy:htmlTest", "copy:staticTest", "chmod:build", "watch"]);
+ "A task which runs all the tests in test/tests.",
+ ["clean:test", "concat:jsTest", "copy:htmlTest", "chmod:build", "exec:tests"]);
grunt.registerTask("prod",
"Creates a production-ready build. Use the --msg flag to add a compile message.",
["eslint", "exec:stats", "clean", "jsdoc", "concat", "copy:htmlDev", "copy:htmlProd", "copy:htmlInline",
- "copy:staticDev", "copy:staticProd", "cssmin", "uglify:prod", "inline", "htmlmin", "chmod"]);
+ "copy:staticDev", "copy:staticProd", "cssmin", "uglify:prod", "inline", "htmlmin", "chmod", "test"]);
grunt.registerTask("docs",
"Compiles documentation in the /docs directory.",
@@ -138,6 +138,7 @@ module.exports = function(grunt) {
"src/js/lib/vkbeautify.js",
"src/js/lib/Sortable.js",
"src/js/lib/bootstrap-colorpicker.js",
+ "src/js/lib/es6-promise.auto.js",
"src/js/lib/xpath.js",
// Custom libraries
@@ -171,11 +172,9 @@ module.exports = function(grunt) {
var jsTestFiles = [].concat(
jsIncludes,
[
- "src/js/lib/vuejs/vue.min.js",
- "src/js/test/*.js",
- "src/js/test/tests/**/*",
- // Start the test runner app!
- "src/js/test/views/html/main.js",
+ "test/TestRegister.js",
+ "test/tests/**/*.js",
+ "test/TestRunner.js",
]
);
@@ -219,7 +218,11 @@ module.exports = function(grunt) {
config: ["src/js/config/**/*.js"],
views: ["src/js/views/**/*.js"],
operations: ["src/js/operations/**/*.js"],
- tests: ["src/js/test/**/*.js"],
+ tests: [
+ "test/**/*.js",
+ "!test/PhantomRunner.js",
+ "!test/NodeRunner.js",
+ ],
},
jsdoc: {
options: {
@@ -238,6 +241,7 @@ module.exports = function(grunt) {
},
clean: {
dev: ["build/dev/*"],
+ test: ["build/test/*"],
prod: ["build/prod/*"],
docs: ["docs/*", "!docs/*.conf.json", "!docs/*.ico"],
},
@@ -261,22 +265,6 @@ module.exports = function(grunt) {
],
dest: "build/dev/styles.css"
},
- cssTest: {
- options: {
- banner: banner.replace(/\/\*\*/g, "/*!"),
- process: function(content, srcpath) {
- // Change special comments from /** to /*! to comply with cssmin
- content = content.replace(/^\/\*\* /g, "/*! ");
- return grunt.template.process(content);
- }
- },
- src: [
- "src/css/lib/**/*.css",
- "src/css/structure/**/*.css",
- "src/css/themes/classic.css"
- ],
- dest: "build/test/styles.css"
- },
js: {
options: {
banner: '"use strict";\n'
@@ -289,7 +277,7 @@ module.exports = function(grunt) {
banner: '"use strict";\n'
},
src: jsTestFiles,
- dest: "build/test/scripts.js"
+ dest: "build/test/tests.js"
}
},
copy: {
@@ -348,21 +336,6 @@ module.exports = function(grunt) {
}
]
},
- staticTest: {
- files: [
- {
- expand: true,
- cwd: "src/static/",
- src: [
- "**/*",
- "**/.*",
- "!stats.txt",
- "!ga.html"
- ],
- dest: "build/test/"
- }
- ]
- },
staticProd: {
files: [
{
@@ -478,6 +451,9 @@ module.exports = function(grunt) {
}
},
exec: {
+ tests: {
+ command: "node test/NodeRunner.js",
+ },
repoSize: {
command: [
"git ls-files | wc -l | xargs printf '\n%b\ttracked files\n'",
@@ -537,15 +513,15 @@ module.exports = function(grunt) {
},
js: {
files: "src/js/**/*.js",
- tasks: ["concat:js", "concat:jsTest", "chmod:build"]
+ tasks: ["concat:js", "chmod:build"]
},
html: {
files: "src/html/**/*.html",
- tasks: ["copy:htmlDev", "copy:htmlTest", "chmod:build"]
+ tasks: ["copy:htmlDev", "chmod:build"]
},
static: {
files: ["src/static/**/*", "src/static/**/.*"],
- tasks: ["copy:staticDev", "copy:staticTest", "chmod:build"]
+ tasks: ["copy:staticDev", "chmod:build"]
},
grunt: {
files: "Gruntfile.js",
diff --git a/package.json b/package.json
index 96c81e3b..f8f17fe4 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
"grunt-exec": "~1.0.1",
"grunt-inline-alt": "~0.3.10",
"grunt-jsdoc": "^2.1.0",
- "ink-docstrap": "^1.1.4"
+ "ink-docstrap": "^1.1.4",
+ "phantomjs-prebuilt": "^2.1.14"
}
}
diff --git a/src/html/test.html b/src/html/test.html
index f38a7251..6c434e52 100755
--- a/src/html/test.html
+++ b/src/html/test.html
@@ -25,72 +25,10 @@
- CyberChef Test Runner
-
-
+ CyberChef
-
- {{ getIcon() }}
-
-
-
-
-
-
-
- {{ status }}
-
- {{ countTestsWithStatus(status.toLowerCase()) }}
-
-
-
-
- Passing
-
- {{ countTestsWithStatus("passing") }}
- /
- {{ tests.length }}
-
-
-
-
- % Passing
-
- {{ ((100.0 * countTestsWithStatus("passing")) / tests.length).toFixed(1) }}%
-
-
-
-
-
-
-
-
-
-
- |
-
- {{ test.name }}
- |
-
- {{ test.output }}
- |
-
-
-
-
-
-
-
- CyberChef Test Runner
-
-
-
-
-
-
-
-
+
+
diff --git a/src/js/.eslintrc.json b/src/js/.eslintrc.json
index 3ad149d5..7ea169f3 100755
--- a/src/js/.eslintrc.json
+++ b/src/js/.eslintrc.json
@@ -111,8 +111,8 @@
"SeasonalWaiter": false,
"WindowWaiter": false,
- /* test */
- "Vue": false,
- "TestRegister": false
+ /* tests */
+ "TestRegister": false,
+ "TestRunner": false
}
}
diff --git a/src/js/lib/es6-promise.auto.js b/src/js/lib/es6-promise.auto.js
new file mode 100644
index 00000000..632b8695
--- /dev/null
+++ b/src/js/lib/es6-promise.auto.js
@@ -0,0 +1,1159 @@
+/*!
+ * @overview es6-promise - a tiny implementation of Promises/A+.
+ * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
+ * @license Licensed under MIT license
+ * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE
+ * @version 4.0.5
+ */
+
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.ES6Promise = factory());
+}(this, (function () { 'use strict';
+
+function objectOrFunction(x) {
+ return typeof x === 'function' || typeof x === 'object' && x !== null;
+}
+
+function isFunction(x) {
+ return typeof x === 'function';
+}
+
+var _isArray = undefined;
+if (!Array.isArray) {
+ _isArray = function (x) {
+ return Object.prototype.toString.call(x) === '[object Array]';
+ };
+} else {
+ _isArray = Array.isArray;
+}
+
+var isArray = _isArray;
+
+var len = 0;
+var vertxNext = undefined;
+var customSchedulerFn = undefined;
+
+var asap = function asap(callback, arg) {
+ queue[len] = callback;
+ queue[len + 1] = arg;
+ len += 2;
+ if (len === 2) {
+ // If len is 2, that means that we need to schedule an async flush.
+ // If additional callbacks are queued before the queue is flushed, they
+ // will be processed by this flush that we are scheduling.
+ if (customSchedulerFn) {
+ customSchedulerFn(flush);
+ } else {
+ scheduleFlush();
+ }
+ }
+};
+
+function setScheduler(scheduleFn) {
+ customSchedulerFn = scheduleFn;
+}
+
+function setAsap(asapFn) {
+ asap = asapFn;
+}
+
+var browserWindow = typeof window !== 'undefined' ? window : undefined;
+var browserGlobal = browserWindow || {};
+var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver;
+var isNode = typeof self === 'undefined' && typeof process !== 'undefined' && ({}).toString.call(process) === '[object process]';
+
+// test for web worker but not in IE10
+var isWorker = typeof Uint8ClampedArray !== 'undefined' && typeof importScripts !== 'undefined' && typeof MessageChannel !== 'undefined';
+
+// node
+function useNextTick() {
+ // node version 0.10.x displays a deprecation warning when nextTick is used recursively
+ // see https://github.com/cujojs/when/issues/410 for details
+ return function () {
+ return process.nextTick(flush);
+ };
+}
+
+// vertx
+function useVertxTimer() {
+ if (typeof vertxNext !== 'undefined') {
+ return function () {
+ vertxNext(flush);
+ };
+ }
+
+ return useSetTimeout();
+}
+
+function useMutationObserver() {
+ var iterations = 0;
+ var observer = new BrowserMutationObserver(flush);
+ var node = document.createTextNode('');
+ observer.observe(node, { characterData: true });
+
+ return function () {
+ node.data = iterations = ++iterations % 2;
+ };
+}
+
+// web worker
+function useMessageChannel() {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = flush;
+ return function () {
+ return channel.port2.postMessage(0);
+ };
+}
+
+function useSetTimeout() {
+ // Store setTimeout reference so es6-promise will be unaffected by
+ // other code modifying setTimeout (like sinon.useFakeTimers())
+ var globalSetTimeout = setTimeout;
+ return function () {
+ return globalSetTimeout(flush, 1);
+ };
+}
+
+var queue = new Array(1000);
+function flush() {
+ for (var i = 0; i < len; i += 2) {
+ var callback = queue[i];
+ var arg = queue[i + 1];
+
+ callback(arg);
+
+ queue[i] = undefined;
+ queue[i + 1] = undefined;
+ }
+
+ len = 0;
+}
+
+function attemptVertx() {
+ try {
+ var r = require;
+ var vertx = r('vertx');
+ vertxNext = vertx.runOnLoop || vertx.runOnContext;
+ return useVertxTimer();
+ } catch (e) {
+ return useSetTimeout();
+ }
+}
+
+var scheduleFlush = undefined;
+// Decide what async method to use to triggering processing of queued callbacks:
+if (isNode) {
+ scheduleFlush = useNextTick();
+} else if (BrowserMutationObserver) {
+ scheduleFlush = useMutationObserver();
+} else if (isWorker) {
+ scheduleFlush = useMessageChannel();
+} else if (browserWindow === undefined && typeof require === 'function') {
+ scheduleFlush = attemptVertx();
+} else {
+ scheduleFlush = useSetTimeout();
+}
+
+function then(onFulfillment, onRejection) {
+ var _arguments = arguments;
+
+ var parent = this;
+
+ var child = new this.constructor(noop);
+
+ if (child[PROMISE_ID] === undefined) {
+ makePromise(child);
+ }
+
+ var _state = parent._state;
+
+ if (_state) {
+ (function () {
+ var callback = _arguments[_state - 1];
+ asap(function () {
+ return invokeCallback(_state, child, callback, parent._result);
+ });
+ })();
+ } else {
+ subscribe(parent, child, onFulfillment, onRejection);
+ }
+
+ return child;
+}
+
+/**
+ `Promise.resolve` returns a promise that will become resolved with the
+ passed `value`. It is shorthand for the following:
+
+ ```javascript
+ let promise = new Promise(function(resolve, reject){
+ resolve(1);
+ });
+
+ promise.then(function(value){
+ // value === 1
+ });
+ ```
+
+ Instead of writing the above, your code now simply becomes the following:
+
+ ```javascript
+ let promise = Promise.resolve(1);
+
+ promise.then(function(value){
+ // value === 1
+ });
+ ```
+
+ @method resolve
+ @static
+ @param {Any} value value that the returned promise will be resolved with
+ Useful for tooling.
+ @return {Promise} a promise that will become fulfilled with the given
+ `value`
+*/
+function resolve(object) {
+ /*jshint validthis:true */
+ var Constructor = this;
+
+ if (object && typeof object === 'object' && object.constructor === Constructor) {
+ return object;
+ }
+
+ var promise = new Constructor(noop);
+ _resolve(promise, object);
+ return promise;
+}
+
+var PROMISE_ID = Math.random().toString(36).substring(16);
+
+function noop() {}
+
+var PENDING = void 0;
+var FULFILLED = 1;
+var REJECTED = 2;
+
+var GET_THEN_ERROR = new ErrorObject();
+
+function selfFulfillment() {
+ return new TypeError("You cannot resolve a promise with itself");
+}
+
+function cannotReturnOwn() {
+ return new TypeError('A promises callback cannot return that same promise.');
+}
+
+function getThen(promise) {
+ try {
+ return promise.then;
+ } catch (error) {
+ GET_THEN_ERROR.error = error;
+ return GET_THEN_ERROR;
+ }
+}
+
+function tryThen(then, value, fulfillmentHandler, rejectionHandler) {
+ try {
+ then.call(value, fulfillmentHandler, rejectionHandler);
+ } catch (e) {
+ return e;
+ }
+}
+
+function handleForeignThenable(promise, thenable, then) {
+ asap(function (promise) {
+ var sealed = false;
+ var error = tryThen(then, thenable, function (value) {
+ if (sealed) {
+ return;
+ }
+ sealed = true;
+ if (thenable !== value) {
+ _resolve(promise, value);
+ } else {
+ fulfill(promise, value);
+ }
+ }, function (reason) {
+ if (sealed) {
+ return;
+ }
+ sealed = true;
+
+ _reject(promise, reason);
+ }, 'Settle: ' + (promise._label || ' unknown promise'));
+
+ if (!sealed && error) {
+ sealed = true;
+ _reject(promise, error);
+ }
+ }, promise);
+}
+
+function handleOwnThenable(promise, thenable) {
+ if (thenable._state === FULFILLED) {
+ fulfill(promise, thenable._result);
+ } else if (thenable._state === REJECTED) {
+ _reject(promise, thenable._result);
+ } else {
+ subscribe(thenable, undefined, function (value) {
+ return _resolve(promise, value);
+ }, function (reason) {
+ return _reject(promise, reason);
+ });
+ }
+}
+
+function handleMaybeThenable(promise, maybeThenable, then$$) {
+ if (maybeThenable.constructor === promise.constructor && then$$ === then && maybeThenable.constructor.resolve === resolve) {
+ handleOwnThenable(promise, maybeThenable);
+ } else {
+ if (then$$ === GET_THEN_ERROR) {
+ _reject(promise, GET_THEN_ERROR.error);
+ } else if (then$$ === undefined) {
+ fulfill(promise, maybeThenable);
+ } else if (isFunction(then$$)) {
+ handleForeignThenable(promise, maybeThenable, then$$);
+ } else {
+ fulfill(promise, maybeThenable);
+ }
+ }
+}
+
+function _resolve(promise, value) {
+ if (promise === value) {
+ _reject(promise, selfFulfillment());
+ } else if (objectOrFunction(value)) {
+ handleMaybeThenable(promise, value, getThen(value));
+ } else {
+ fulfill(promise, value);
+ }
+}
+
+function publishRejection(promise) {
+ if (promise._onerror) {
+ promise._onerror(promise._result);
+ }
+
+ publish(promise);
+}
+
+function fulfill(promise, value) {
+ if (promise._state !== PENDING) {
+ return;
+ }
+
+ promise._result = value;
+ promise._state = FULFILLED;
+
+ if (promise._subscribers.length !== 0) {
+ asap(publish, promise);
+ }
+}
+
+function _reject(promise, reason) {
+ if (promise._state !== PENDING) {
+ return;
+ }
+ promise._state = REJECTED;
+ promise._result = reason;
+
+ asap(publishRejection, promise);
+}
+
+function subscribe(parent, child, onFulfillment, onRejection) {
+ var _subscribers = parent._subscribers;
+ var length = _subscribers.length;
+
+ parent._onerror = null;
+
+ _subscribers[length] = child;
+ _subscribers[length + FULFILLED] = onFulfillment;
+ _subscribers[length + REJECTED] = onRejection;
+
+ if (length === 0 && parent._state) {
+ asap(publish, parent);
+ }
+}
+
+function publish(promise) {
+ var subscribers = promise._subscribers;
+ var settled = promise._state;
+
+ if (subscribers.length === 0) {
+ return;
+ }
+
+ var child = undefined,
+ callback = undefined,
+ detail = promise._result;
+
+ for (var i = 0; i < subscribers.length; i += 3) {
+ child = subscribers[i];
+ callback = subscribers[i + settled];
+
+ if (child) {
+ invokeCallback(settled, child, callback, detail);
+ } else {
+ callback(detail);
+ }
+ }
+
+ promise._subscribers.length = 0;
+}
+
+function ErrorObject() {
+ this.error = null;
+}
+
+var TRY_CATCH_ERROR = new ErrorObject();
+
+function tryCatch(callback, detail) {
+ try {
+ return callback(detail);
+ } catch (e) {
+ TRY_CATCH_ERROR.error = e;
+ return TRY_CATCH_ERROR;
+ }
+}
+
+function invokeCallback(settled, promise, callback, detail) {
+ var hasCallback = isFunction(callback),
+ value = undefined,
+ error = undefined,
+ succeeded = undefined,
+ failed = undefined;
+
+ if (hasCallback) {
+ value = tryCatch(callback, detail);
+
+ if (value === TRY_CATCH_ERROR) {
+ failed = true;
+ error = value.error;
+ value = null;
+ } else {
+ succeeded = true;
+ }
+
+ if (promise === value) {
+ _reject(promise, cannotReturnOwn());
+ return;
+ }
+ } else {
+ value = detail;
+ succeeded = true;
+ }
+
+ if (promise._state !== PENDING) {
+ // noop
+ } else if (hasCallback && succeeded) {
+ _resolve(promise, value);
+ } else if (failed) {
+ _reject(promise, error);
+ } else if (settled === FULFILLED) {
+ fulfill(promise, value);
+ } else if (settled === REJECTED) {
+ _reject(promise, value);
+ }
+}
+
+function initializePromise(promise, resolver) {
+ try {
+ resolver(function resolvePromise(value) {
+ _resolve(promise, value);
+ }, function rejectPromise(reason) {
+ _reject(promise, reason);
+ });
+ } catch (e) {
+ _reject(promise, e);
+ }
+}
+
+var id = 0;
+function nextId() {
+ return id++;
+}
+
+function makePromise(promise) {
+ promise[PROMISE_ID] = id++;
+ promise._state = undefined;
+ promise._result = undefined;
+ promise._subscribers = [];
+}
+
+function Enumerator(Constructor, input) {
+ this._instanceConstructor = Constructor;
+ this.promise = new Constructor(noop);
+
+ if (!this.promise[PROMISE_ID]) {
+ makePromise(this.promise);
+ }
+
+ if (isArray(input)) {
+ this._input = input;
+ this.length = input.length;
+ this._remaining = input.length;
+
+ this._result = new Array(this.length);
+
+ if (this.length === 0) {
+ fulfill(this.promise, this._result);
+ } else {
+ this.length = this.length || 0;
+ this._enumerate();
+ if (this._remaining === 0) {
+ fulfill(this.promise, this._result);
+ }
+ }
+ } else {
+ _reject(this.promise, validationError());
+ }
+}
+
+function validationError() {
+ return new Error('Array Methods must be provided an Array');
+};
+
+Enumerator.prototype._enumerate = function () {
+ var length = this.length;
+ var _input = this._input;
+
+ for (var i = 0; this._state === PENDING && i < length; i++) {
+ this._eachEntry(_input[i], i);
+ }
+};
+
+Enumerator.prototype._eachEntry = function (entry, i) {
+ var c = this._instanceConstructor;
+ var resolve$$ = c.resolve;
+
+ if (resolve$$ === resolve) {
+ var _then = getThen(entry);
+
+ if (_then === then && entry._state !== PENDING) {
+ this._settledAt(entry._state, i, entry._result);
+ } else if (typeof _then !== 'function') {
+ this._remaining--;
+ this._result[i] = entry;
+ } else if (c === Promise) {
+ var promise = new c(noop);
+ handleMaybeThenable(promise, entry, _then);
+ this._willSettleAt(promise, i);
+ } else {
+ this._willSettleAt(new c(function (resolve$$) {
+ return resolve$$(entry);
+ }), i);
+ }
+ } else {
+ this._willSettleAt(resolve$$(entry), i);
+ }
+};
+
+Enumerator.prototype._settledAt = function (state, i, value) {
+ var promise = this.promise;
+
+ if (promise._state === PENDING) {
+ this._remaining--;
+
+ if (state === REJECTED) {
+ _reject(promise, value);
+ } else {
+ this._result[i] = value;
+ }
+ }
+
+ if (this._remaining === 0) {
+ fulfill(promise, this._result);
+ }
+};
+
+Enumerator.prototype._willSettleAt = function (promise, i) {
+ var enumerator = this;
+
+ subscribe(promise, undefined, function (value) {
+ return enumerator._settledAt(FULFILLED, i, value);
+ }, function (reason) {
+ return enumerator._settledAt(REJECTED, i, reason);
+ });
+};
+
+/**
+ `Promise.all` accepts an array of promises, and returns a new promise which
+ is fulfilled with an array of fulfillment values for the passed promises, or
+ rejected with the reason of the first passed promise to be rejected. It casts all
+ elements of the passed iterable to promises as it runs this algorithm.
+
+ Example:
+
+ ```javascript
+ let promise1 = resolve(1);
+ let promise2 = resolve(2);
+ let promise3 = resolve(3);
+ let promises = [ promise1, promise2, promise3 ];
+
+ Promise.all(promises).then(function(array){
+ // The array here would be [ 1, 2, 3 ];
+ });
+ ```
+
+ If any of the `promises` given to `all` are rejected, the first promise
+ that is rejected will be given as an argument to the returned promises's
+ rejection handler. For example:
+
+ Example:
+
+ ```javascript
+ let promise1 = resolve(1);
+ let promise2 = reject(new Error("2"));
+ let promise3 = reject(new Error("3"));
+ let promises = [ promise1, promise2, promise3 ];
+
+ Promise.all(promises).then(function(array){
+ // Code here never runs because there are rejected promises!
+ }, function(error) {
+ // error.message === "2"
+ });
+ ```
+
+ @method all
+ @static
+ @param {Array} entries array of promises
+ @param {String} label optional string for labeling the promise.
+ Useful for tooling.
+ @return {Promise} promise that is fulfilled when all `promises` have been
+ fulfilled, or rejected if any of them become rejected.
+ @static
+*/
+function all(entries) {
+ return new Enumerator(this, entries).promise;
+}
+
+/**
+ `Promise.race` returns a new promise which is settled in the same way as the
+ first passed promise to settle.
+
+ Example:
+
+ ```javascript
+ let promise1 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve('promise 1');
+ }, 200);
+ });
+
+ let promise2 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve('promise 2');
+ }, 100);
+ });
+
+ Promise.race([promise1, promise2]).then(function(result){
+ // result === 'promise 2' because it was resolved before promise1
+ // was resolved.
+ });
+ ```
+
+ `Promise.race` is deterministic in that only the state of the first
+ settled promise matters. For example, even if other promises given to the
+ `promises` array argument are resolved, but the first settled promise has
+ become rejected before the other promises became fulfilled, the returned
+ promise will become rejected:
+
+ ```javascript
+ let promise1 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve('promise 1');
+ }, 200);
+ });
+
+ let promise2 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ reject(new Error('promise 2'));
+ }, 100);
+ });
+
+ Promise.race([promise1, promise2]).then(function(result){
+ // Code here never runs
+ }, function(reason){
+ // reason.message === 'promise 2' because promise 2 became rejected before
+ // promise 1 became fulfilled
+ });
+ ```
+
+ An example real-world use case is implementing timeouts:
+
+ ```javascript
+ Promise.race([ajax('foo.json'), timeout(5000)])
+ ```
+
+ @method race
+ @static
+ @param {Array} promises array of promises to observe
+ Useful for tooling.
+ @return {Promise} a promise which settles in the same way as the first passed
+ promise to settle.
+*/
+function race(entries) {
+ /*jshint validthis:true */
+ var Constructor = this;
+
+ if (!isArray(entries)) {
+ return new Constructor(function (_, reject) {
+ return reject(new TypeError('You must pass an array to race.'));
+ });
+ } else {
+ return new Constructor(function (resolve, reject) {
+ var length = entries.length;
+ for (var i = 0; i < length; i++) {
+ Constructor.resolve(entries[i]).then(resolve, reject);
+ }
+ });
+ }
+}
+
+/**
+ `Promise.reject` returns a promise rejected with the passed `reason`.
+ It is shorthand for the following:
+
+ ```javascript
+ let promise = new Promise(function(resolve, reject){
+ reject(new Error('WHOOPS'));
+ });
+
+ promise.then(function(value){
+ // Code here doesn't run because the promise is rejected!
+ }, function(reason){
+ // reason.message === 'WHOOPS'
+ });
+ ```
+
+ Instead of writing the above, your code now simply becomes the following:
+
+ ```javascript
+ let promise = Promise.reject(new Error('WHOOPS'));
+
+ promise.then(function(value){
+ // Code here doesn't run because the promise is rejected!
+ }, function(reason){
+ // reason.message === 'WHOOPS'
+ });
+ ```
+
+ @method reject
+ @static
+ @param {Any} reason value that the returned promise will be rejected with.
+ Useful for tooling.
+ @return {Promise} a promise rejected with the given `reason`.
+*/
+function reject(reason) {
+ /*jshint validthis:true */
+ var Constructor = this;
+ var promise = new Constructor(noop);
+ _reject(promise, reason);
+ return promise;
+}
+
+function needsResolver() {
+ throw new TypeError('You must pass a resolver function as the first argument to the promise constructor');
+}
+
+function needsNew() {
+ throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");
+}
+
+/**
+ Promise objects represent the eventual result of an asynchronous operation. The
+ primary way of interacting with a promise is through its `then` method, which
+ registers callbacks to receive either a promise's eventual value or the reason
+ why the promise cannot be fulfilled.
+
+ Terminology
+ -----------
+
+ - `promise` is an object or function with a `then` method whose behavior conforms to this specification.
+ - `thenable` is an object or function that defines a `then` method.
+ - `value` is any legal JavaScript value (including undefined, a thenable, or a promise).
+ - `exception` is a value that is thrown using the throw statement.
+ - `reason` is a value that indicates why a promise was rejected.
+ - `settled` the final resting state of a promise, fulfilled or rejected.
+
+ A promise can be in one of three states: pending, fulfilled, or rejected.
+
+ Promises that are fulfilled have a fulfillment value and are in the fulfilled
+ state. Promises that are rejected have a rejection reason and are in the
+ rejected state. A fulfillment value is never a thenable.
+
+ Promises can also be said to *resolve* a value. If this value is also a
+ promise, then the original promise's settled state will match the value's
+ settled state. So a promise that *resolves* a promise that rejects will
+ itself reject, and a promise that *resolves* a promise that fulfills will
+ itself fulfill.
+
+
+ Basic Usage:
+ ------------
+
+ ```js
+ let promise = new Promise(function(resolve, reject) {
+ // on success
+ resolve(value);
+
+ // on failure
+ reject(reason);
+ });
+
+ promise.then(function(value) {
+ // on fulfillment
+ }, function(reason) {
+ // on rejection
+ });
+ ```
+
+ Advanced Usage:
+ ---------------
+
+ Promises shine when abstracting away asynchronous interactions such as
+ `XMLHttpRequest`s.
+
+ ```js
+ function getJSON(url) {
+ return new Promise(function(resolve, reject){
+ let xhr = new XMLHttpRequest();
+
+ xhr.open('GET', url);
+ xhr.onreadystatechange = handler;
+ xhr.responseType = 'json';
+ xhr.setRequestHeader('Accept', 'application/json');
+ xhr.send();
+
+ function handler() {
+ if (this.readyState === this.DONE) {
+ if (this.status === 200) {
+ resolve(this.response);
+ } else {
+ reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']'));
+ }
+ }
+ };
+ });
+ }
+
+ getJSON('/posts.json').then(function(json) {
+ // on fulfillment
+ }, function(reason) {
+ // on rejection
+ });
+ ```
+
+ Unlike callbacks, promises are great composable primitives.
+
+ ```js
+ Promise.all([
+ getJSON('/posts'),
+ getJSON('/comments')
+ ]).then(function(values){
+ values[0] // => postsJSON
+ values[1] // => commentsJSON
+
+ return values;
+ });
+ ```
+
+ @class Promise
+ @param {function} resolver
+ Useful for tooling.
+ @constructor
+*/
+function Promise(resolver) {
+ this[PROMISE_ID] = nextId();
+ this._result = this._state = undefined;
+ this._subscribers = [];
+
+ if (noop !== resolver) {
+ typeof resolver !== 'function' && needsResolver();
+ this instanceof Promise ? initializePromise(this, resolver) : needsNew();
+ }
+}
+
+Promise.all = all;
+Promise.race = race;
+Promise.resolve = resolve;
+Promise.reject = reject;
+Promise._setScheduler = setScheduler;
+Promise._setAsap = setAsap;
+Promise._asap = asap;
+
+Promise.prototype = {
+ constructor: Promise,
+
+ /**
+ The primary way of interacting with a promise is through its `then` method,
+ which registers callbacks to receive either a promise's eventual value or the
+ reason why the promise cannot be fulfilled.
+
+ ```js
+ findUser().then(function(user){
+ // user is available
+ }, function(reason){
+ // user is unavailable, and you are given the reason why
+ });
+ ```
+
+ Chaining
+ --------
+
+ The return value of `then` is itself a promise. This second, 'downstream'
+ promise is resolved with the return value of the first promise's fulfillment
+ or rejection handler, or rejected if the handler throws an exception.
+
+ ```js
+ findUser().then(function (user) {
+ return user.name;
+ }, function (reason) {
+ return 'default name';
+ }).then(function (userName) {
+ // If `findUser` fulfilled, `userName` will be the user's name, otherwise it
+ // will be `'default name'`
+ });
+
+ findUser().then(function (user) {
+ throw new Error('Found user, but still unhappy');
+ }, function (reason) {
+ throw new Error('`findUser` rejected and we're unhappy');
+ }).then(function (value) {
+ // never reached
+ }, function (reason) {
+ // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'.
+ // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'.
+ });
+ ```
+ If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream.
+
+ ```js
+ findUser().then(function (user) {
+ throw new PedagogicalException('Upstream error');
+ }).then(function (value) {
+ // never reached
+ }).then(function (value) {
+ // never reached
+ }, function (reason) {
+ // The `PedgagocialException` is propagated all the way down to here
+ });
+ ```
+
+ Assimilation
+ ------------
+
+ Sometimes the value you want to propagate to a downstream promise can only be
+ retrieved asynchronously. This can be achieved by returning a promise in the
+ fulfillment or rejection handler. The downstream promise will then be pending
+ until the returned promise is settled. This is called *assimilation*.
+
+ ```js
+ findUser().then(function (user) {
+ return findCommentsByAuthor(user);
+ }).then(function (comments) {
+ // The user's comments are now available
+ });
+ ```
+
+ If the assimliated promise rejects, then the downstream promise will also reject.
+
+ ```js
+ findUser().then(function (user) {
+ return findCommentsByAuthor(user);
+ }).then(function (comments) {
+ // If `findCommentsByAuthor` fulfills, we'll have the value here
+ }, function (reason) {
+ // If `findCommentsByAuthor` rejects, we'll have the reason here
+ });
+ ```
+
+ Simple Example
+ --------------
+
+ Synchronous Example
+
+ ```javascript
+ let result;
+
+ try {
+ result = findResult();
+ // success
+ } catch(reason) {
+ // failure
+ }
+ ```
+
+ Errback Example
+
+ ```js
+ findResult(function(result, err){
+ if (err) {
+ // failure
+ } else {
+ // success
+ }
+ });
+ ```
+
+ Promise Example;
+
+ ```javascript
+ findResult().then(function(result){
+ // success
+ }, function(reason){
+ // failure
+ });
+ ```
+
+ Advanced Example
+ --------------
+
+ Synchronous Example
+
+ ```javascript
+ let author, books;
+
+ try {
+ author = findAuthor();
+ books = findBooksByAuthor(author);
+ // success
+ } catch(reason) {
+ // failure
+ }
+ ```
+
+ Errback Example
+
+ ```js
+
+ function foundBooks(books) {
+
+ }
+
+ function failure(reason) {
+
+ }
+
+ findAuthor(function(author, err){
+ if (err) {
+ failure(err);
+ // failure
+ } else {
+ try {
+ findBoooksByAuthor(author, function(books, err) {
+ if (err) {
+ failure(err);
+ } else {
+ try {
+ foundBooks(books);
+ } catch(reason) {
+ failure(reason);
+ }
+ }
+ });
+ } catch(error) {
+ failure(err);
+ }
+ // success
+ }
+ });
+ ```
+
+ Promise Example;
+
+ ```javascript
+ findAuthor().
+ then(findBooksByAuthor).
+ then(function(books){
+ // found books
+ }).catch(function(reason){
+ // something went wrong
+ });
+ ```
+
+ @method then
+ @param {Function} onFulfilled
+ @param {Function} onRejected
+ Useful for tooling.
+ @return {Promise}
+ */
+ then: then,
+
+ /**
+ `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same
+ as the catch block of a try/catch statement.
+
+ ```js
+ function findAuthor(){
+ throw new Error('couldn't find that author');
+ }
+
+ // synchronous
+ try {
+ findAuthor();
+ } catch(reason) {
+ // something went wrong
+ }
+
+ // async with promises
+ findAuthor().catch(function(reason){
+ // something went wrong
+ });
+ ```
+
+ @method catch
+ @param {Function} onRejection
+ Useful for tooling.
+ @return {Promise}
+ */
+ 'catch': function _catch(onRejection) {
+ return this.then(null, onRejection);
+ }
+};
+
+function polyfill() {
+ var local = undefined;
+
+ if (typeof global !== 'undefined') {
+ local = global;
+ } else if (typeof self !== 'undefined') {
+ local = self;
+ } else {
+ try {
+ local = Function('return this')();
+ } catch (e) {
+ throw new Error('polyfill failed because global object is unavailable in this environment');
+ }
+ }
+
+ var P = local.Promise;
+
+ if (P) {
+ var promiseToString = null;
+ try {
+ promiseToString = Object.prototype.toString.call(P.resolve());
+ } catch (e) {
+ // silently ignored
+ }
+
+ if (promiseToString === '[object Promise]' && !P.cast) {
+ return;
+ }
+ }
+
+ local.Promise = Promise;
+}
+
+// Strange compat..
+Promise.polyfill = polyfill;
+Promise.Promise = Promise;
+
+return Promise;
+
+})));
+
+ES6Promise.polyfill();
+//# sourceMappingURL=es6-promise.auto.map
\ No newline at end of file
diff --git a/src/js/test/TestRegister.js b/src/js/test/TestRegister.js
deleted file mode 100644
index 2e49d75a..00000000
--- a/src/js/test/TestRegister.js
+++ /dev/null
@@ -1,106 +0,0 @@
-/**
- * TestRegister.js
- *
- * This is so individual files can register their tests in one place, and
- * ensure that they will get run by the frontend.
- *
- * @author tlwr [toby@toby.codes
- *
- * @copyright Crown Copyright 2017
- * @license Apache-2.0
- *
- */
-(function() {
- /**
- * Add a list of tests to the register.
- *
- * @class
- */
- function TestRegister() {
- this.tests = [];
- }
-
- /**
- * Add a list of tests to the register.
- *
- * @param {Object[]} tests
- */
- TestRegister.prototype.addTests = function(tests) {
- this.tests = this.tests.concat(tests.map(function(test) {
-
- test.status = "waiting";
- test.output = "";
-
- return test;
- }));
- };
-
- /**
- * Returns the list of tests.
- *
- * @returns {Object[]} tests
- */
- TestRegister.prototype.getTests = function() {
- return this.tests;
- };
-
- /**
- * Runs all the tests in the register and updates the state of each test.
- *
- */
- TestRegister.prototype.runTests = function() {
- this.tests.forEach(function(test, i) {
- var chef = new Chef();
-
- // This resolve is to not instantly break when async operations are
- // supported. Marked as TODO.
- Promise.resolve(chef.bake(
- test.input,
- test.recipeConfig,
- {},
- 0,
- 0
- ))
- .then(function(result) {
- if (result.error) {
- if (test.expectedError) {
- test.status = "passing";
- } else {
- test.status = "erroring";
- test.output = [
- "Erroring",
- "-------",
- result.error.displayStr,
- ].join("\n");
- }
- } else {
- if (test.expectedError) {
- test.status = "failing";
- test.output = [
- "Failing",
- "-------",
- "Expected an error but did not receive one.",
- ].join("\n");
- } else if (result.result === test.expectedOutput) {
- test.status = "passing";
- } else {
- test.status = "failing";
- test.output = [
- "Failing",
- "-------",
- "Expected",
- "-------",
- test.expectedOutput,
- "Actual",
- "-------",
- result.result,
- ].join("\n");
- }
- }
- });
- });
- };
-
- // Singleton TestRegister, keeping things simple and obvious.
- window.TestRegister = new TestRegister();
-})();
diff --git a/src/js/test/views/html/main.js b/src/js/test/views/html/main.js
deleted file mode 100644
index aaf364fd..00000000
--- a/src/js/test/views/html/main.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * main.js
- *
- * Simple VueJS app for running all the tests and displaying some basic stats.
- * @author tlwr [toby@toby.codes]
- * @copyright Crown Copyright 2017
- * @license Apache-2.0
- *
- */
-(function() {
- Vue.component("test-status-icon", {
- template: "#test-status-icon-template",
- props: ["status"],
-
- methods: {
- getIcon: function() {
- var icons = {
- waiting: "⌚",
- loading: "⚡",
- passing: "✔️️",
- failing: "❌",
- erroring: "☠️",
- };
-
- return icons[this.status];
- }
- },
- });
-
- Vue.component("test-stats", {
- template: "#test-stats-template",
- props: ["tests"],
-
- methods: {
- countTestsWithStatus: function(status) {
- return this.tests.filter(function(test) {
- return test.status === status;
- }).length;
- },
- },
- });
-
- Vue.component("tests", {
- template: "#tests-template",
- props: ["tests"],
- });
-
- window.TestRunner = new Vue({
- el: "main",
- data: {
- tests: TestRegister.getTests(),
- },
-
- mounted: function() {
- TestRegister.runTests();
- },
- });
-})();
diff --git a/test/NodeRunner.js b/test/NodeRunner.js
new file mode 100644
index 00000000..e1c0d9dd
--- /dev/null
+++ b/test/NodeRunner.js
@@ -0,0 +1,24 @@
+/**
+ * NodeRunner.js
+ *
+ * The purpose of this file is to execute via PhantomJS the file
+ * PhantomRunner.js, because PhantomJS is managed by node.
+ *
+ * @author tlwr [toby@toby.codes
+ *
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ *
+ */
+var path = require("path");
+var phantomjs = require("phantomjs-prebuilt");
+
+var phantomEntryPoint = path.join(__dirname, "PhantomRunner.js");
+var program = phantomjs.exec(phantomEntryPoint);
+
+program.stdout.pipe(process.stdout);
+program.stderr.pipe(process.stderr);
+
+program.on("exit", function(status) {
+ process.exit(status);
+});
diff --git a/test/PhantomRunner.js b/test/PhantomRunner.js
new file mode 100644
index 00000000..26ce0f2c
--- /dev/null
+++ b/test/PhantomRunner.js
@@ -0,0 +1,80 @@
+/**
+ * PhantomRunner.js
+ *
+ * This file navigates to build/test/index.html and logs the test results.
+ *
+ * @author tlwr [toby@toby.codes
+ *
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ *
+ */
+var page = require("webpage").create();
+
+var allTestsPassing = true;
+var testStatusCounts = {
+ total: 0,
+};
+
+function statusToIcon(status) {
+ var icons = {
+ erroring: "🔥",
+ failing: "❌",
+ passing: "✔️️",
+ };
+ return icons[status] || "?";
+}
+
+page.onCallback = function(messageType) {
+ if (messageType === "testResult") {
+ var testResult = arguments[1];
+
+ allTestsPassing = allTestsPassing && testResult.status === "passing";
+ var newCount = (testStatusCounts[testResult.status] || 0) + 1;
+ testStatusCounts[testResult.status] = newCount;
+ testStatusCounts.total += 1;
+
+ console.log([
+ statusToIcon(testResult.status),
+ testResult.test.name
+ ].join(" "));
+
+ if (testResult.output) {
+ console.log(
+ testResult.output
+ .trim()
+ .replace(/^/, "\t")
+ .replace(/\n/g, "\n\t")
+ );
+ }
+ } else if (messageType === "exit") {
+
+ console.log("\n");
+
+ for (var testStatus in testStatusCounts) {
+ var count = testStatusCounts[testStatus];
+ if (count > 0) {
+ console.log(testStatus.toUpperCase(), count);
+ }
+ }
+
+ if (!allTestsPassing) {
+ console.log("\n")
+ console.log("Not all tests are passing");
+ }
+
+ phantom.exit(allTestsPassing ? 0 : 1);
+ }
+};
+
+page.open("file:///home/toby/Code/CyberChef/build/test/index.html", function(status) {
+ if (status !== "success") {
+ console.log("STATUS", status);
+ phantom.exit(1);
+ }
+});
+
+setTimeout(function() {
+ // Timeout
+ phantom.exit(1);
+}, 10 * 1000);
diff --git a/test/TestRegister.js b/test/TestRegister.js
new file mode 100644
index 00000000..e3ecbcb3
--- /dev/null
+++ b/test/TestRegister.js
@@ -0,0 +1,96 @@
+/**
+ * TestRegister.js
+ *
+ * This is so individual files can register their tests in one place, and
+ * ensure that they will get run by the frontend.
+ *
+ * @author tlwr [toby@toby.codes
+ *
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ *
+ */
+(function() {
+ /**
+ * Add a list of tests to the register.
+ *
+ * @class
+ */
+ function TestRegister() {
+ this.tests = [];
+ }
+
+ /**
+ * Add a list of tests to the register.
+ *
+ * @param {Object[]} tests
+ */
+ TestRegister.prototype.addTests = function(tests) {
+ this.tests = this.tests.concat(tests);
+ };
+
+ /**
+ * Returns the list of tests.
+ *
+ * @returns {Object[]} tests
+ */
+ TestRegister.prototype.getTests = function() {
+ return this.tests;
+ };
+
+ /**
+ * Runs all the tests in the register.
+ *
+ */
+ TestRegister.prototype.runTests = function() {
+ return Promise.all(
+ this.tests.map(function(test, i) {
+ var chef = new Chef();
+
+ return Promise.resolve(chef.bake(
+ test.input,
+ test.recipeConfig,
+ {},
+ 0,
+ 0
+ ))
+ .then(function(result) {
+ var ret = {
+ test: test,
+ status: null,
+ output: null,
+ };
+
+ if (result.error) {
+ if (test.expectedError) {
+ ret.status = "passing";
+ } else {
+ ret.status = "erroring";
+ ret.output = result.error.displayStr;
+ }
+ } else {
+ if (test.expectedError) {
+ ret.status = "failing";
+ ret.output = "Expected an error but did not receive one.";
+ } else if (result.result === test.expectedOutput) {
+ ret.status = "passing";
+ } else {
+ ret.status = "failing";
+ ret.output = [
+ "Expected",
+ "\t" + test.expectedOutput.replace(/\n/g, "\n\t"),
+ "Received",
+ "\t" + result.result.replace(/\n/g, "\n\t"),
+ ].join("\n");
+ }
+ }
+
+ return ret;
+ });
+ })
+ );
+ };
+
+ // Singleton TestRegister, keeping things simple and obvious.
+ window.TestRegister = new TestRegister();
+})();
diff --git a/test/TestRunner.js b/test/TestRunner.js
new file mode 100644
index 00000000..5984f731
--- /dev/null
+++ b/test/TestRunner.js
@@ -0,0 +1,38 @@
+/**
+ * TestRunner.js
+ *
+ * This is for actually running the tests in the test register.
+ *
+ * @author tlwr [toby@toby.codes
+ *
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ *
+ */
+(function() {
+ document.addEventListener("DOMContentLoaded", function() {
+ TestRegister.runTests()
+ .then(function(results) {
+ results.forEach(function(testResult) {
+ if (typeof window.callPhantom === "function") {
+ window.callPhantom(
+ "testResult",
+ testResult
+ );
+ } else {
+ var output = [
+ "----------",
+ testResult.test.name,
+ testResult.status,
+ testResult.output,
+ ].join("
");
+ document.body.innerHTML += "" + output + "
";
+ }
+ });
+
+ if (typeof window.callPhantom === "function") {
+ window.callPhantom("exit");
+ }
+ });
+ });
+})();
diff --git a/src/js/test/tests/core.js b/test/tests/core.js
similarity index 52%
rename from src/js/test/tests/core.js
rename to test/tests/core.js
index 28ec60a3..fe596cb0 100644
--- a/src/js/test/tests/core.js
+++ b/test/tests/core.js
@@ -6,47 +6,47 @@
*
*/
TestRegister.addTests([
- {
- name: "Example error",
- input: "1\n2\na\n4",
- expectedOutput: "1\n2\n3\n4",
- recipeConfig: [
- {
- op: "Fork",
- args: ["\n", "\n", false],
- },
- {
- op: "To Base",
- args: [16],
- },
- ],
- },
- {
- name: "Example non-error when error was expected",
- input: "1",
- expectedError: true,
- recipeConfig: [
- {
- op: "To Base",
- args: [16],
- },
- ],
- },
- {
- name: "Example fail",
- input: "1\n2\na\n4",
- expectedOutput: "1\n2\n3\n4",
- recipeConfig: [
- {
- op: "Fork",
- args: ["\n", "\n", true],
- },
- {
- op: "To Base",
- args: [16],
- },
- ],
- },
+ //{
+ // name: "Example error",
+ // input: "1\n2\na\n4",
+ // expectedOutput: "1\n2\n3\n4",
+ // recipeConfig: [
+ // {
+ // op: "Fork",
+ // args: ["\n", "\n", false],
+ // },
+ // {
+ // op: "To Base",
+ // args: [16],
+ // },
+ // ],
+ //},
+ //{
+ // name: "Example non-error when error was expected",
+ // input: "1",
+ // expectedError: true,
+ // recipeConfig: [
+ // {
+ // op: "To Base",
+ // args: [16],
+ // },
+ // ],
+ //},
+ //{
+ // name: "Example fail",
+ // input: "1\n2\na\n4",
+ // expectedOutput: "1\n2\n3\n4",
+ // recipeConfig: [
+ // {
+ // op: "Fork",
+ // args: ["\n", "\n", true],
+ // },
+ // {
+ // op: "To Base",
+ // args: [16],
+ // },
+ // ],
+ //},
{
name: "Fork: nothing",
input: "",
diff --git a/src/js/test/tests/operations/Base58.js b/test/tests/operations/Base58.js
similarity index 100%
rename from src/js/test/tests/operations/Base58.js
rename to test/tests/operations/Base58.js
diff --git a/src/js/test/tests/operations/MorseCode.js b/test/tests/operations/MorseCode.js
similarity index 100%
rename from src/js/test/tests/operations/MorseCode.js
rename to test/tests/operations/MorseCode.js