[FEATURE] Introduce UnitTests for JavaScript 57/48557/10
authorFrank Naegler <frank.naegler@typo3.org>
Fri, 10 Jun 2016 16:14:13 +0000 (18:14 +0200)
committerSusanne Moog <typo3@susannemoog.de>
Mon, 20 Jun 2016 08:13:40 +0000 (10:13 +0200)
This patch introduce UnitTests for JavaSscript.
It uses karma as test runner and jasmine as testing framework.
The tests running on travis with PhantomJS. Locally other browsers like Chrome,
Firefox, Safari or IE can be used as well.

Resolves: #76590
Releases: master
Change-Id: I171ed5f50943f8c30d71c7035b86814cf9cbcbbe
Reviewed-on: https://review.typo3.org/48557
Reviewed-by: Andreas Fernandez <typo3@scripting-base.de>
Tested-by: Andreas Fernandez <typo3@scripting-base.de>
Reviewed-by: Susanne Moog <typo3@susannemoog.de>
Tested-by: Susanne Moog <typo3@susannemoog.de>
.travis.yml
Build/package.json
typo3/sysext/backend/Tests/JavaScript/FormEngineValidationTest.js [new file with mode: 0644]
typo3/sysext/backend/Tests/JavaScript/GridEditorTest.js [new file with mode: 0644]
typo3/sysext/backend/Tests/JavaScript/IconsTest.js [new file with mode: 0644]
typo3/sysext/core/Build/Configuration/JSUnit/Bootstrap.js [new file with mode: 0644]
typo3/sysext/core/Build/Configuration/JSUnit/Helper.js [new file with mode: 0644]
typo3/sysext/core/Build/Configuration/JSUnit/karma.conf.js [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-76590-IntroduceUnitTestsForJavaScript.rst [new file with mode: 0644]

index 88be753..d412fa8 100644 (file)
@@ -1,11 +1,14 @@
 language: php
 
+node_js:
+    - "0.10"
+
 matrix:
   fast_finish: true
 
   include:
     - php: 7
-      env: UNIT_TESTS=yes FUNCTIONAL_TESTS=yes ACCEPTANCE_TESTS=no PHP_LINT=yes XLF_CHECK=yes SUBMODULE_TEST=yes  EXCEPTIONCODE_TEST=yes
+      env: UNIT_TESTS=yes FUNCTIONAL_TESTS=yes ACCEPTANCE_TESTS=no JSUNIT_TESTS=yes PHP_LINT=yes XLF_CHECK=yes SUBMODULE_TEST=yes  EXCEPTIONCODE_TEST=yes
 
 sudo: false
 
@@ -61,6 +64,10 @@ before_script:
   - export typo3DatabaseUsername="root"
   - export typo3DatabasePassword=""
 
+before_install:
+    - export DISPLAY=:99.0
+    - sh -e /etc/init.d/xvfb start
+
 script:
   - >
     if [[ "$UNIT_TESTS" == "yes" ]]; then
@@ -69,35 +76,41 @@ script:
 
   - >
     if [[ "$FUNCTIONAL_TESTS" == "yes" ]]; then
-        find . -wholename '*typo3/sysext/*/Tests/Functional/*Test.php' | parallel --jobs 6 --gnu 'echo; echo "Running functional test suite {}"; ./bin/phpunit -c typo3/sysext/core/Build/FunctionalTests.xml {}'
+      find . -wholename '*typo3/sysext/*/Tests/Functional/*Test.php' | parallel --jobs 6 --gnu 'echo; echo "Running functional test suite {}"; ./bin/phpunit -c typo3/sysext/core/Build/FunctionalTests.xml {}'
     fi
 
   - >
     if [[ "$ACCEPTANCE_TESTS" == "yes" ]]; then
-        ./bin/codecept run Acceptance -c typo3/sysext/core/Build/AcceptanceTests.yml --debug
+      ./bin/codecept run Acceptance -c typo3/sysext/core/Build/AcceptanceTests.yml --debug
+    fi
+
+  - >
+    if [[ "$JSUNIT_TESTS" == "yes" ]]; then
+      cd Build && npm install && cd ..
+      ./Build/node_modules/karma/bin/karma start typo3/sysext/core/Build/Configuration/JSUnit/karma.conf.js --single-run
     fi
 
   - >
     if [[ "$PHP_LINT" == "yes" ]]; then
-        find typo3/ -name \*.php -not -path "vendor/*" | parallel --jobs 6 --gnu php -d display_errors=stderr -l {} > /dev/null \;
+      find typo3/ -name \*.php -not -path "vendor/*" | parallel --jobs 6 --gnu php -d display_errors=stderr -l {} > /dev/null \;
     fi
 
   - >
     if [[ "$XLF_CHECK" == "yes" ]]; then
-        ./typo3/sysext/core/Build/Scripts/xlfcheck.sh
+      ./typo3/sysext/core/Build/Scripts/xlfcheck.sh
     fi
 
   - >
-      if [[ "$SUBMODULE_TEST" == "yes" ]]; then
-          /bin/bash -c "
-              if [[ `git submodule status 2>&1 | wc -l` -ne 0 ]]; then
-                  echo \"Found a submodule definition in repository\";
-                  exit 99;
-              fi
-          "
-      fi
+    if [[ "$SUBMODULE_TEST" == "yes" ]]; then
+      /bin/bash -c "
+        if [[ `git submodule status 2>&1 | wc -l` -ne 0 ]]; then
+          echo \"Found a submodule definition in repository\";
+          exit 99;
+        fi
+      "
+    fi
 
   - >
     if [[ "$EXCEPTIONCODE_TEST" == "yes" ]]; then
-        ./typo3/sysext/core/Build/Scripts/duplicateExceptionCodeCheck.sh
+      ./typo3/sysext/core/Build/Scripts/duplicateExceptionCodeCheck.sh
     fi
index fcaf479..d3c12ed 100644 (file)
     "grunt-contrib-less": "~1.0.0",
     "grunt-contrib-uglify": "0.9.1",
     "grunt-contrib-watch": "~0.6.1",
+    "grunt-copy": "*",
     "grunt-npm-install": "^0.2.0",
     "grunt-postcss": "^0.7.1",
     "grunt-svgmin": "2.0.1",
-    "grunt-copy": "*"
+    "jasmine-core": "^2.4.1",
+    "karma": "^0.13.22",
+    "karma-chrome-launcher": "^1.0.1",
+    "karma-firefox-launcher": "^1.0.0",
+    "karma-ie-launcher": "^1.0.0",
+    "karma-jasmine": "^1.0.2",
+    "karma-opera-launcher": "^1.0.0",
+    "karma-phantomjs-launcher": "^1.0.0",
+    "karma-requirejs": "^1.0.0",
+    "karma-safari-launcher": "^1.0.0",
+    "phantomjs-prebuilt": "^2.1.7",
+    "requirejs": "^2.2.0"
   }
 }
