diff --git a/.gitignore b/.gitignore
index fa5ff5c4f07a6..edb129f856d2d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,9 @@ tests/codeception/tests/acceptance/*Tester.php
tests/codeception/tests/functional/*Tester.php
tests/codeception/tests/unit/*Tester.php
+# Node modules #
+node_modules/
+
# phpDocumentor Logs #
phpdoc-*
diff --git a/.travis.yml b/.travis.yml
index fbbdd311e6c35..41e0daceb0f34 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,6 +6,7 @@ language: php
env:
global:
- RUN_PHPCS="no"
+ - RUN_JAVASCRIPT_TESTS="no"
- INSTALL_MEMCACHE="yes"
- INSTALL_MEMCACHED="yes"
- INSTALL_REDIS="yes"
@@ -13,6 +14,9 @@ env:
matrix:
fast_finish: true
include:
+ - node_js: 6.1
+ sudo: true
+ env: RUN_JAVASCRIPT_TESTS="yes"
- php: 5.3
env: INSTALL_APC="yes"
- php: 5.4
@@ -50,6 +54,8 @@ services:
- redis-server
before_script:
+ # JavaScript tests
+ - if [[ $RUN_JAVASCRIPT_TESTS == "yes" ]]; then export DISPLAY=:99.0; bash tests/javascript/travis-tests.sh $PWD; fi
# Make sure all dev dependencies are installed
- composer install
# Set up databases for testing
@@ -74,6 +80,7 @@ before_script:
script:
- libraries/vendor/bin/phpunit --configuration travisci-phpunit.xml
- if [[ $RUN_PHPCS == "yes" ]]; then libraries/vendor/bin/phpcs --report=full --extensions=php -p --standard=build/phpcs/Joomla .; fi
+ - if [[ $RUN_JAVASCRIPT_TESTS == "yes" ]]; then tests/javascript/node_modules/karma/bin/karma start karma.conf.js --single-run ; fi
branches:
except:
diff --git a/karma.conf.js b/karma.conf.js
new file mode 100644
index 0000000000000..9446fd44c157e
--- /dev/null
+++ b/karma.conf.js
@@ -0,0 +1,84 @@
+// 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-ajax', 'jasmine', 'requirejs'],
+
+ // list of files / patterns to load in the browser
+ files: [
+ {pattern: 'tests/javascript/node_modules/jquery/dist/jquery.min.js', included: false},
+ {pattern: 'tests/javascript/node_modules/jasmine-jquery/lib/jasmine-jquery.js', included: false},
+ {pattern: 'tests/javascript/node_modules/text/text.js', included: false},
+ {pattern: 'media/jui/js/bootstrap.min.js', included: false},
+ {pattern: 'media/system/js/*.js', included: false},
+ {pattern: 'tests/javascript/**/fixture.html', included: false},
+ {pattern: 'tests/javascript/**/spec.js', included: false},
+ {pattern: 'tests/javascript/**/spec-setup.js', included: false},
+ {pattern: 'images/*.png', included: false},
+
+ 'tests/javascript/test-main.js'
+ ],
+
+ // list of files to exclude
+ exclude: [
+ 'media/system/js/*uncompressed.js'
+ ],
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ '**/system/js/*!(uncompressed).js': ['coverage']
+ },
+
+ // coverage reporter configuration
+ coverageReporter: {
+ type : 'text'
+ },
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['verbose', 'coverage'],
+
+ // 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'],
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: false,
+
+ // list of plugins
+ plugins: [
+ 'karma-jasmine',
+ 'karma-jasmine-ajax',
+ 'karma-firefox-launcher',
+ 'karma-coverage',
+ 'karma-requirejs',
+ 'karma-verbose-reporter'
+ ],
+
+ // Concurrency level
+ // how many browser should be started simultaneous
+ concurrency: Infinity
+ });
+};
diff --git a/tests/javascript/caption/fixtures/fixture.html b/tests/javascript/caption/fixtures/fixture.html
new file mode 100644
index 0000000000000..6ed385eb316d1
--- /dev/null
+++ b/tests/javascript/caption/fixtures/fixture.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/javascript/caption/spec-setup.js b/tests/javascript/caption/spec-setup.js
new file mode 100644
index 0000000000000..f11d37fa2938e
--- /dev/null
+++ b/tests/javascript/caption/spec-setup.js
@@ -0,0 +1,17 @@
+/**
+ * @copyright Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved.
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ * @package Joomla
+ * @subpackage JavaScript Tests
+ * @since 3.6
+ * @version 1.0.0
+ */
+
+define(['jquery', 'text!testsRoot/caption/fixtures/fixture.html', 'libs/caption'], function ($, fixture) {
+ $('body').append(fixture);
+
+ new JCaption('#single img.test');
+ new JCaption('#multiple img.test');
+ new JCaption('#empty img.test');
+ new JCaption('#options img.test');
+});
diff --git a/tests/javascript/caption/spec.js b/tests/javascript/caption/spec.js
new file mode 100644
index 0000000000000..64d50a1436635
--- /dev/null
+++ b/tests/javascript/caption/spec.js
@@ -0,0 +1,68 @@
+/**
+ * @copyright Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved.
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ * @package Joomla
+ * @subpackage JavaScript Tests
+ * @since 3.6
+ * @version 1.0.0
+ */
+
+define(['jquery', 'testsRoot/caption/spec-setup', 'jasmineJquery'], function ($) {
+
+ describe('JCaption applied to single image', function () {
+ it('Should have caption as "Joomla Title 1" under image', function () {
+ expect($('#single').find('p')).toHaveText('Joomla Title 1');
+ });
+ });
+
+ describe('JCaption applied for multiple images', function () {
+ it('Should have caption "Joomla Title 1" under image 1', function () {
+ expect($('#multiple').find('p').first()).toHaveText('Joomla Title 1');
+ });
+
+ it('Should have caption as "Joomla Title 2" under image 2', function() {
+ expect($('#multiple').find('p').last()).toHaveText('Joomla Title 2');
+ });
+ });
+
+ describe('JCaption with empty title attribute value', function () {
+ it('Should not have a
element inside the image container', function () {
+ expect($('#empty')).not.toContainElement('p');
+ });
+ });
+
+ describe('JCaption with no additional options', function () {
+ var $element = $('img#no-options');
+ it('Should have container CSS {float: none}', function () {
+ expect($element.parent()).toHaveCss({
+ float: 'none'
+ });
+ });
+ });
+
+ describe('JCaption with additional options', function () {
+ it('Should have 2 elements with class right', function () {
+ expect($('#options').find('.right').length).toEqual(2);
+ });
+
+ it('Should have container width as 100 when element width attribute is set to 100', function () {
+ expect($('img#width-attr').parent().width()).toEqual(100);
+ });
+
+ it('Should have container width as 90 when element style is set to width: 90px', function () {
+ expect($('img#width-style').parent().width()).toEqual(90);
+ });
+
+ it('Should have float: right in container CSS when element attribute align is set to right', function () {
+ expect($('img#align-attr').parent()).toHaveCss({
+ float: 'right'
+ });
+ });
+
+ it('Should have float: right in container CSS when element style is set to float: right', function () {
+ expect($('img#align-style').parent()).toHaveCss({
+ float: 'right'
+ });
+ });
+ });
+});
diff --git a/tests/javascript/core/fixtures/fixture.html b/tests/javascript/core/fixtures/fixture.html
new file mode 100644
index 0000000000000..c7e001fa3c389
--- /dev/null
+++ b/tests/javascript/core/fixtures/fixture.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/javascript/core/spec-setup.js b/tests/javascript/core/spec-setup.js
new file mode 100644
index 0000000000000..eee57e2adf70b
--- /dev/null
+++ b/tests/javascript/core/spec-setup.js
@@ -0,0 +1,12 @@
+/**
+ * @copyright Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved.
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ * @package Joomla
+ * @subpackage JavaScript Tests
+ * @since 3.6
+ * @version 1.0.0
+ */
+
+define(['jquery', 'text!testsRoot/core/fixtures/fixture.html', 'libs/core'], function ($, fixture) {
+ $('body').append(fixture);
+});
diff --git a/tests/javascript/core/spec.js b/tests/javascript/core/spec.js
new file mode 100644
index 0000000000000..19ac3684b5c89
--- /dev/null
+++ b/tests/javascript/core/spec.js
@@ -0,0 +1,199 @@
+/**
+ * @copyright Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved.
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ * @package Joomla
+ * @subpackage JavaScript Tests
+ * @since 3.6
+ * @version 1.0.0
+ */
+
+define(['jquery', 'testsRoot/core/spec-setup', 'jasmineJquery'], function ($) {
+
+ describe('Core Joomla.submitform', function () {
+ var form = document.getElementById('adminForm');
+ form.task = {};
+
+ beforeEach(function () {
+ spyOnEvent('#adminForm', 'submit');
+ form.removeChild = jasmine.createSpy('removeChild');
+
+ Joomla.submitform('article.add', form, true);
+ });
+
+ it('should assign task to form.task.value', function () {
+ expect(form.task.value).toEqual('article.add');
+ });
+ it('should set attribute novalidate to false', function () {
+ expect($(form)).toHaveAttr('novalidate', 'false');
+ });
+ it('should add input submit button to DOM', function () {
+ expect($('#adminForm')).toContainElement('input[type="submit"]');
+ });
+ it('should make the added input element invisible', function () {
+ expect($('#adminForm').children('input[type="submit"]')).not.toBeVisible();
+ });
+ it('should click the input element', function () {
+ expect('submit').toHaveBeenTriggeredOn('#adminForm');
+ });
+ it('should remove the input element', function () {
+ expect(form.removeChild).toHaveBeenCalled();
+ });
+ });
+
+ describe('Core Joomla.JText', function () {
+ var ob = {
+ 'JTOGGLE_SHOW_SIDEBAR': 'Show Sidebar',
+ 'Jtoggle_Hide_Sidebar': 'Hide Sidebar'
+ };
+
+ beforeAll(function () {
+ Joomla.JText.load(ob);
+ });
+
+ it('should add content passed via load() to the strings object', function () {
+ expect(Joomla.JText.strings.JTOGGLE_SHOW_SIDEBAR).toEqual('Show Sidebar');
+ expect(Joomla.JText.strings.JTOGGLE_HIDE_SIDEBAR).toEqual('Hide Sidebar');
+ });
+ it('should return \'Show Sidebar\' on calling Joomla.JText._(\'JTOGGLE_SHOW_SIDEBAR\', \'test\')', function () {
+ expect(Joomla.JText._('JTOGGLE_SHOW_SIDEBAR', 'test')).toEqual('Show Sidebar');
+ });
+ it('should return \'Show Sidebar\' on calling Joomla.JText._(\'Jtoggle_Show_Sidebar\', \'test\')', function () {
+ expect(Joomla.JText._('Jtoggle_Show_Sidebar', 'test')).toEqual('Show Sidebar');
+ });
+ it('should return \'test\' on calling Joomla.JText._(\'JTOGGLE_REMOVE_SIDEBAR\', \'test\')', function () {
+ expect(Joomla.JText._('JTOGGLE_REMOVE_SIDEBAR', 'test')).toEqual('test');
+ });
+ });
+
+ describe('Core Joomla.replaceTokens', function () {
+ var newToken = '123456789123456789123456789ABCDE';
+
+ beforeAll(function () {
+ Joomla.replaceTokens(newToken);
+ });
+
+ it('should set name of all hidden input elements with value = 1 and name = old token to new token', function () {
+ var $elements =$('.replace-tokens-input');
+ expect($elements[0].name).toEqual(newToken);
+ expect($elements[1].name).toEqual(newToken);
+ });
+ it('should not set name of non hidden input elements to new token', function () {
+ expect($('.replace-tokens-input #invalid-type').name).not.toEqual(newToken);
+ });
+ it('should not set name of input elements with value other than 1 to new token', function () {
+ expect($('.replace-tokens-input #invalid-value').name).not.toEqual(newToken);
+ });
+ it('should not set name of input elements with invalid name to new token', function () {
+ expect($('.replace-tokens-input #invalid-name').name).not.toEqual(newToken);
+ });
+ });
+
+ describe('Core Joomla.checkAll', function () {
+ var form = document.getElementById('check-all-form');
+ form.boxchecked = {};
+ var element = document.getElementById('cb0');
+
+ beforeAll(function () {
+ Joomla.checkAll(element);
+ });
+
+ it('should return false when input element is not inside a form', function () {
+ expect(Joomla.checkAll(document.getElementById('cb-no-form'))).toEqual(false);
+ });
+
+ it('should check all the checkboxes that has id starting with \'cb\' inside the form', function () {
+ expect($('#cb0')).toBeChecked();
+ expect($('#cb1')).toBeChecked();
+ expect($('#cb2')).toBeChecked();
+ expect($('#no-cb3')).not.toBeChecked();
+ });
+
+ it('should set the number of checked boxes in the form to form.boxchecked', function () {
+ expect(form.boxchecked.value).toEqual(3);
+ });
+
+ it('should use passed in stub to look for input elements', function () {
+ var element = document.getElementById('stub-check-test-1');
+ Joomla.checkAll(element, 'stub');
+
+ expect($('#stub-check-test-1')).toBeChecked();
+ expect($('#stub-check-test-2')).toBeChecked();
+ });
+ });
+
+ describe('Core Joomla.renderMessages and Joomla.removeMessages', function () {
+ var messages = {
+ "message": ["Message one", "Message two"],
+ "error": ["Error one", "Error two"]
+ };
+
+ beforeAll(function () {
+ Joomla.JText.load({"message": "Message"});
+ Joomla.renderMessages(messages);
+ });
+
+ it('renderMessages should render titles when translated strings are available', function () {
+ expect($('h4.alert-heading').first()).toContainText('Message');
+ });
+
+ it('renderMessages should render messages inside a div having class alert-message', function () {
+ var $messages = $('div.alert-success').children('div');
+ expect($messages[0]).toContainText('Message two');
+ expect($messages[1]).toContainText('Message one');
+ });
+
+ it('renderMessages should render errors inside a div having class alert-error', function () {
+ var $messages = $('div.alert-error').children('div');
+ expect($messages[0]).toContainText('Error two');
+ expect($messages[1]).toContainText('Error one');
+ });
+
+ it('removeMessages should remove all content from system-message-container', function () {
+ Joomla.removeMessages();
+ expect($("#system-message-container")).toBeEmpty();
+ });
+ });
+
+ describe('Core Joomla.isChecked', function () {
+ var form = document.getElementById('ischecked-test-form');
+ form.boxchecked = {value: 5};
+
+ beforeAll(function () {
+ Joomla.isChecked(true, form);
+ });
+
+ it('should increase form.boxchecked.value from 5 to 6', function () {
+ expect(form.boxchecked.value).toEqual(6);
+ });
+ it('should set checkAllToggle.checked to false', function () {
+ expect(form.elements[ 'checkall-toggle' ].checked).toEqual(false);
+ });
+ });
+
+ describe('Core Joomla.tableOrdering', function () {
+ beforeAll(function () {
+ submitformFn = Joomla.submitform;
+ Joomla.submitform = jasmine.createSpy('submitform');
+
+ this.form = document.getElementById('table-ordering-test-form');
+ this.form.filter_order = {};
+ this.form.filter_order_Dir = {};
+
+ Joomla.tableOrdering('order', 'dir', 'task', this.form);
+ });
+
+ afterAll(function() {
+ Joomla.submitform = submitformFn;
+ });
+
+ it('should call Joomla.submitform with params task and form', function () {
+ expect(Joomla.submitform).toHaveBeenCalledWith('task', this.form);
+ });
+ it('should set form.filter_order.value = order', function () {
+ expect(this.form.filter_order.value).toEqual('order')
+ });
+ it('should set form.filter_order_Dir.value = dir', function () {
+ expect(this.form.filter_order_Dir.value).toEqual('dir')
+ });
+ });
+});
diff --git a/tests/javascript/package.json b/tests/javascript/package.json
new file mode 100644
index 0000000000000..5efa1f25a0f67
--- /dev/null
+++ b/tests/javascript/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "joomla-cms",
+ "version": "1.0.0",
+ "description": "Joomla! CMS™ [![Analytics](https://ga-beacon.appspot.com/UA-544070-3/joomla-cms/readme)](https://github.com/igrigorik/ga-beacon)\r ====================",
+ "main": "index.js",
+ "directories": {
+ "test": "tests"
+ },
+ "scripts": {
+ "test": "node node_modules/karma/bin/karma start ../../karma.conf.js --single-run"
+ },
+ "author": "Thanuditha Ruchiranga Wickramasinghe",
+ "license": "GPL-2.0",
+ "private": true,
+ "devDependencies": {
+ "jasmine-core": "^2.4.1",
+ "jasmine-jquery": "^2.1.1",
+ "jquery": "^2.2.3",
+ "karma": "^0.13.22",
+ "karma-coverage": "^1.0.0",
+ "karma-firefox-launcher": "^0.1.7",
+ "karma-jasmine": "^0.3.8",
+ "karma-jasmine-ajax": "^0.1.13",
+ "karma-requirejs": "^1.0.0",
+ "karma-verbose-reporter": "0.0.3",
+ "requirejs": "^2.2.0",
+ "text": "requirejs/text"
+ }
+}
diff --git a/tests/javascript/permissions/fixtures/fixture.html b/tests/javascript/permissions/fixtures/fixture.html
new file mode 100644
index 0000000000000..284c6059f9e06
--- /dev/null
+++ b/tests/javascript/permissions/fixtures/fixture.html
@@ -0,0 +1,18 @@
+