Skip to content
This repository has been archived by the owner on Jul 29, 2024. It is now read-only.

Commit

Permalink
feat(plugin): ngHint plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
sjelin committed Nov 24, 2014
1 parent cbe9b65 commit 2572feb
Show file tree
Hide file tree
Showing 11 changed files with 2,539 additions and 1 deletion.
228 changes: 228 additions & 0 deletions plugins/ngHintPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
var q = require('q'),
fs = require('fs'),
_ = require('lodash'),
ngHintNames = {
ngHintControllers: 'Controllers',
ngHintDirectives: 'Directives',
ngHintDom: 'DOM',
ngHintEvents: 'Events',
ngHintInterpolation: 'Interpolation',
ngHintModules: 'Modules',
ngHintScopes: 'Scopes',
ngHintGeneral: 'General'
};

/**
* You enable this plugin in your config file:
*
* exports.config = {
* ...
* plugins: {
* ...
* ngHint: {
* asTests: {Boolean},
* excludeURLs: {(String|RegExp)[]}
* }
* }
* }
*
* asTests specifies if the plugin should generate passed/failed test results
* based on the ngHint output or instead log the results to the console.
* Defaults to true.
*
* excludeURLs specifies a list of URLs for which ngHint results should be
* ignored. Defaults to []
*/

/*
* The stratagy for this plugin is as follows:
*
* During setup, install the ngHint code and listeners to capture its output.
* Store the output in the following format:
* {page URL} -> {module} -> {message} -> {message type}
*
* So, for instance, you might have:
* {
* 'google.com': {
* 'Controllers': {
* {'It is against Angular best practices to...': warning}
* },
* 'Modules: {
* {'Module "Search" was loaded but does not exist': error}
* }
* }
* }
*
* We store the messages as keys in objects in order to avoid duplicates.
*/

/**
* Configures the plugin to grab the output of ngHint
*
* @param {Object} config The configuration file for the ngHint plugin
* @public
*/
function setup(config) {
// Intercept ngHint output
browser.addMockModule('protractorNgHintCaptureModule_', function() {
angular.module('protractorNgHintCaptureModule_', []);
var hintInstalled = true;
if (!angular.hint) {
hintInstalled = false;
angular.hint = {};
}
/** override */
angular.hint.onMessage = function(module, message, type) {
var ngHintLog = JSON.parse(localStorage.getItem(
'ngHintLog_protractor') || '{}');
var pageLog = ngHintLog[location] || {};
var moduleLog = pageLog[module] || {};
moduleLog[message] = type;
pageLog[module] = moduleLog;
ngHintLog[location] = pageLog;
localStorage.setItem('ngHintLog_protractor',
JSON.stringify(ngHintLog));
};
if (!hintInstalled) {
angular.hint.onMessage('General', 'ngHint plugin cannot be run as ' +
'ngHint code was never included into the page', 'warning');
}
});
};

/**
* Checks if a URL should not be examined by the ngHint plugin
*
* @param {String} url The URL to check for exclusion
* @param {Object} config The configuration file for the ngHint plugin
* @return {Boolean} true if the URL should not be examined by the ngHint plugin
* @private
*/
function isExcluded(url, config) {
var excludeURLs = config.excludeURLs || [];
for (var i = 0; i < excludeURLs.length; i++) {
if (typeof excludeURLs[i] == typeof "") {
if (url == excludeURLs[i]) {
return true;
}
} else {
if (excludeURLs[i].test(url)) {
return true;
}
}
}
return false;
}

/**
* Checks if a warning message should be ignored by the ngHint plugin
*
* @param {String} message The message to check
* @return {Boolean} true if the message should be ignored
* @private
*/
function isMessageToIgnore(message) {
if (message == 'It is against Angular best practices to instantiate a ' +
'controller on the window. This behavior is deprecated in Angular ' +
'1.3.0') {
return true; // An ngHint bug, see http://git.io/S3yySQ
}

var module = /^Module "(\w*)" was created but never loaded\.$/.exec(
message);
if (module != null) {
module = module[1];
if (ngHintNames[module] != null) {
return true; // An ngHint module
}
if ((module == 'protractorBaseModule_') || (module ==
'protractorNgHintCaptureModule_')) {
return true; // A protractor module
}
}

return false;
}