diff --git a/typo3/sysext/backend/Tests/JavaScript/FormEngineValidationTest.js b/typo3/sysext/backend/Tests/JavaScript/FormEngineValidationTest.js
new file mode 100644 (file)
index 0000000..9a30349
--- /dev/null
@@ -0,0 +1,315 @@
+define(['jquery', 'TYPO3/CMS/Backend/FormEngineValidation'], function($, FormEngineValidation) {
+       'use strict';
+
+       describe('TYPO3/CMS/Backend/FormEngineValidationTest:', function() {
+               /**
+                * @type {*[]}
+                */
+               var formatValueDataProvider = [
+                       {
+                               'description': 'works for type date',
+                               'type': 'date',
+                               'value': 0,
+                               'config': [],
+                               'result': ''
+                       },
+                       {
+                               'description': 'works for type date with timestamp',
+                               'type': 'date',
+                               'value': 10000000,
+                               'config': [],
+                               'result': '26-4-1970'
+                       },
+                       {
+                               'description': 'works for type datetime',
+                               'type': 'datetime',
+                               'value': 0,
+                               'config': [],
+                               'result': ''
+                       },
+                       {
+                               'description': 'works for type datetime with timestamp',
+                               'type': 'datetime',
+                               'value': 10000000,
+                               'config': [],
+                               'result': '17:46 26-4-1970'
+                       },
+                       {
+                               'description': 'works for type time',
+                               'type': 'time',
+                               'value': 0,
+                               'config': [],
+                               'result': '0:00'
+                       },
+                       {
+                               'description': 'works for type time with timestamp',
+                               'type': 'time',
+                               'value': 10000000,
+                               'config': [],
+                               'result': '17:46'
+                       }
+               ];
+
+               /**
+                * @dataProvider formatValueDataProvider
+                * @test
+                */
+               describe('tests for formatValue', function() {
+                       using(formatValueDataProvider, function(testCase) {
+                               it(testCase.description, function() {
+                                       var result = FormEngineValidation.formatValue(testCase.type, testCase.value, testCase.config);
+                                       expect(result).toBe(testCase.result);
+                               });
+                       });
+               });
+
+               /**
+                * @type {*[]}
+                */
+               var processValueDataProvider = [
+                       {
+                               'description': 'works for command alpha with numeric value',
+                               'command': 'alpha',
+                               'value': '1234',
+                               'config': [],
+                               'result': ''
+                       },
+                       {
+                               'description': 'works for command alpha with string value',
+                               'command': 'alpha',
+                               'value': 'abc',
+                               'config': [],
+                               'result': 'abc'
+                       },
+                       {
+                               'description': 'works for command alpha with alphanum input',
+                               'command': 'alpha',
+                               'value': 'abc123',
+                               'config': [],
+                               'result': 'abc'
+                       },
+                       {
+                               'description': 'works for command alpha with alphanum input',
+                               'command': 'alpha',
+                               'value': '123abc123',
+                               'config': [],
+                               'result': 'abc'
+                       }
+               ];
+
+               /**
+                * @dataProvider processValueDataProvider
+                * @test
+                */
+               describe('test for processValue', function() {
+                       using(processValueDataProvider, function(testCase) {
+                               it(testCase.description, function() {
+                                       var result = FormEngineValidation.processValue(testCase.command, testCase.value, testCase.config);
+                                       expect(result).toBe(testCase.result);
+                               });
+                       });
+               });
+
+               /**
+                * @test
+                */
+               xdescribe('tests for validateField');
+
+               /**
+                * @test
+                */
+               describe('tests for trimExplode', function () {
+                       it('works for comma as separator and list without spaces', function () {
+                               expect(FormEngineValidation.trimExplode(',', 'foo,bar,baz')).toEqual(['foo', 'bar', 'baz']);
+                       });
+                       it('works for comma as separator and list with spaces', function () {
+                               expect(FormEngineValidation.trimExplode(',', ' foo , bar , baz ')).toEqual(['foo', 'bar', 'baz']);
+                       });
+                       it('works for pipe as separator and list with spaces', function () {
+                               expect(FormEngineValidation.trimExplode('|', ' foo | bar | baz ')).toEqual(['foo', 'bar', 'baz']);
+                       });
+               });
+
+               /**
+                * @test
+                */
+               describe('tests for parseInt', function() {
+                       it('works for value 0', function () {
+                               expect(FormEngineValidation.parseInt(0)).toBe(0);
+                       });
+                       it('works for value 1', function () {
+                               expect(FormEngineValidation.parseInt(1)).toBe(1);
+                       });
+                       it('works for value -1', function () {
+                               expect(FormEngineValidation.parseInt(-1)).toBe(-1);
+                       });
+                       it('works for value "0"', function () {
+                               expect(FormEngineValidation.parseInt('0')).toBe(0);
+                       });
+                       it('works for value "1"', function () {
+                               expect(FormEngineValidation.parseInt('1')).toBe(1);
+                       });
+                       it('works for value "-1"', function () {
+                               expect(FormEngineValidation.parseInt('-1')).toBe(-1);
+                       });
+                       it('works for value 0.5', function () {
+                               expect(FormEngineValidation.parseInt(0.5)).toBe(0);
+                       });
+                       it('works for value "0.5"', function () {
+                               expect(FormEngineValidation.parseInt('0.5')).toBe(0);
+                       });
+                       it('works for value "foo"', function () {
+                               expect(FormEngineValidation.parseInt('foo')).toBe(0);
+                       });
+                       it('works for value true', function () {
+                               expect(FormEngineValidation.parseInt(true)).toBe(0);
+                       });
+                       it('works for value false', function () {
+                               expect(FormEngineValidation.parseInt(false)).toBe(0);
+                       });
+                       it('works for value null', function () {
+                               expect(FormEngineValidation.parseInt(null)).toBe(0);
+                       });
+               });
+               /**
+                * @test
+                */
+               describe('tests for parseDouble', function() {
+                       it('works for value 0', function () {
+                               expect(FormEngineValidation.parseDouble(0)).toBe('0.00');
+                       });
+                       it('works for value 1', function () {
+                               expect(FormEngineValidation.parseDouble(1)).toBe('1.00');
+                       });
+                       it('works for value -1', function () {
+                               expect(FormEngineValidation.parseDouble(-1)).toBe('-1.00');
+                       });
+                       it('works for value "0"', function () {
+                               expect(FormEngineValidation.parseDouble('0')).toBe('0.00');
+                       });
+                       it('works for value "1"', function () {
+                               expect(FormEngineValidation.parseDouble('1')).toBe('1.00');
+                       });
+                       it('works for value "-1"', function () {
+                               expect(FormEngineValidation.parseDouble('-1')).toBe('-1.00');
+                       });
+                       it('works for value 0.5', function () {
+                               expect(FormEngineValidation.parseDouble(0.5)).toBe('0.50');
+                       });
+                       it('works for value "0.5"', function () {
+                               expect(FormEngineValidation.parseDouble('0.5')).toBe('0.50');
+                       });
+                       it('works for value "foo"', function () {
+                               expect(FormEngineValidation.parseDouble('foo')).toBe('0.00');
+                       });
+                       it('works for value true', function () {
+                               expect(FormEngineValidation.parseDouble(true)).toBe('0.00');
+                       });
+                       it('works for value false', function () {
+                               expect(FormEngineValidation.parseDouble(false)).toBe('0.00');
+                       });
+                       it('works for value null', function () {
+                               expect(FormEngineValidation.parseDouble(null)).toBe('0.00');
+                       });
+               });
+
+               /**
+                * @test
+                */
+               describe('tests for btrim', function() {
+                       var result = FormEngineValidation.btrim(' test ');
+
+                       it('works for string with whitespace in begin and end', function() {
+                               expect(result).toBe(' test');
+                       });
+               });
+
+               /**
+                * @test
+                */
+               describe('tests for ltrim', function() {
+                       var result = FormEngineValidation.ltrim(' test ');
+
+                       it('works for string with whitespace in begin and end', function() {
+                               expect(result).toBe('test ');
+                       });
+               });
+
+               /**
+                * @test
+                */
+               xdescribe('tests for parseDateTime');
+
+               /**
+                * @test
+                */
+               xdescribe('tests for parseDate');
+
+               /**
+                * @test
+                */
+               xdescribe('tests for parseTime');
+
+               /**
+                * @test
+                */
+               xdescribe('tests for parseYear');
+
+               /**
+                * @test
+                */
+               describe('tests for getYear', function () {
+                       var currentDate = new Date();
+                       afterEach(function() {
+                               jasmine.clock().mockDate(currentDate);
+                       });
+
+                       it('works for current date', function () {
+                               var date = new Date();
+                               expect(FormEngineValidation.getYear(date)).toBe(date.getYear() + 1900);
+                       });
+                       it('works for year 2013', function () {
+                               var baseTime = new Date(2013, 9, 23);
+                               jasmine.clock().mockDate(baseTime);
+                               expect(FormEngineValidation.getYear(baseTime)).toBe(2013);
+                       })
+               });
+
+               /**
+                * @test
+                */
+               describe('tests for getDate', function () {
+                       var currentDate = new Date();
+                       afterEach(function() {
+                               jasmine.clock().mockDate(currentDate);
+                       });
+
+                       xit('works for year 2013', function () {
+                               var baseTime = new Date(2013, 9, 23, 13, 13, 13);
+                               jasmine.clock().mockDate(baseTime);
+                               expect(FormEngineValidation.getDate(baseTime)).toBe(1382479200);
+                       })
+               });
+
+               /**
+                * @test
+                */
+               describe('tests for splitStr', function () {
+                       it('works for command and index', function () {
+                               expect(FormEngineValidation.splitStr('foo,bar,baz', ',', -1)).toBe('foo');
+                               expect(FormEngineValidation.splitStr('foo,bar,baz', ',', 0)).toBe('foo');
+                               expect(FormEngineValidation.splitStr('foo,bar,baz', ',', 1)).toBe('foo');
+                               expect(FormEngineValidation.splitStr('foo,bar,baz', ',', 2)).toBe('bar');
+                               expect(FormEngineValidation.splitStr('foo,bar,baz', ',', 3)).toBe('baz');
+                               expect(FormEngineValidation.splitStr(' foo , bar , baz ', ',', 1)).toBe(' foo ');
+                               expect(FormEngineValidation.splitStr(' foo , bar , baz ', ',', 2)).toBe(' bar ');
+                               expect(FormEngineValidation.splitStr(' foo , bar , baz ', ',', 3)).toBe(' baz ');
+                       });
+               });
+
+               /**
+                * @test
+                */
+               xdescribe('tests for split');
+       });
+});
diff --git a/typo3/sysext/backend/Tests/JavaScript/GridEditorTest.js b/typo3/sysext/backend/Tests/JavaScript/GridEditorTest.js
new file mode 100644 (file)
index 0000000..18321e6
--- /dev/null
@@ -0,0 +1,45 @@
+define(['jquery', 'TYPO3/CMS/Backend/GridEditor'], function($, GridEditor) {
+       'use strict';
+
+       describe('TYPO3/CMS/Backend/GridEditorTest:', function() {
+               /**
+                * @test
+                */
+               describe('tests for getNewCell', function() {
+                       it('works and return a default cell object', function() {
+                               var cell = {
+                                       spanned: 0,
+                                       rowspan: 1,
+                                       colspan: 1,
+                                       name: '',
+                                       colpos: ''
+                               };
+                               expect(GridEditor.getNewCell()).toEqual(cell);
+                       });
+               });
+
+               /**
+                * @test
+                */
+               describe('tests for addRow', function() {
+                       var origData = GridEditor.data;
+                       it('works and add a new row', function() {
+                               //GridEditor.addRow();
+                               //expect(GridEditor.data.length).toBe(origData.length + 1);
+                               pending('TypeError: undefined is not an object (evaluating GridEditor.data.push)');
+                       });
+               });
+
+               /**
+                * @test
+                */
+               describe('tests for stripMarkup', function() {
+                       it('works with string which contains html markup only', function() {
+                               expect(GridEditor.stripMarkup('<b>foo</b>')).toBe('');
+                       });
+                       it('works with string which contains html markup and normal text', function() {
+                               expect(GridEditor.stripMarkup('<b>foo</b> bar')).toBe(' bar');
+                       });
+               });
+       });
+});
diff --git a/typo3/sysext/backend/Tests/JavaScript/IconsTest.js b/typo3/sysext/backend/Tests/JavaScript/IconsTest.js
new file mode 100644 (file)
index 0000000..72a8dc6
--- /dev/null
@@ -0,0 +1,74 @@
+define(['jquery', 'TYPO3/CMS/Backend/Icons'], function($, Icons) {
+       'use strict';
+
+       describe('TYPO3/CMS/Backend/IconsTest:', function() {
+               /**
+                * @test
+                */
+               describe('tests for Icons object', function() {
+                       it('has all sizes', function() {
+                               expect(Icons.sizes.small).toBe('small');
+                               expect(Icons.sizes.default).toBe('default');
+                               expect(Icons.sizes.large).toBe('large');
+                               expect(Icons.sizes.overlay).toBe('overlay');
+                       });
+                       it('has all states', function() {
+                               expect(Icons.states.default).toBe('default');
+                               expect(Icons.states.disabled).toBe('disabled');
+                       });
+                       it('has all markupIdentifiers', function() {
+                               expect(Icons.markupIdentifiers.default).toBe('default');
+                               expect(Icons.markupIdentifiers.inline).toBe('inline');
+                       });
+               });
+
+               /**
+                * @test
+                */
+               describe('tests for Icons::getIcon', function() {
+                       beforeEach(function() {
+                               spyOn(Icons, 'getIcon');
+                               Icons.getIcon('test', Icons.sizes.small, null, Icons.states.default, Icons.markupIdentifiers.default);
+                       });
+
+                       it("tracks that the spy was called", function() {
+                               expect(Icons.getIcon).toHaveBeenCalled();
+                       });
+                       it("tracks all the arguments of its calls", function() {
+                               expect(Icons.getIcon).toHaveBeenCalledWith('test', Icons.sizes.small, null, Icons.states.default, Icons.markupIdentifiers.default);
+                       });
+                       xit('works get icon from remote server');
+               });
+
+               /**
+                * @test
+                */
+               describe('tests for Icons::putInCache', function() {
+                       it('works for simply identifier and markup', function() {
+                               Icons.putInCache('foo', 'bar');
+                               expect(Icons.cache['foo']).toBe('bar');
+                       });
+               });
+
+               /**
+                * @test
+                */
+               describe('tests for Icons::getFromCache', function() {
+                       it('return undefined for uncached icon', function() {
+                               expect(Icons.getFromCache('bar')).not.toBeDefined();
+                       });
+               });
+
+               /**
+                * @test
+                */
+               describe('tests for Icons::isCached', function() {
+                       it('return true for cached icon', function() {
+                               expect(Icons.isCached('foo')).toBe(true);
+                       });
+                       it('return false for uncached icon', function() {
+                               expect(Icons.isCached('bar')).toBe(false);
+                       });
+               });
+       });
+});
diff --git a/typo3/sysext/core/Build/Configuration/JSUnit/Bootstrap.js b/typo3/sysext/core/Build/Configuration/JSUnit/Bootstrap.js
new file mode 100644 (file)
index 0000000..2a364a7
--- /dev/null
@@ -0,0 +1,86 @@
+'use strict';
+
+var tests = [];
+var paths = {
+       'jquery-ui': '/typo3/sysext/core/Resources/Public/JavaScript/Contrib/jquery-ui',
+       'datatables': '/typo3/sysext/core/Resources/Public/JavaScript/Contrib/jquery.dataTables',
+       'matchheight': '/typo3/sysext/core/Resources/Public/JavaScript/Contrib/jquery.matchHeight-min',
+       'nprogress': '/typo3/sysext/core/Resources/Public/JavaScript/Contrib/nprogress',
+       'moment': '/typo3/sysext/core/Resources/Public/JavaScript/Contrib/moment',
+       'cropper': '/typo3/sysext/core/Resources/Public/JavaScript/Contrib/cropper.min',
+       'imagesloaded': '/typo3/sysext/core/Resources/Public/JavaScript/Contrib/imagesloaded.pkgd.min',
+       'bootstrap': '/base/typo3/sysext/core/Resources/Public/JavaScript/Contrib/bootstrap/bootstrap',
+       'twbs/bootstrap-datetimepicker': '/typo3/sysext/core/Resources/Public/JavaScript/Contrib/bootstrap-datetimepicker',
+       'autosize': '/typo3/sysext/core/Resources/Public/JavaScript/Contrib/autosize',
+       'taboverride': '/typo3/sysext/core/Resources/Public/JavaScript/Contrib/taboverride.min',
+       'twbs/bootstrap-slider': '/typo3/sysext/core/Resources/Public/JavaScript/Contrib/bootstrap-slider.min',
+       'jquery/autocomplete': '/typo3/sysext/core/Resources/Public/JavaScript/Contrib/jquery.autocomplete'
+};
+
+/**
+ * Collect test files and define namespace mapping for RequireJS config
+ */
+for (var file in window.__karma__.files) {
+       if (window.__karma__.files.hasOwnProperty(file)) {
+               // Add dynamic path mapping for requirejs config
+               if (/Resources\/Public\/JavaScript\//.test(file)) {
+                       var parts = file.split('/');
+                       var extkey = parts[4];
+                       var extname = extkey.replace(/_([a-z])/g, function(g) {
+                               return g[1].toUpperCase();
+                       });
+                       extname = extname.charAt(0).toUpperCase() + extname.slice(1);
+                       var namespace = 'TYPO3/CMS/' + extname;
+                       if (typeof paths[namespace] === 'undefined') {
+                               paths[namespace] = '/base/typo3/sysext/' + extkey + '/Resources/Public/JavaScript';
+                       }
+               }
+               // Find all test files
+               var testFilePattern = /Tests\/JavaScript\/(.*)Test\.js$/gi;
+               if (testFilePattern.test(file)) {
+                       tests.push(file);
+               }
+       }
+}
+
+/**
+ * Define environment
+ * Set global objects and variables
+ * @TODO: hopefully we can cleanup the following lines in future
+ */
+if (typeof TYPO3 === 'undefined') {
+       var TYPO3 = TYPO3 || {};
+       TYPO3.jQuery = jQuery.noConflict(true);
+       TYPO3.settings = {
+               'FormEngine': {
+                       'formName': 'Test'
+               },
+               'DateTimePicker': {
+                       'DateFormat': 'd.m.Y'
+               },
+               'ajaxUrls': {
+               }
+       };
+       TYPO3.lang = {};
+}
+
+top.TYPO3 = TYPO3;
+var TBE_EDITOR = {};
+
+/**
+ * RequireJS setup
+ */
+requirejs.config({
+       // Karma serves files from '/base'
+       baseUrl: '/base',
+
+       paths: paths,
+
+       shim: {},
+
+       // ask Require.js to load these files (all our tests)
+       deps: tests,
+
+       // start test run, once Require.js is done
+       callback: window.__karma__.start
+});
diff --git a/typo3/sysext/core/Build/Configuration/JSUnit/Helper.js b/typo3/sysext/core/Build/Configuration/JSUnit/Helper.js
new file mode 100644 (file)
index 0000000..d8f7685
--- /dev/null
@@ -0,0 +1,34 @@
+'use strict';
+
+/**
+ * Helper function to implement DataProvider
+ * @param {Function|Array|Object} values
+ * @param {Function} func
+ */
+function using(values, func) {
+       if (values instanceof Function) {
+               values = values();
+       }
+
+       if (values instanceof Array) {
+               values.forEach(function(value) {
+                       if (!(value instanceof Array)) {
+                               value = [value];
+                       }
+
+                       func.apply(this, value);
+               });
+       } else {
+               var objectKeys = Object.keys(values);
+
+               objectKeys.forEach(function(key) {
+                       if (!(values[key] instanceof Array)) {
+                               values[key] = [values[key]];
+                       }
+
+                       values[key].push(key);
+
+                       func.apply(this, values[key]);
+               });
+       }
+}
diff --git a/typo3/sysext/core/Build/Configuration/JSUnit/karma.conf.js b/typo3/sysext/core/Build/Configuration/JSUnit/karma.conf.js
new file mode 100644 (file)
index 0000000..173b240
--- /dev/null
@@ -0,0 +1,65 @@
+'use strict';
+
+/**
+ * Karma configuration
+ */
+
+module.exports = function(config) {
+       config.set({
+               // base path that will be used to resolve all patterns (eg. files, exclude)
+               basePath: '../../../../../../',
+
+               // frameworks to use
+               // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+               frameworks: ['jasmine', 'requirejs'],
+
+               // list of files / patterns to load in the browser
+               files: [
+                       { pattern: 'typo3/sysext/core/Resources/Public/JavaScript/Contrib/jquery/jquery-2.2.3.js', included: true },
+                       { pattern: 'typo3/sysext/**/Resources/Public/JavaScript/**/*.js', included: false },
+                       { pattern: 'typo3/sysext/**/Tests/JavaScript/**/*.js', included: false },
+                       'typo3/sysext/core/Build/Configuration/JSUnit/Helper.js',
+                       'typo3/sysext/core/Build/Configuration/JSUnit/Bootstrap.js'
+               ],
+
+               // list of files to exclude
+               exclude: [
+               ],
+
+               // preprocess matching files before serving them to the browser
+               // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+               preprocessors: {
+               },
+
+               // test results reporter to use
+               // possible values: 'dots', 'progress'
+               // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+               reporters: ['progress'],
+
+               // web server port
+               port: 9876,
+
+               // enable / disable colors in the output (reporters and logs)
+               colors: true,
+
+               // level of logging
+               // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+               logLevel: config.LOG_INFO,
+
+               // enable / disable watching file and executing tests whenever any file changes
+               autoWatch: true,
+
+               // start these browsers
+               // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+               // browsers: ['Firefox', 'Chrome', 'Safari', 'PhantomJS', 'Opera', 'IE'],
+               browsers: ['PhantomJS'],
+
+               // Continuous Integration mode
+               // if true, Karma captures browsers, runs the tests and exits
+               singleRun: false,
+
+               // Concurrency level
+               // how many browser should be started simultaneous
+               concurrency: Infinity
+       })
+};
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-76590-IntroduceUnitTestsForJavaScript.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-76590-IntroduceUnitTestsForJavaScript.rst
new file mode 100644 (file)
index 0000000..38f69d5
--- /dev/null
@@ -0,0 +1,67 @@
+====================================================
+Feature: #76590 - Introduce UnitTests for JavaScript
+====================================================
+
+Description
+===========
+
+The core use karma as test runner and jasmine as testing framework for JavaScript UnitTests.
+The tests will be run on travis with PhantomJS.
+Locally other browsers like Chrome, Firefox, Safari or IE can be used as well.
+
+To run the UnitTests on a local system the following steps are necessary:
+
+1. Install dependencies
+
+.. code-block:: bash
+
+   cd Build
+   npm install
+   cd ..
+
+2. Run the tests from your terminal
+
+.. code-block:: bash
+
+   # Execute the tests only once
+   ./Build/node_modules/karma/bin/karma start typo3/sysext/core/Build/Configuration/JSUnit/karma.conf.js --single-run
+
+   # Execute the tests for every change (file watcher mode)
+   ./Build/node_modules/karma/bin/karma start typo3/sysext/core/Build/Configuration/JSUnit/karma.conf.js
+
+   # Execute the tests for different browser
+   ./Build/node_modules/karma/bin/karma start typo3/sysext/core/Build/Configuration/JSUnit/karma.conf.js --single-run --browsers Chrome,Safari,Firefox
+
+Test-Files
+==========
+
+Any test file must be located in extension folder ``typo3/sysext/<EXTKEY>/Tests/JavaScript/``
+The filename must end with Test.js, e.g. ``GridEditorTest.js``
+Each testfile must be implemented as AMD module, must use strict mode and have to use :js:`describe` with module name as outer wrap for each test.
+The following code block shows a good example:
+
+.. code-block:: javascript
+
+   define(['jquery', 'TYPO3/CMS/Backend/AnyModule'], function($, AnyModule) {
+      'use strict';
+      // first and outer wrap describe the test class name
+      describe('TYPO3/CMS/Backend/AnyModuleTest:', function() {
+         // second wrap describe the method to test
+         describe('tests for fooAction', function() {
+            // the first parameter of each 'it' method describe the test-case.
+            it('works for parameter a and b', function() {});
+         });
+         describe('tests for barAction', function() {
+            it('works for parameter a and b', function() {});
+         });
+      }
+   }
+
+Please take a look at the existing test-files and read the jasmine documentation for further information.
+
+DataProvider for tests
+----------------------
+
+For testing a set of values, the core implement a kind of DataProvider. To use the DataProvider you have to use the function :js:`using`.
+Please take a look at ``FormEngineValidationTest.js`` for an example.
+