Skip to content
Snippets Groups Projects
Commit 7ebe1201 authored by Tom Robinson's avatar Tom Robinson
Browse files

Merge pull request #472 from metabase/client_testing

Client testing
parents cd701824 c1f66137
No related branches found
No related tags found
No related merge requests found
Showing
with 226 additions and 261 deletions
......@@ -20,5 +20,5 @@ profiles.clj
/*.trace.db
/resources/frontend_client/app/dist/
/node_modules/
/.js_hint_output/
/.babel_cache
/coverage
......@@ -6,8 +6,8 @@ test:
override:
# 0) runs unit tests w/ H2 local DB. Runs against both Mongo + H2 test datasets
# 1) runs unit tests w/ Postgres local DB. Only runs against H2 test dataset so we can be sure tests work in either scenario
# 2) runs Eastwood linter
# 3) runs JS linter + Bikeshed linter
# 2) runs Eastwood linter + Bikeshed linter
# 3) runs JS linter + JS test
# 4) runs lein uberjar
- case $CIRCLE_NODE_INDEX in 0) MB_TEST_DATASETS=generic-sql,mongo lein test ;; 1) MB_DB_TYPE=postgres MB_DB_DBNAME=circle_test MB_DB_PORT=5432 MB_DB_USER=ubuntu MB_DB_HOST=localhost lein test ;; 2) lein eastwood ;; 3) ./lint_js.sh && lein bikeshed --max-line-length 240 ;; 4) CI_DISABLE_WEBPACK_MINIFICATION=1 lein uberjar ;; esac:
- case $CIRCLE_NODE_INDEX in 0) MB_TEST_DATASETS=generic-sql,mongo lein test ;; 1) MB_DB_TYPE=postgres MB_DB_DBNAME=circle_test MB_DB_PORT=5432 MB_DB_USER=ubuntu MB_DB_HOST=localhost lein test ;; 2) lein eastwood && lein bikeshed --max-line-length 240 ;; 3) npm run lint && npm run build && npm run test ;; 4) CI_DISABLE_WEBPACK_MINIFICATION=1 lein uberjar ;; esac:
parallel: true
#! /bin/bash
# Simple shell script for running jshint and nicely formatting the output :heart_eyes_cat:
ESLINT='./node_modules/eslint/bin/eslint.js'
JS_FILES=`find resources/frontend_client/app -name "*.js" | grep -v bower_components | grep -v 'app/test/' | grep -v '\#' | grep -v 'app/dist/'`
BOLD='\033[1;30m'
RED='\033[0;31m' # \e doesn't work on OS X but \033 works on either
NC='\033[0m'
HAS_ERROR=0
# Make the .js_hint_output dir if needed
if [ ! -d .js_hint_output ]; then
mkdir .js_hint_output
fi
# cache file names are are just filename with '/' replaced by '_' and stuck in the .js_hint_output dir
cache_file_name () {
echo ".js_hint_output/"`echo $1 | sed 's:/:_:g'`
}
# return unix timestamp for file or 0 if it doesn't exist
# find out if stat supports --format (Linux or Mac w/ updated coreutils via homebrew) or
# if we need to use -c (default OS X)
# OS X version doesn't support --version so see if doing that returns non-zero exit code
if (( `stat --version >/dev/null 2>/dev/null; echo "$?"` )); then
STAT_FORMAT_FLAG='-f%m';
else
STAT_FORMAT_FLAG='--format=%Y';
fi
file_modified () {
file=$1
if [ -f "$file" ] && [ -n "$file" ]; then # make sure filename is non-empty, -f will return true otherwise
echo `stat $STAT_FORMAT_FLAG $file`
else
echo "0";
fi
}
# Default output repeats file name on every offending line;
# Instead, we'll print the filename once in bold for all bad files
# and then print just the errors in red
run_js_lint () {
file=$1
output_file=$2
$ESLINT $file | perl -pe 's/^.*(line.*)$/$1/' | sort > $output_file
}
for file in $JS_FILES; do
# Find matching cached output if file hasn't changed since last run
cache_file=$(cache_file_name $file)
# get file modified dates
file_modified_date=$(file_modified $file)
cache_file_modified_date=$(file_modified $cache_file)
# run js lint to (re-)generate cache file if it's older than $file
if [ $cache_file_modified_date -lt $file_modified_date ]; then
run_js_lint $file $cache_file
fi
# ok, output the cached file either way
errors=$(cat $cache_file)
if [ -n "$errors" ]; then
echo -e "\n\n"${BOLD}"$file"
echo -e ${RED}
echo "$errors"
echo -e ${NC}
HAS_ERROR=1
fi
done
if [[ $HAS_ERROR -eq 1 ]]; then
exit 1
else
exit 0
fi
......@@ -17,6 +17,7 @@
"angular-cookies": "1.2.28",
"angular-gridster": "0.11.7",
"angular-http-auth": "1.2.1",
"angular-mocks": "1.2.28",
"angular-readable-time": "git://github.com/wildlyinaccurate/angular-readable-time#0.1.1",
"angular-resource": "1.2.28",
"angular-route": "1.2.28",
......@@ -51,16 +52,27 @@
"eslint-plugin-react": "^2.5.0",
"extract-text-webpack-plugin": "^0.8.1",
"glob": "^5.0.10",
"http-server": "^0.6.1",
"karma": "~0.10",
"karma-junit-reporter": "^0.2.2",
"istanbul-instrumenter-loader": "^0.1.3",
"jasmine-core": "^2.3.4",
"karma": "^0.12.36",
"karma-coverage": "^0.4.2",
"karma-jasmine": "^0.3.5",
"karma-webpack": "^1.5.1",
"ng-annotate-webpack-plugin": "^0.1.2",
"node-libs-browser": "^0.5.2",
"protractor": "~0.20.1",
"protractor": "^2.1.0",
"shelljs": "^0.2.6",
"style-loader": "^0.12.3",
"webpack": "^1.9.10",
"webpack-postcss-tools": "^1.1.1"
},
"scripts": {}
"scripts": {
"lint": "./node_modules/eslint/bin/eslint.js --ignore-pattern 'resources/frontend_client/app/dist/**/*' resources/frontend_client/app",
"test": "./node_modules/karma/bin/karma start resources/frontend_client/test/karma.conf.js --single-run",
"test-watch": "./node_modules/karma/bin/karma start resources/frontend_client/test/karma.conf.js --auto-watch",
"test-e2e": "./node_modules/protractor/bin/webdriver-manager update && ./node_modules/protractor/bin/protractor resources/frontend_client/test/protractor-conf.js",
"build": "./node_modules/webpack/bin/webpack.js",
"build-watch": "./node_modules/webpack/bin/webpack.js --watch",
"start": "lein ring server"
}
}
'use strict';
// Metabase Services
var MetabaseServices = angular.module('corvus.metabase.services', ['ngResource', 'ngCookies']);
var MetabaseServices = angular.module('corvus.metabase.services', [
'ngResource',
'ngCookies',
'corvus.services'
]);
MetabaseServices.factory('Metabase', ['$resource', '$cookies', 'CorvusCore', function($resource, $cookies, CorvusCore) {
return $resource('/api/meta', {}, {
......@@ -335,4 +339,4 @@ MetabaseServices.factory('TableSegment', ['$resource', '$cookies', function($res
},
});
}]);
\ No newline at end of file
}]);
'use strict';
/* https://github.com/angular/protractor/blob/master/docs/getting-started.md */
describe('my app', function() {
browser.get('index.html');
it('should automatically redirect to /view1 when location hash/fragment is empty', function() {
expect(browser.getLocationAbsUrl()).toMatch("/view1");
});
describe('view1', function() {
beforeEach(function() {
browser.get('index.html#/view1');
});
it('should render view1 when user navigates to /view1', function() {
expect(element.all(by.css('[ng-view] p')).first().getText()).
toMatch(/partial for view 1/);
});
});
describe('view2', function() {
beforeEach(function() {
browser.get('index.html#/view2');
});
it('should render view2 when user navigates to /view2', function() {
expect(element.all(by.css('[ng-view] p')).first().getText()).
toMatch(/partial for view 2/);
});
});
});
module.exports = function(config) {
config.set({
basePath : '../',
files : [
'app/bower_components/angular/angular.js',
'app/bower_components/angular-route/angular-route.js',
'app/bower_components/angular-mocks/angular-mocks.js',
'app/js/**/*.js',
'test/unit/**/*.js'
],
autoWatch : true,
frameworks: ['jasmine'],
browsers : ['Chrome'],
plugins : [
'karma-chrome-launcher',
'karma-firefox-launcher',
'karma-jasmine',
'karma-junit-reporter'
],
junitReporter : {
outputFile: 'test_out/unit.xml',
suite: 'unit'
}
});
};
exports.config = {
allScriptsTimeout: 11000,
specs: [
'e2e/*.js'
],
capabilities: {
'browserName': 'chrome'
},
baseUrl: 'http://localhost:8000/app/',
framework: 'jasmine',
jasmineNodeOpts: {
defaultTimeoutInterval: 30000
}
};
'use strict';
/* jasmine specs for controllers go here */
describe('controllers', function() {
beforeEach(module('myApp.controllers'));
it('should ....', inject(function($controller) {
//spec body
var myCtrl1 = $controller('MyCtrl1', { $scope: {} });
expect(myCtrl1).toBeDefined();
}));
it('should ....', inject(function($controller) {
//spec body
var myCtrl2 = $controller('MyCtrl2', { $scope: {} });
expect(myCtrl2).toBeDefined();
}));
});
'use strict';
/* jasmine specs for directives go here */
describe('directives', function() {
beforeEach(module('myApp.directives'));
describe('app-version', function() {
it('should print current version', function() {
module(function($provide) {
$provide.value('version', 'TEST_VER');
});
inject(function($compile, $rootScope) {
var element = $compile('<span app-version></span>')($rootScope);
expect(element.text()).toEqual('TEST_VER');
});
});
});
});
'use strict';
/* jasmine specs for filters go here */
describe('filter', function() {
beforeEach(module('myApp.filters'));
describe('interpolate', function() {
beforeEach(module(function($provide) {
$provide.value('version', 'TEST_VER');
}));
it('should replace VERSION', inject(function(interpolateFilter) {
expect(interpolateFilter('before %VERSION% after')).toEqual('before TEST_VER after');
}));
});
});
'use strict';
/* jasmine specs for services go here */
describe('service', function() {
beforeEach(module('myApp.services'));
describe('version', function() {
it('should return current version', inject(function(version) {
expect(version).toEqual('0.1');
}));
});
});
'use strict';
describe('metabase', function() {
it('should redirect logged-out user to /auth/login', function() {
browser.get('/');
expect(browser.getLocationAbsUrl()).toMatch("/auth/login");
});
});
'use strict';
var webpackConfig = require('../../../webpack.config');
webpackConfig.module.postLoaders = [
{ test: /\.js$/, exclude: /(\.spec\.js|vendor|node_modules)/, loader: 'istanbul-instrumenter' }
];
module.exports = function(config) {
config.set({
basePath: '../',
files: [
'app/dist/vendor.bundle.js',
'app/dist/app.bundle.js',
'../../node_modules/angular-mocks/angular-mocks.js',
'test/unit/**/*.spec.js'
],
exclude: [
],
preprocessors: {
'test/unit/**/*.spec.js': ['webpack']
},
frameworks: [
'jasmine'
],
reporters: [
'progress',
'coverage'
],
webpack: {
resolve: webpackConfig.resolve,
module: webpackConfig.module
},
coverageReporter: {
dir: '../../coverage/',
subdir: function(browser) {
return browser.toLowerCase().split(/[ /-]/)[0];
},
reporters: [
{ type: 'text', file: 'text.txt' },
{ type: 'text-summary', file: 'text-summary.txt' },
{ type: 'html' }
]
},
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
browsers: ['Chrome'],
autoWatch: true,
singleRun: false
});
};
exports.config = {
allScriptsTimeout: 11000,
specs: [
'e2e/*.js'
],
capabilities: {
'browserName': 'chrome'
},
baseUrl: 'http://localhost:3000/',
framework: 'jasmine',
jasmineNodeOpts: {
defaultTimeoutInterval: 30000
}
};
'use strict';
import 'metabase/controllers';
describe('corvus.controllers', function() {
beforeEach(angular.mock.module('corvus.controllers'));
describe('Homepage', function() {
beforeEach(angular.mock.inject(function($location) {
spyOn($location, 'path').and.returnValue('Fake location');
}))
it('should redirect logged-out user to /auth/login', inject(function($controller, $location) {
$controller('Homepage', { $scope: {}, AppState: { model: { currentUser: null }} });
expect($location.path).toHaveBeenCalledWith('/auth/login');
}));
it('should redirect logged-in user to /dash/', inject(function($controller, $location) {
$controller('Homepage', { $scope: {}, AppState: { model: { currentUser: {} }} });
expect($location.path).toHaveBeenCalledWith('/dash/');
}));
});
});
'use strict';
import 'metabase/directives';
describe('corvus.directives', function() {
beforeEach(angular.mock.module('corvus.directives'));
describe('mb-scroll-shadow', function() {
var element;
beforeEach(angular.mock.inject(function($compile, $rootScope) {
element = $compile('<div mb-scroll-shadow style="height: 10px; overflow-y: scroll;"><div style="height: 20px;">x</div></div>')($rootScope);
angular.element(document.body).append(element); // must be added to the body to scrolling stuff to work
}));
it('should not add the ScrollShadow class on scroll if scrollTop is 0', function() {
element[0].scrollTop = 0;
element.triggerHandler('scroll');
expect(element.hasClass('ScrollShadow')).toBe(false);
});
it('should add the ScrollShadow class if scrollTop is greater than 0', function() {
element[0].scrollTop = 5;
element.triggerHandler('scroll');
expect(element.hasClass('ScrollShadow')).toBe(true);
});
it('should remove the ScrollShadow class on scroll if scrollTop is 0', function() {
element.addClass('ScrollShadow');
element[0].scrollTop = 0;
element.triggerHandler('scroll');
expect(element.hasClass('ScrollShadow')).toBe(false);
});
})
});
'use strict';
import 'metabase/filters';
describe('corvus.filters', function() {
beforeEach(angular.mock.module('corvus.filters'));
describe('interpolate', function() {
beforeEach(angular.mock.module(function($provide) {
$provide.value('version', 'TEST_VER');
}));
it('should replace VERSION', angular.mock.inject(function(interpolateFilter) {
expect(interpolateFilter('before %VERSION% after')).toEqual('before TEST_VER after');
}));
});
describe('slice', function() {
it('should slice the input array', angular.mock.inject(function(sliceFilter) {
expect(sliceFilter([1, 2, 3, 4], 1, 3)).toEqual([2, 3]);
}));
});
describe('isempty', function() {
it('should not replace non-empty input', angular.mock.inject(function(isemptyFilter) {
expect(isemptyFilter('non-empty', 'replaced')).toEqual('non-empty');
}));
it('should replace null input', angular.mock.inject(function(isemptyFilter) {
expect(isemptyFilter(null, 'replaced')).toEqual('replaced');
}));
it('should replace empty input', angular.mock.inject(function(isemptyFilter) {
expect(isemptyFilter('', 'replaced')).toEqual('replaced');
}));
});
});
'use strict';
import 'metabase/services';
import 'metabase/metabase/metabase.services';
describe('corvus.metabase.services', function() {
beforeEach(angular.mock.module('corvus.metabase.services'));
describe('Metabase', function() {
it('should return empty list of databases', inject(function(Metabase, $httpBackend) {
$httpBackend.expect('GET', '/api/meta/db/?org=')
.respond(200, '[]');
Metabase.db_list().$promise.then(function(data) {
expect(data.length).toEqual(0);
});
$httpBackend.flush();
}));
});
});
......@@ -14,13 +14,15 @@ var glob = require('glob');
var BASE_PATH = __dirname + '/resources/frontend_client/app/';
// All JS files except dist and test
var JS_SRC = glob.sync(BASE_PATH + '**/*.js', { ignore: BASE_PATH + '{bower_components,dist,test}/**/*.js' });
var JS_SRC = glob.sync(BASE_PATH + '**/*.js', { ignore: BASE_PATH + 'dist/**/*.js' });
// All CSS files in app/css and app/components
var CSS_SRC = glob.sync(BASE_PATH + 'css/**/*.css').concat(glob.sync(BASE_PATH + 'components/**/*.css'));
// Need to scan the CSS files for variable and custom media used across files
// NOTE: this requires "webpack -w" (watch mode) to be restarted when variables change :(
console.warn("Warning: in weback watch mode you must restart webpack if you change any CSS variables or custom media queries");
if (process.argv.indexOf("-w") >= 0 || process.argv.indexOf("--watch") >= 0) {
console.warn("Warning: in weback watch mode you must restart webpack if you change any CSS variables or custom media queries");
}
var cssMaps = { vars: {}, media: {}, selector: {} };
CSS_SRC.map(webpackPostcssTools.makeVarMap).forEach(function(map) {
for (var name in cssMaps) _.extend(cssMaps[name], map[name]);
......@@ -47,7 +49,7 @@ module.exports = {
loaders: [
// JavaScript
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel', query: { cacheDirectory: '.babel_cache' }},
{ test: /\.js$/, exclude: /node_modules/, loader: 'eslint' },
{ test: /\.js$/, exclude: /node_modules|\.spec\.js/, loader: 'eslint' },
// CSS
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader?sourceMap!cssnext-loader') }
// { test: /\.css$/, loader: 'style-loader!css-loader!cssnext-loader' }
......@@ -58,7 +60,7 @@ module.exports = {
},
resolve: {
modulesDirectories: [],
// modulesDirectories: [],
alias: {
'metabase': __dirname + '/resources/frontend_client/app',
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment