diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index ea1b26dd80..16b213eacb 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -589,7 +589,9 @@ Menu::Menu() { #endif - + // Developer >> Tests >>> + MenuWrapper* testMenu = developerMenu->addMenu("Tests"); + addActionToQMenuAndActionHash(testMenu, MenuOption::RunClientScriptTests, 0, dialogsManager.data(), SLOT(showTestingResults())); // Developer > Timing >>> MenuWrapper* timingMenu = developerMenu->addMenu("Timing"); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index ad11b91796..8998e616e8 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -172,6 +172,7 @@ namespace MenuOption { const QString ResetAvatarSize = "Reset Avatar Size"; const QString ResetSensors = "Reset Sensors"; const QString RunningScripts = "Running Scripts..."; + const QString RunClientScriptTests = "Run Client Script Tests"; const QString RunTimingTests = "Run Timing Tests"; const QString ScriptEditor = "Script Editor..."; const QString ScriptedMotorControl = "Enable Scripted Motor Control"; diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 679fb7f59d..03c71d8573 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -158,6 +158,15 @@ void DialogsManager::showScriptEditor() { _scriptEditor->raise(); } +void DialogsManager::showTestingResults() { + if (!_testingDialog) { + _testingDialog = new TestingDialog(qApp->getWindow()); + connect(_testingDialog, SIGNAL(closed()), _testingDialog, SLOT(deleteLater())); + } + _testingDialog->show(); + _testingDialog->raise(); +} + void DialogsManager::showDomainConnectionDialog() { // if the dialog already exists we delete it so the connection data is refreshed if (_domainConnectionDialog) { diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index e89bc43020..c02c1fc2c3 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -17,6 +17,7 @@ #include #include "HMDToolsDialog.h" +#include "TestingDialog.h" class AnimationsDialog; class AttachmentsDialog; @@ -26,6 +27,7 @@ class DiskCacheEditor; class LodToolsDialog; class OctreeStatsDialog; class ScriptEditorWindow; +class TestingDialog; class QMessageBox; class DomainConnectionDialog; @@ -38,6 +40,7 @@ public: QPointer getHMDToolsDialog() const { return _hmdToolsDialog; } QPointer getLodToolsDialog() const { return _lodToolsDialog; } QPointer getOctreeStatsDialog() const { return _octreeStatsDialog; } + QPointer getTestingDialog() const { return _testingDialog; } void emitAddressBarShown(bool visible) { emit addressBarShown(visible); } public slots: @@ -55,6 +58,7 @@ public slots: void hmdTools(bool showTools); void showScriptEditor(); void showDomainConnectionDialog(); + void showTestingResults(); // Application Update void showUpdateDialog(); @@ -83,6 +87,7 @@ private: QPointer _lodToolsDialog; QPointer _octreeStatsDialog; QPointer _scriptEditor; + QPointer _testingDialog; QPointer _domainConnectionDialog; }; diff --git a/interface/src/ui/TestingDialog.cpp b/interface/src/ui/TestingDialog.cpp new file mode 100644 index 0000000000..f55eb63a5b --- /dev/null +++ b/interface/src/ui/TestingDialog.cpp @@ -0,0 +1,35 @@ +// +// TestingDialog.cpp +// interface/src/ui +// +// Created by Ryan Jones on 12/3/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ScriptEngines.h" + +#include "ui/TestingDialog.h" +#include "Application.h" + +TestingDialog::TestingDialog(QWidget* parent) : + QDialog(parent, Qt::Window | Qt::WindowCloseButtonHint | Qt::WindowStaysOnTopHint), + _console(new JSConsole(this)) +{ + setAttribute(Qt::WA_DeleteOnClose); + setWindowTitle(windowLabel); + + _console->setFixedHeight(TESTING_CONSOLE_HEIGHT); + + auto _engines = DependencyManager::get(); + _engine = _engines->loadScript(qApp->applicationDirPath() + testRunnerRelativePath); + _console->setScriptEngine(_engine); + connect(_engine, &ScriptEngine::finished, this, &TestingDialog::onTestingFinished); +} + +void TestingDialog::onTestingFinished(const QString& scriptPath) { + _engine = nullptr; + _console->setScriptEngine(nullptr); +} diff --git a/interface/src/ui/TestingDialog.h b/interface/src/ui/TestingDialog.h new file mode 100644 index 0000000000..b90b8f2e99 --- /dev/null +++ b/interface/src/ui/TestingDialog.h @@ -0,0 +1,35 @@ +// +// TestingDialog.h +// interface/src/ui +// +// Created by Ryan Jones on 12/3/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_TestingDialog_h +#define hifi_TestingDialog_h + +#include +#include "ScriptEngine.h" +#include "JSConsole.h" + +const QString windowLabel = "Client Script Tests"; +const QString testRunnerRelativePath = "/scripts/developer/tests/unit_tests/testRunner.js"; +const unsigned int TESTING_CONSOLE_HEIGHT = 400; + +class TestingDialog : public QDialog { + Q_OBJECT +public: + TestingDialog(QWidget* parent); + + void onTestingFinished(const QString& scriptPath); + +private: + std::unique_ptr _console; + ScriptEngine* _engine; +}; + +#endif diff --git a/scripts/developer/libraries/jasmine/hifi-boot.js b/scripts/developer/libraries/jasmine/hifi-boot.js index 0c66a925af..f490a3618f 100644 --- a/scripts/developer/libraries/jasmine/hifi-boot.js +++ b/scripts/developer/libraries/jasmine/hifi-boot.js @@ -1,27 +1,50 @@ - (function() { + var BALLOT_X = '✗'; + var CHECKMARK = '✓'; + var DOWN_RIGHT_ARROW = '↳'; + var PASSED = 'passed'; + var lastSpecStartTime; function ConsoleReporter(options) { + var startTime = new Date().getTime(); + var errorCount = 0; this.jasmineStarted = function (obj) { - print("jasmineStarted: numSpecs = " + obj.totalSpecsDefined); + print('Jasmine started with ' + obj.totalSpecsDefined + ' tests.'); }; this.jasmineDone = function (obj) { - print("jasmineDone"); + var ERROR = errorCount === 1 ? 'error' : 'errors'; + var endTime = new Date().getTime(); + print('
'); + if (errorCount === 0) { + print ('All tests passed!'); + } else { + print('Tests completed with ' + + errorCount + ' ' + ERROR + '.'); + } + print('Tests completed in ' + (endTime - startTime) + 'ms.'); }; this.suiteStarted = function(obj) { - print("suiteStarted: \"" + obj.fullName + "\""); + print(obj.fullName); }; this.suiteDone = function(obj) { - print("suiteDone: \"" + obj.fullName + "\" " + obj.status); + print(''); }; this.specStarted = function(obj) { - print("specStarted: \"" + obj.fullName + "\""); + lastSpecStartTime = new Date().getTime(); }; this.specDone = function(obj) { - print("specDone: \"" + obj.fullName + "\" " + obj.status); + var specEndTime = new Date().getTime(); + var symbol = obj.status === PASSED ? + '' + CHECKMARK + '' : + '' + BALLOT_X + ''; + print('... ' + obj.fullName + ' ' + symbol + ' ' + '[' + + (specEndTime - lastSpecStartTime) + 'ms]'); - var i, l = obj.failedExpectations.length; - for (i = 0; i < l; i++) { - print(" " + obj.failedExpectations[i].message); + var specErrors = obj.failedExpectations.length; + errorCount += specErrors; + for (var i = 0; i < specErrors; i++) { + print('' + DOWN_RIGHT_ARROW + + ' ' + + obj.failedExpectations[i].message + ''); } }; return this; @@ -44,10 +67,11 @@ function extend(destination, source) { for (var property in source) { - destination[property] = source[property]; + if (source.hasOwnProperty(property)) { + destination[property] = source[property]; + } } return destination; } - }()); diff --git a/scripts/developer/tests/avatarUnitTests.js b/scripts/developer/tests/unit_tests/avatarUnitTests.js similarity index 93% rename from scripts/developer/tests/avatarUnitTests.js rename to scripts/developer/tests/unit_tests/avatarUnitTests.js index 29e3ad0588..7032b5f5e6 100644 --- a/scripts/developer/tests/avatarUnitTests.js +++ b/scripts/developer/tests/unit_tests/avatarUnitTests.js @@ -1,7 +1,4 @@ -Script.include("../libraries/jasmine/jasmine.js"); -Script.include("../libraries/jasmine/hifi-boot.js"); - // Art3mis var DEFAULT_AVATAR_URL = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/contents/e76946cc-c272-4adf-9bb6-02cde0a4b57d/8fd984ea6fe1495147a3303f87fa6e23.fst?1460131758"; @@ -55,5 +52,3 @@ describe("MyAvatar", function () { }); -jasmine.getEnv().execute(); - diff --git a/scripts/developer/tests/bindUnitTest.js b/scripts/developer/tests/unit_tests/bindUnitTest.js similarity index 61% rename from scripts/developer/tests/bindUnitTest.js rename to scripts/developer/tests/unit_tests/bindUnitTest.js index 95fd497916..609487a30b 100644 --- a/scripts/developer/tests/bindUnitTest.js +++ b/scripts/developer/tests/unit_tests/bindUnitTest.js @@ -1,9 +1,12 @@ -Script.include('../libraries/jasmine/jasmine.js'); -Script.include('../libraries/jasmine/hifi-boot.js'); -Script.include('../../system/libraries/utils.js'); +Script.include('../../../system/libraries/utils.js'); describe('Bind', function() { - it('functions should have bind available', function() { + it('exists for functions', function() { + var FUNC = 'function'; + expect(typeof(function() {}.bind)).toEqual(FUNC); + }); + + it('should allow for setting context of this', function() { var foo = 'bar'; function callAnotherFn(anotherFn) { @@ -22,10 +25,6 @@ describe('Bind', function() { var instance = new TestConstructor(); - expect(typeof(instance.doSomething.bind) !== 'undefined'); expect(instance.doSomething()).toEqual(foo); }); }); - -jasmine.getEnv().execute(); -Script.stop(); \ No newline at end of file diff --git a/scripts/developer/tests/unit_tests/entityUnitTests.js b/scripts/developer/tests/unit_tests/entityUnitTests.js new file mode 100644 index 0000000000..ad1606c4e9 --- /dev/null +++ b/scripts/developer/tests/unit_tests/entityUnitTests.js @@ -0,0 +1,53 @@ +describe('Entity', function() { + var center = Vec3.sum( + MyAvatar.position, + Vec3.multiply(3, Quat.getFront(Camera.getOrientation())) + ); + var boxEntity; + var boxProps = { + type: 'Box', + color: { + red: 255, + green: 255, + blue: 255, + }, + position: center, + dimensions: { + x: 1, + y: 1, + z: 1, + }, + }; + + beforeEach(function() { + boxEntity = Entities.addEntity(boxProps); + }); + + afterEach(function() { + Entities.deleteEntity(boxEntity); + boxEntity = null; + }); + + it('can be added programmatically', function() { + expect(typeof(boxEntity)).toEqual('string'); + }); + + it('instantiates itself correctly', function() { + var props = Entities.getEntityProperties(boxEntity); + expect(props.type).toEqual(boxProps.type); + }); + + it('can be modified after creation', function() { + var newPos = { + x: boxProps.position.x, + y: boxProps.position.y, + z: boxProps.position.z + 1.0, + }; + Entities.editEntity(boxEntity, { + position: newPos, + }); + + var props = Entities.getEntityProperties(boxEntity); + expect(Math.round(props.position.z)).toEqual(Math.round(newPos.z)); + }); +}); \ No newline at end of file diff --git a/scripts/developer/tests/unit_tests/testRunner.js b/scripts/developer/tests/unit_tests/testRunner.js new file mode 100644 index 0000000000..31d83cd986 --- /dev/null +++ b/scripts/developer/tests/unit_tests/testRunner.js @@ -0,0 +1,13 @@ +// Include testing library +Script.include('../../libraries/jasmine/jasmine.js'); +Script.include('../../libraries/jasmine/hifi-boot.js') + +// Include unit tests +// FIXME: Figure out why jasmine done() is not working. +// Script.include('avatarUnitTests.js'); +Script.include('bindUnitTest.js'); +Script.include('entityUnitTests.js'); + +// Run the tests +jasmine.getEnv().execute(); +Script.stop();