/**
* Checks the information which has been stored by the ngHint plugin and
* generates passed/failed tests and/or console output
*
* @param {Object} config The configuration file for the ngHint plugin
* @return {q.Promise} A promise which resolves to the results of any passed or
* failed tests
* @public
*/
function teardown(config) {
// Get logged data
return browser.executeScript_(function() {
return localStorage.getItem('ngHintLog_protractor') || '{}';
}, 'get ngHintLog').then(function(ngHintLog) {
ngHintLog = JSON.parse(ngHintLog);

// Get a list of all the modules we tested against
var modulesUsed = _.union.apply(_, [_.values(ngHintNames)].concat(
_.values(ngHintLog).map(Object.keys)));

// Check log
var testOut = {failedCount: 0, specResults: []};
for (url in ngHintLog) {
if (!isExcluded(url, config)) {
for (var i = 0; i < modulesUsed.length; i++) {
// Add new test to output
var assertions = [];
testOut.specResults.push({
description: 'Angular Hint Test: ' + modulesUsed[i] + ' (' + url +
')',
assertions: assertions,
duration: 1
});

// Fill in the test details
var messages = ngHintLog[url][modulesUsed[i]];
if (messages) {
for (var message in messages) {
if (!isMessageToIgnore(message)) {
assertions.push({
passed: false,
errorMsg: messages[message] + " -- " + message,
stackTrace: ''
});
}
}
}

if (assertions.length == 0) {
// Pass
assertions.push({
passed: true,
errorMsg: '',
stackTrace: ''
});
} else {
// Fail
testOut.failedCount++;
}
}
}
}

// Return
if (config.asTests == false) {
for (var i = 0; i < testOut.specResults.length; i++) {
for (var j = 0; j < testOut.specResults[i].assertions.length; j++) {
var assertion = testOut.specResults[i].assertions[j];
if (!assertion.passed) {
console.log(assertion.errorMsg);
}
}
}
} else if ((testOut.failedCount > 0) || (testOut.specResults.length > 0)) {
return testOut;
}
});
}

// Export
exports.setup = setup;
exports.teardown = teardown;
17 changes: 16 additions & 1 deletion scripts/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ var passingTests = [
'node lib/cli.js spec/suitesConf.js --suite okspec',
'node lib/cli.js spec/suitesConf.js --suite okmany,okspec',
'node lib/cli.js spec/pluginsBasicConf.js',
'node lib/cli.js spec/pluginsFullConf.js'
'node lib/cli.js spec/pluginsFullConf.js',
'node lib/cli.js spec/ngHintSuccessConfig.js'
];

passingTests.push(
Expand Down Expand Up @@ -88,4 +89,18 @@ executor.addCommandlineTest('node lib/cli.js spec/errorTest/mochaFailureConf.js'
stacktrace: 'mocha_failure_spec.js:11:20'
}]);

// Check ngHint plugin

executor.addCommandlineTest('node lib/cli.js spec/ngHintFailConfig.js')
.expectExitCode(1)
.expectErrors([{
message: 'warning -- ngHint plugin cannot be run as ngHint code was ' +
'never included into the page'
}, {
message: 'warning -- ngHint is included on the page, but is not active ' +
'because there is no `ng-hint` attribute present'
}, {
message: 'warning -- Module "xApp" was created but never loaded.'
}]);

executor.execute();
7 changes: 7 additions & 0 deletions spec/ngHint/fail_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
describe('check if ngHint plugin works on bad apps', function() {
it('should have ngHint problems on bad apps', function() {
browser.get("ngHint/noNgHint.html");
browser.get("ngHint/noTag.html");
browser.get("ngHint/unused.html");
});
});
5 changes: 5 additions & 0 deletions spec/ngHint/success_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
describe('check if ngHint plugin works on good apps', function() {
it('should not have ngHint problems on a good app', function() {
browser.get("ngHint/index.html");
});
});
10 changes: 10 additions & 0 deletions spec/ngHintFailConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
var env = require('./environment.js');

exports.config = {
seleniumAddress: env.seleniumAddress,
specs: ['ngHint/fail_spec.js'],
baseUrl: env.baseUrl,
plugins: [{
path: "../plugins/ngHintPlugin.js"
}]
};
10 changes: 10 additions & 0 deletions spec/ngHintSuccessConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
var env = require('./environment.js');

exports.config = {
seleniumAddress: env.seleniumAddress,
specs: ['ngHint/success_spec.js'],
baseUrl: env.baseUrl,
plugins: [{
path: "../plugins/ngHintPlugin.js"
}]
};
21 changes: 21 additions & 0 deletions testapp/ngHint/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>

<html ng-app="xApp" ng-hint>
<head>
<meta charset="utf-8">
<title>Angular.js Example</title>
<!-- Use version Angular 1.2.x since ngHint doesn't work with 1.3.x -->
<script src="../lib/angular_v1.2.9/angular.min.js"></script>
<script src="ngHint.js"></script>
<script>
angular.module('xApp', []);
</script>
</head>
<body>
First name:<input ng-model="firstName" type="text"/>
<br>
Last name:<input ng-model="lastName" type="text"/>
<br>
Hello {{firstName}} {{lastName}}
</body>
</html>
Loading

0 comments on commit 2572feb

Please sign in to comment.