diff --git a/README.md b/README.md index 0832b8c..1da5838 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,114 @@ -Validator +garðr-validator ========= + +Collect data from a display-ads lifecyle and validate the data. This project tries to give a nice framework for doing this. + [![Build Status](https://travis-ci.org/gardr/validator.png)](https://travis-ci.org/gardr/validator) [![Coverage Status](https://coveralls.io/repos/gardr/validator/badge.png)](https://coveralls.io/r/gardr/validator) [![NPM version](https://badge.fury.io/js/gardr-validator.png)](http://badge.fury.io/js/gardr-validator) [![Dependency Status](https://david-dm.org/gardr/validator.png)](https://david-dm.org/gardr/validator) [![devDependency Status](https://david-dm.org/gardr/validator/dev-status.png)](https://david-dm.org/gardr/validator#info=devDependencies) -# Installation +#### Installation $ npm install -# Contributing +### Examples + +See the web-gui for this project: https://github.com/gardr/validator-web/blob/master/lib/routes/validate.js#L284 + + +#### Writing intrumentation and validators + +##### Intrument / Hook example filename: 'someData.js': + module.exports = { + 'onBeforeExit': function (api, config) { + api.switchToIframe(); + if (config.someConfigBoolean){ + api.set('collectedData', api.evaluate(function(config){ + return window.someData; + }, config)); + } + } + }; + +##### Preprocessor example filename 'fixSomethingAsync.js' + module.exports = { + 'dependencies': ['someData'], + 'preprocess': function(harvested, output, next, globalOptions){ + output('someKey', {data: harvested.someData||{}}); + setTimeout(next, 1); + } + }; + +##### Validator example filename 'someData.js' + module.exports = { + 'preprocessors': [ + 'fixSomethingAsync' + ], + 'dependencies': [ + 'someData' + ], + 'validate': function(harvested, report, next, globalOptions){ + if (this.someConfigBoolean){ + if (harvested.someData){ + report.error('Some message'); + } + } + } + }; + +##### Adding instrumentation/hooks etc to a run + + var run = require('gardr-validator'); + var options = { + 'include': [ + { + name: 'someData', + path: '/resolved/path/to/someData.js' + } + ], + 'config':{ + 'someData': { + 'someConfigBoolean': true + } + } + }; + run(options, function(phantomError, harvest, report){ + if (phantomError){ + // do something + return; + } + assert(harvest.someData); + assert(harvest.someKey); + assert(report.errors.length === 1); + }) + +#### Options to runner + { + instrument: [ + 'actions', // defaults to files in /lib/rule/instrument/actions.js + {name: 'css'}, + {name: 'custom', path: '/absolute/path/to/file'}, + {name: 'custom2', code: 'var someCode = "";'} + ], + preprocess: [ + //.. + ], + validate: [ + //.. + ] + //rest of runner default options, see /config folder. + } + + +#### Contributing +YES, pull requests with tests. Be sure to create a issue and let us know you are working on it - maybe we can help out with insights etc. ## Running tests $ npm test +##### Alternatives + +(please let us know of alternatives to this project) diff --git a/config/config.js b/config/config.js new file mode 100644 index 0000000..9df50fb --- /dev/null +++ b/config/config.js @@ -0,0 +1,72 @@ +var pathLib = require('path'); + +function resolve(url) { + var args = [__dirname, '..', 'lib', 'phantom'].concat(url.split('/')); + var result = pathLib.join.apply(null, args); + return pathLib.resolve(result); +} + +var validatorConfig = require('./validatorConfig.js'); + +module.exports = { + parentUrl: resolve('resources/parent.html'), + iframeUrl: resolve('resources/iframe.html'), + + + validatorBase: null, + + instrument: [ + // 'errors', // common + // 'har', // common + // 'log', + // 'actions', + // 'css', + // 'script', + // 'screenshots', + // 'timers', + // 'jquery', + // 'gardr', + // 'touch' + ], + preprocess: [ + 'screenshots', + 'har' + ], + validate : [ + 'common', + 'log' , + 'css' , + 'timers', + 'jquery', + 'gardr', + 'sizes', + 'codeUsage', + 'touch' + ], + + + // config for hooks etc, namespace for convenience. + config: validatorConfig, + + viewport: { + width: 980, + height: 225 + }, + + width: { + min: 980, + max: 980 + }, + height: { + min: 225, + max: 225 + }, + + // used for requests - fetching new resources + headers: { + 'Cache-Control': 'no-cache', + 'Accept-Encoding': 'identify' + }, + pageRunTime: 12000, + userAgent: 'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25' +}; diff --git a/config/validatorConfig.js b/config/validatorConfig.js new file mode 100644 index 0000000..932142f --- /dev/null +++ b/config/validatorConfig.js @@ -0,0 +1,75 @@ +module.exports = { + actions: { + trigger: { + click: true, + mouseover: true + }, + trackWindowOpen: true + }, + screenshots: { + ms: 25, + onlyUnique: true + }, + scripts: { + collectAttributes: true + }, + codeUsage: { + geolocation: { + active: true, + trackAfterInteraction: false + } + }, + css: { + strictRules: true, + filterOutStyleTagsWith: '* { padding: 0; margin: 0; border: 0; }' + }, + errors: { + //allowedErrors: 0 + }, + gardr: { + // takes input from viewportOptions + iframeNotAllowed: true, + enforceStyling: true, + enforceSpec: true, + illegalTags: ['meta[name=\"viewport\"]'] + }, + jquery: { + versionsBack: 1, + wrapAnimate: true + }, + log: { + //output logs to view maybe? + }, + sizes: { + //refetchResources: true, // processReources.js + //filterAfterUserInteraction: true, // onHalfTime triggers actions + thresholdBytes: 100000, // bytes gziped + giveExtraThreshold: { + jQuery: true, + jQueryThreshold: 33369, + AdForm: true + }, + minimumPayloadSize: 100, + maxRequests: { + style: 0, + script: 2, + errors: 0, + image: 20, + other: 3 + } + }, + timers: { + nameToTriggerWrap: 'iframe.htm', + setTimeout: 20, + setInterval: 1, + requestAnimationFrame: 0 + }, + touch: { + swipeTop: true, + swipeRight: true, + swipeLeft: true, + frames: 20, + swipeTime: 250, + delayBeforeNext: 1800 + } +}; diff --git a/index.js b/index.js index bc14860..bd74a3d 100644 --- a/index.js +++ b/index.js @@ -1 +1,3 @@ -module.exports = require('./lib/index.js').run; +var lib = require('./lib/index.js'); +module.exports = lib.run; +module.exports.defaults= lib.defaults; diff --git a/lib/helpers.js b/lib/helpers.js index 6d0dae1..c9f7833 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -1,38 +1,50 @@ -var path = require('path'); var fs = require('fs'); +var pathLib = require('path'); var async = require('async'); -var RULE_RUNNER_BASE = path.join(__dirname, '.', 'rule'); -var HOOK_BASE = path.join(RULE_RUNNER_BASE, 'hook'); -var VALIDATOR_BASE = path.join(RULE_RUNNER_BASE, 'validator'); -var PRE_BASE = path.join(RULE_RUNNER_BASE, 'preprocessor'); +var internals = {}; -function collect(base){ - return function(spec){ - return Object.keys(spec).map(function (key) { - var res; - if (typeof spec[key] === 'string') { - res = spec[key]; - } else { - // default to validator root dirname - res = path.join(base, key + '.js'); - } - return res; - }); +internals.mapEntry = function (type) { + return function (entry) { + if (typeof entry === 'object') { + // maybe validate??? + return entry; + } + + if (typeof entry === 'string') { + return { + 'name': entry, + 'path': pathLib.join(__dirname, '.', 'rule', type, entry + '.js') + }; + } else { + throw new Error('Wrong configuration of includes'); + } }; -} +}; -var collectSpec = collect(HOOK_BASE); -var collectValidator = collect(VALIDATOR_BASE); -var collectPreprocessor = collect(PRE_BASE); +internals.collect = function (type) { + return function (specList) { + if (!Array.isArray(specList)){ + console.log('specList'.red, specList); + throw new TypeError('Should send in a list'); + } + return specList.map(internals.mapEntry(type)); + }; +}; -function statFiles(list, done) { +internals.statFiles = function (list, done) { return async.map(list, fs.stat, done); -} +}; module.exports = { - collectSpec: collectSpec, - collectValidator: collectValidator, - collectPreprocessor: collectPreprocessor, - statFiles: statFiles + 'mapEntry': internals.mapEntry, + 'collect': function (parent) { + parent.instrument = internals.collect('instrument')(parent.instrument); + parent.preprocess = internals.collect('preprocess')(parent.preprocess); + parent.validate = internals.collect('validate')(parent.validate); + }, + 'collectSpec': internals.collect('instrument'), + 'collectValidator': internals.collect('validate'), + 'collectPreprocessor': internals.collect('preprocess'), + 'statFiles': internals.statFiles }; diff --git a/lib/index.js b/lib/index.js index 300d138..d41014b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,39 +1,113 @@ 'use strict'; +var hoek = require('hoek'); +var pathLib = require('path'); var helpers = require('./helpers.js'); var validate = require('./validate.js').validate; var spawn = require('./spawn.js'); -var path = require('path'); +var defaults = require('../config/config.js'); -var validatorBase = path.resolve(path.join(__dirname, '..')); +var internals = {}; -function run(options, callback) { - if (!options || !options.hooks) { - return callback(new Error('Missing spec')); +var validatorBase = pathLib.resolve(pathLib.join(__dirname, '..')); + +internals.prepareOptions = function(options){ + if (options.hooks || options.preprocessors || options.validators){ + throw new Error('Deprecated keys'); + } + // convert includes into validators + if (options.include){ + if (options.validate){ + throw new Error('Include is a alias for validate, do not use both'); + } + options.validate = options.include; + delete options.include; + } +}; + + +internals.resolveIncludes = function(options){ + + function hasDependency(dependencyName, type){ + return options[type].some(function(entry){ + return dependencyName && dependencyName === entry.name; + }); + } + + function addDependency(dependencyName, type){ + type = typeof type === 'string' ? type : 'instrument'; + if (!options[type]){ + options[type] = [ + helpers.mapEntry(type)(dependencyName) + ]; + } else if (hasDependency(dependencyName, type) !== true) { + options[type].push( + helpers.mapEntry(type)(dependencyName) + ); + } + } + + function readDependencies(entry){ + // console.log('readDependencies'.blue, entry.name); + if (!entry.path){ + return; + } + try{ + var mod = require(entry.path); + if (mod.dependencies){ + entry.dependencies = mod.dependencies; + entry.dependencies.forEach(addDependency); + } + if (mod.preprocessors){ + entry.preprocessors = mod.preprocessors; + entry.preprocessors.forEach(function(dependencyName){ + addDependency(dependencyName, 'preprocess'); + }); + } + // console.log('readDependencies()',entry.name,' result:'.blue, entry.dependencies && entry.dependencies.length); + } catch(e){ + console.log('failed reading module '+entry.name, entry); + throw e; + } } + + // normalize + helpers.collect(options); + + // resolve dependencies - starting with requiring all validators and preprocessors, and collecting dependencies. + // todo: dependencies relative to same path? ./ + options.validate.forEach(readDependencies); + options.preprocess.forEach(readDependencies); +}; +internals.run = function (options, callback) { if (!callback){ throw new Error('Missing callback'); } - options.validatorBase = validatorBase; - options.hooks = helpers.collectSpec(options.hooks); - - if (options.validators) { - options.validatorFiles = helpers.collectValidator(options.validators); - } else { - options.validatorFiles = []; + if (!options){ + return callback(new Error('Missing options')); } - if (options.preprocessors) { - options.preprocessorFiles = helpers.collectPreprocessor(options.preprocessors); + + internals.prepareOptions(options); + + options = hoek.applyToDefaults(defaults, options); + + internals.resolveIncludes(options); + + if (!options.instrument || options.instrument.length === 0) { + return callback(new Error('Missing hooks/instrumentation from configuration')); } - helpers.statFiles(options.hooks, function (err) { + options.validatorBase = validatorBase; + + var byPath = function(o){return o.path;}; + helpers.statFiles(options.instrument.map(byPath), function (err) { if (err) { console.log('statFiles error', err); return callback(err, null); } // spawn phantomJS process - spawn(options, handleResult, function(processError, harvestedData){ + spawn(options, internals.handleResult, function(processError, harvestedData){ if (processError){ console.log('Spawn error', processError); callback(processError); @@ -42,11 +116,11 @@ function run(options, callback) { } }); }); -} +}; // handle result from phantomJS process -function handleResult(jsonResult, callback, parentError) { //Todo logic around parentError. +internals.handleResult = function (jsonResult, callback, parentError) { //Todo logic around parentError. var result = null; var error = null; @@ -85,10 +159,10 @@ function handleResult(jsonResult, callback, parentError) { //Todo logic around p } callback(error, result); - -} +}; module.exports = { - handleResult: handleResult, - run: run + 'defaults': defaults, + 'handleResult': internals.handleResult, + 'run': internals.run }; diff --git a/lib/phantom/createHooks.js b/lib/phantom/createHooks.js index 6b9ed08..18e4892 100644 --- a/lib/phantom/createHooks.js +++ b/lib/phantom/createHooks.js @@ -46,7 +46,17 @@ function requireFilesAndMapToLists(files) { // its possible to inject object instead of file reference var mod; if (typeof file === 'object'){ - mod = file; + if (file.code){ + // TODO code as string + // mod; + } else if (file.path){ + mod = require(file.path); + if(!mod.name){ + mod.name = getFileName(file.path); + } + } else { + mod = file; + } } else { mod = require(file); if(!mod.name){ @@ -55,7 +65,7 @@ function requireFilesAndMapToLists(files) { } if (!mod.name){ - throw new Error('Missing name'); + throw new Error('Missing instrument filename from module exports.'); } Object.keys(mod).forEach(function (key) { @@ -65,7 +75,10 @@ function requireFilesAndMapToLists(files) { hooks[key] = hooks[key] || []; // PUSH CALLBACK - hooks[key].push({callback: mod[key], name: mod.name}); + hooks[key].push({ + 'callback': mod[key], + 'name': mod.name + }); }); }); } @@ -90,7 +103,11 @@ function createPartialWrapper(hook, callbacks, api) { function handler(data) { if (typeof data.callback === 'function') { try { - var apiContext = api.createSubContext(data.name); + var options = api.getOptions(); + var apiContext = [ + api.createSubContext(data.name), + options && options.config[data.name] + ]; data.callback.apply(self, args.concat(apiContext)); } catch (e) { if (api && api.getGlobalResult) { @@ -111,7 +128,7 @@ function createPartialWrapper(hook, callbacks, api) { } module.exports = function (page, options, api) { - var callers = requireFilesAndMapToLists(options.hooks); + var callers = requireFilesAndMapToLists(options.instrument); //if (result) result.callers = Object.keys(callers); var alreadyHookedUp = {}; diff --git a/lib/phantom/defaults.js b/lib/phantom/defaults.js deleted file mode 100644 index 49fe8c8..0000000 --- a/lib/phantom/defaults.js +++ /dev/null @@ -1,20 +0,0 @@ -var pathLib = require('path'); - -function resolve(url) { - var args = [__dirname].concat(url.split('/')); - var result = pathLib.join.apply(null, args); - return pathLib.resolve(result); -} - -module.exports = { - headers: { - 'Cache-Control': 'no-cache', - 'Accept-Encoding': 'identify' - }, - width: 980, - height: 225, - parentUrl: resolve('resources/parent.html'), - iframeUrl: resolve('resources/iframe.html'), - pageRunTime: 12000, - userAgent: 'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25' -}; diff --git a/lib/phantom/main.js b/lib/phantom/main.js index 0cb16f3..ba89ba2 100644 --- a/lib/phantom/main.js +++ b/lib/phantom/main.js @@ -104,8 +104,8 @@ if (system.args.length <= 1) { page.customHeaders = opt.headers; page.settings.userAgent = opt.userAgent; page.viewportSize = { - width: opt.width, - height: opt.height + width: opt.viewport.width, + height: opt.viewport.height }; page.javaScriptConsoleMessageSent('Page.open starting, opening up '+opt.parentUrl); diff --git a/lib/phantom/resources/gardr-manager.js b/lib/phantom/resources/gardr-manager.js index 71dedfd..14deefb 100644 --- a/lib/phantom/resources/gardr-manager.js +++ b/lib/phantom/resources/gardr-manager.js @@ -27,6 +27,10 @@ if (!Function.prototype.bind) { window.initManager = function (options) { options = JSON.parse(options); + if (!options.scriptUrl){ + throw new Error('Missing scriptUrl from gardr-manager'); + } + var manager = window.__manager = getManager({ iframeUrl: options.iframeUrl }); @@ -50,8 +54,8 @@ window.initManager = function (options) { manager.queue('phantom', { url: options.scriptUrl, container: 'ADS', - height: options.height, - width: options.width + height: options.viewport.height, + width: options.viewport.width }); manager.renderAll(function (err, result) { diff --git a/lib/rule/hook/screenshots.js b/lib/rule/hook/screenshots.js deleted file mode 100644 index e83ed02..0000000 --- a/lib/rule/hook/screenshots.js +++ /dev/null @@ -1,21 +0,0 @@ -var images = []; - -module.exports = { - 'onPageOpen': function (api) { - var options = api.getOptions(); - - var base = options.outputDirectory + '/' + options.width + 'x' + options.height + '_'; - - function handler() { - var image = api.getPNG(); - if (image !== images[images.length - 1]) { - api.renderToFile(base + Date.now() + '.png'); - images.push(image); - } - // loop - window.setTimeout(handler, 25); - } - - window.setTimeout(handler, 25); - } -}; diff --git a/lib/rule/hook/actions.js b/lib/rule/instrument/actions.js similarity index 68% rename from lib/rule/hook/actions.js rename to lib/rule/instrument/actions.js index 12485fe..89a922e 100644 --- a/lib/rule/hook/actions.js +++ b/lib/rule/instrument/actions.js @@ -1,4 +1,4 @@ -function performUserInteraction() { +function performUserInteraction(config) { function action(element, type) { if (element === null) { @@ -19,15 +19,28 @@ function performUserInteraction() { } var element = document.body.firstChild; + if (!element){ + element = document.body; + } var banner = element.querySelector('div[data-responsive],div[onclick],div,a'); if (!banner) { banner = element.firstChild; } - action(banner, 'mouseover'); - setTimeout(function(){ - action(banner, 'click'); - }, 100); + + if (!banner) { + return 'Missing element'; + } + + if (config.trigger.mouseover){ + action(banner, 'mouseover'); + } + if (config.trigger.click){ + setTimeout(function(){ + action(banner, 'click'); + }, 100); + } + } function wrapWindowOpen() { @@ -59,18 +72,22 @@ function collectWindowOpen() { } module.exports = { - 'onHalfTime': function (api) { + 'onHalfTime': function (api, config) { api.set('actionTime', +new Date()); api.switchToIframe(); - api.evaluate(wrapWindowOpen); + if (config.trackWindowOpen){ + api.evaluate(wrapWindowOpen, config); + } setTimeout(function () { api.switchToIframe(); - api.evaluate(performUserInteraction); + api.set('actionResult', api.evaluate(performUserInteraction, config)); }, 50); }, - 'onBeforeExit': function (api) { + 'onBeforeExit': function (api, config) { api.switchToIframe(); - api.set('windowOpened', api.evaluate(collectWindowOpen)); + if (config.trackWindowOpen){ + api.set('windowOpened', api.evaluate(collectWindowOpen, config)); + } } }; diff --git a/lib/rule/hook/errors.js b/lib/rule/instrument/common.js similarity index 89% rename from lib/rule/hook/errors.js rename to lib/rule/instrument/common.js index a13a95d..10815d8 100644 --- a/lib/rule/hook/errors.js +++ b/lib/rule/instrument/common.js @@ -2,7 +2,7 @@ module.exports = { 'name': 'common', 'onError': function (msg, trace, api) { api.setPush('errors', { - type: 'hook/error::page.onError', + type: 'instrument/error::page.onError', date: Date.now(), message: msg, trace: trace.map(function (entry) { diff --git a/lib/rule/hook/css.js b/lib/rule/instrument/css.js similarity index 73% rename from lib/rule/hook/css.js rename to lib/rule/instrument/css.js index c06d3d6..dcb0a61 100644 --- a/lib/rule/hook/css.js +++ b/lib/rule/instrument/css.js @@ -1,8 +1,8 @@ module.exports = { - 'onBeforeExit': function (api) { - function getStyles() { + 'onBeforeExit': function (api, config) { + function getStyles(config) { var filterOut = [ - '* { padding: 0; margin: 0; border: 0; }' + config.filterOutStyleTagsWith ]; function filterOutGardr(v) { @@ -19,6 +19,6 @@ module.exports = { } api.switchToIframe(); - api.set('styles', api.evaluate(getStyles)); + api.set('styles', api.evaluate(getStyles, config)); } }; diff --git a/lib/rule/hook/gardr.js b/lib/rule/instrument/gardr.js similarity index 76% rename from lib/rule/hook/gardr.js rename to lib/rule/instrument/gardr.js index 2798870..7d7947c 100644 --- a/lib/rule/hook/gardr.js +++ b/lib/rule/instrument/gardr.js @@ -1,4 +1,4 @@ -function collectCSS() { +function collectCSS(config) { var KEYS = ['height', 'width', 'position', 'left', 'right', 'top', 'bottom', 'z-index', 'display', 'visability']; // wrapper element var element = document.getElementById('GARDR'); @@ -56,7 +56,7 @@ function collectCSS() { }; } - var illegal = ['meta[name="viewport"]'].filter(filterFound).map(report); + var illegal = [config.illegalTags].filter(filterFound).map(report); return { illegal: illegal, @@ -66,27 +66,13 @@ function collectCSS() { } module.exports = { - 'onPageOpen': function onPageOpen(api) { - - var strOptions = JSON.stringify(api.getOptions()); - + 'onPageOpen': function onPageOpen(api/*, config*/) { api.evaluate(function (options) { - // TODO move - // var resolveFilepathsToOptions = { - // parentUrl: 'lib/report/resources/parent.html', - // iframeUrl: 'lib/report/resources/iframe.html', - // }; - - // Object.keys(resolveFilepathsToOptions).forEach(function (key) { - // options[key] = resolve(resolveFilepathsToOptions[key]); - // }); - - window.initManager(options); - }, strOptions); + }, JSON.stringify(api.getOptions())); }, - 'onBeforeExit': function (api) { + 'onBeforeExit': function (api, config) { // api.switchToMainFrame(); @@ -101,8 +87,10 @@ module.exports = { }; } - api.set('data', api.evaluate(gardrData)); + // get Gardr Host/Manager data + api.set('data', api.evaluate(gardrData, config)); + // inspect iframe dom api.switchToIframe(); - api.set('dom', api.evaluate(collectCSS)); + api.set('dom', api.evaluate(collectCSS, config)); } }; diff --git a/lib/rule/hook/har.js b/lib/rule/instrument/har.js similarity index 100% rename from lib/rule/hook/har.js rename to lib/rule/instrument/har.js diff --git a/lib/rule/hook/jquery.js b/lib/rule/instrument/jquery.js similarity index 100% rename from lib/rule/hook/jquery.js rename to lib/rule/instrument/jquery.js diff --git a/lib/rule/hook/log.js b/lib/rule/instrument/log.js similarity index 83% rename from lib/rule/hook/log.js rename to lib/rule/instrument/log.js index 8972bf8..e69a2dd 100644 --- a/lib/rule/hook/log.js +++ b/lib/rule/instrument/log.js @@ -1,7 +1,7 @@ module.exports = { 'onConsoleMessage': function saveLog(msg, lineNum, sourceId, api) { var entry = { - type: 'hook/log:page.onConsoleMessage', + type: 'instrument/log:page.onConsoleMessage', time: Date.now(), message: msg, sourceId: sourceId, diff --git a/lib/rule/instrument/screenshots.js b/lib/rule/instrument/screenshots.js new file mode 100644 index 0000000..cdc7273 --- /dev/null +++ b/lib/rule/instrument/screenshots.js @@ -0,0 +1,24 @@ +var images = []; + +module.exports = { + 'onPageOpen': function (api, config) { + var options = api.getOptions(); + + var base = options.outputDirectory + '/' + options.viewport.width + 'x' + options.viewport.height + '_'; + + function handler() { + var image = api.getPNG(); + if (config.onlyUnique === true && image !== images[images.length - 1]) { + api.renderToFile(base + Date.now() + '.png'); + images.push(image); + } + if (config.onlyUnique !== true) { + api.renderToFile(base + Date.now() + '.png'); + } + // loop + window.setTimeout(handler, config.ms); + } + + window.setTimeout(handler, config.ms); + } +}; diff --git a/lib/rule/hook/script.js b/lib/rule/instrument/script.js similarity index 100% rename from lib/rule/hook/script.js rename to lib/rule/instrument/script.js diff --git a/lib/rule/hook/timers.js b/lib/rule/instrument/timers.js similarity index 82% rename from lib/rule/hook/timers.js rename to lib/rule/instrument/timers.js index 0076cde..1361a66 100644 --- a/lib/rule/hook/timers.js +++ b/lib/rule/instrument/timers.js @@ -15,8 +15,8 @@ function wrapTimeouts(api) { } module.exports = { - 'onResourceReceived': function (response, api) { - if (response.stage === 'end' && response.url.indexOf('iframe.htm') > -1) { + 'onResourceReceived': function (response, api, config) { + if (response.stage === 'end' && response.url.indexOf(config.nameToTriggerWrap) > -1) { wrapTimeouts(api); } }, diff --git a/lib/rule/hook/touch.js b/lib/rule/instrument/touch.js similarity index 58% rename from lib/rule/hook/touch.js rename to lib/rule/instrument/touch.js index 924e5fd..a2eeeab 100644 --- a/lib/rule/hook/touch.js +++ b/lib/rule/instrument/touch.js @@ -1,10 +1,10 @@ var KEY = '__tests'; -function onHalfTime(api) { +function onHalfTime(api, config) { api.set('actionTime', +new Date()); api.switchToIframe(); api.injectLocalJs('./../../node_modules/mock-phantom-touch-events/lib/index.js'); - api.evaluate(function (key) { + api.evaluate(function (key, config) { function collectEvents(subKey) { return function (synteticEvent) { @@ -22,52 +22,30 @@ function onHalfTime(api) { }; } - try { - // window.touchActionSequence( - // document.getElementById('GARDR').getElementsByTagName('div')[0], - // [85, 80], - // [400, 45], - // 750, - // 50, - // collectEvents('testSwipe') - // ); - - var gardrElem = document.getElementById('GARDR'); - if (gardrElem !== null) { - var elem = gardrElem.querySelector('div[onclick],div'); - - window.swipeTop( - elem, - 250, - 20, - collectEvents('swipeTop') - ); - setTimeout(function () { - window.swipeRight( - elem, - 250, - 20, - collectEvents('swipeRight') - ); + var possibleActions = ['swipeTop', 'swipeRight', 'swipeLeft', 'swipeBottom']; + var actions = possibleActions.filter(function(key){ + return config[key] === true; + }); - setTimeout(function () { - window.swipeLeft( - elem, - 250, - 20, - collectEvents('swipeLeft') - ); - }, 1800); - }, 1800); + var current = 0; + function doNext(elem){ + var key = possibleActions[current]; + window[key](elem, config.swipeTime, config.frames, collectEvents(key)); + current++; + if (current < actions.length){ + setTimeout(function(){ doNext(elem); }, config.delayBeforeNext); } + } + try { + doNext(document.getElementById('GARDR').querySelector('div[onclick],div')); } catch (e) { console.log('Failed swiping', e, e.message, e.stack); } - }, KEY); + }, KEY, config); } -function onBeforeExit(api) { +function onBeforeExit(api/*, config*/) { var probed = api.evaluate(function (key) { if (!window[key]) { return { @@ -79,6 +57,7 @@ function onBeforeExit(api) { window[key][_key][_method].forEach(function (e) { e.returnValue = e.__event.returnValue; e.defaultPrevented = e.__event.defaultPrevented; + // cleanup event delete e.__event; }); }); diff --git a/lib/rule/lib/getLatestJquery.js b/lib/rule/lib/getLatestJquery.js index 1ae17f4..80f5c23 100644 --- a/lib/rule/lib/getLatestJquery.js +++ b/lib/rule/lib/getLatestJquery.js @@ -1,9 +1,9 @@ -//var log = require('../../logger.js'); - var request = require('request'); + +var internals = {}; var RE_VERSIONS = /(\d{1,2})\.(\d{1,2})\.?(\d{1,2})?/; -function createVersionObj(versionStr) { +internals.createVersionObj = function (versionStr) { var items = versionStr.match(RE_VERSIONS); var res = { major: parseInt(items[1], 10) || 0, @@ -11,39 +11,43 @@ function createVersionObj(versionStr) { patch: parseInt(items[3], 10) || 0 }; - res.sortKey = [format(res.major), format(res.minor), format(res.patch)].join('') * 1; + res.sortKey = [ + internals.format(res.major), + internals.format(res.minor), + internals.format(res.patch) + ].join('') * 1; return res; -} +}; -function unique(v, i, arr) { +internals.unique = function (v, i, arr) { return arr.lastIndexOf(v) === i; -} +}; -function format(num) { +internals.format = function (num) { return num > 9 ? '' + num : '0' + num; -} +}; -function reverseBySortKey(a, b){return a.sortKey < b.sortKey ? 1 : -1;} -function orderBySortKey(a, b) { return a.sortKey > b.sortKey ? 1 : -1;} +internals.reverseBySortKey = function (a, b){return a.sortKey < b.sortKey ? 1 : -1;}; +internals.orderBySortKey = function (a, b) { return a.sortKey > b.sortKey ? 1 : -1;}; var RE_LETTER = /[a-z]+/gim; -function filterTags(tags) { +internals.filterTags = function (tags) { tags = tags.map(function (o) { return o.name; }) .filter(function (name) { return !name.match(RE_LETTER); // ignore beta releases etc }) - .map(createVersionObj).sort(orderBySortKey); + .map(internals.createVersionObj).sort(internals.orderBySortKey); // get majors, e.g. [1, 2] - var major = tags.map(function (v) { return v.major;}).filter(unique); + var major = tags.map(function (v) { return v.major;}).filter(internals.unique); // get latest 2 versions from majors var correctTags = major.map(function (version) { var match; - tags.sort(reverseBySortKey).some(function (o) { + tags.sort(internals.reverseBySortKey).some(function (o) { if (!match && o.major === version) { match = o; return true; @@ -53,14 +57,15 @@ function filterTags(tags) { }); return correctTags; -} +}; var cached; -function getLatest(cb) { +internals.getLatest = function (cb) { if (cached){ return cb(cached); } - request('https://api.github.com/repos/jquery/jquery/tags', {headers: {'User-Agent': 'gardr/validator-web'}}, function (err, res, body) { + var opt = {'timeout': 5000, 'headers': {'User-Agent': 'gardr/validator-web'}}; + request('https://api.github.com/repos/jquery/jquery/tags', opt, function (err, res, body) { if (err){ //log.error('Failed requesting jquery version info form github:', err); } @@ -69,7 +74,7 @@ function getLatest(cb) { if (body){ try { data = JSON.parse(body); - data = filterTags(data); + data = internals.filterTags(data); cached = data; } catch (e) { // abit dirty @@ -80,9 +85,9 @@ function getLatest(cb) { cb(data); }); -} +}; module.exports = { - getLatest: getLatest, - createVersionObj: createVersionObj + getLatest: internals.getLatest, + createVersionObj: internals.createVersionObj }; diff --git a/lib/rule/preprocessor/har.js b/lib/rule/preprocess/har.js similarity index 89% rename from lib/rule/preprocessor/har.js rename to lib/rule/preprocess/har.js index e206453..12b47aa 100644 --- a/lib/rule/preprocessor/har.js +++ b/lib/rule/preprocess/har.js @@ -1,10 +1,8 @@ /* */ - var createHAR = require('../../createHAR.js'); var version = require('../../../package.json').version; var processResources = require('./processResources.js'); - module.exports = { dependencies: ['har'], preprocess: function (harvested, output) { @@ -15,6 +13,10 @@ module.exports = { var input = harvested.har.input; + if (!input || !input.resources){ + throw new Error('Missing HAR resources'); + } + var raw = { 'resources': input.resources.filter(filterHttp), 'startTime': input.startTime diff --git a/lib/rule/preprocessor/jquery.js b/lib/rule/preprocess/jquery.js similarity index 68% rename from lib/rule/preprocessor/jquery.js rename to lib/rule/preprocess/jquery.js index 3ec75b7..b563c4b 100644 --- a/lib/rule/preprocessor/jquery.js +++ b/lib/rule/preprocess/jquery.js @@ -12,9 +12,9 @@ function getLastest(harvested, output, next) { module.exports = { dependencies: ['jquery'], - preprocess: function (harvested, output, next, options) { - if (harvested.jquery.version) { - getLastest(harvested, output, next, options); + preprocess: function (harvested, output, next) { + if (harvested.jquery && harvested.jquery.version) { + getLastest.apply(this, Array.prototype.slice.call(arguments)); } else { next(); } diff --git a/lib/rule/preprocessor/log.js b/lib/rule/preprocess/log.js similarity index 100% rename from lib/rule/preprocessor/log.js rename to lib/rule/preprocess/log.js diff --git a/lib/rule/preprocessor/processResources.js b/lib/rule/preprocess/processResources.js similarity index 91% rename from lib/rule/preprocessor/processResources.js rename to lib/rule/preprocess/processResources.js index 6214467..bd25ed6 100644 --- a/lib/rule/preprocessor/processResources.js +++ b/lib/rule/preprocess/processResources.js @@ -3,7 +3,9 @@ var request = require('request'); var zlib = require('zlib'); var hoek = require('hoek'); -function each(rawFileData, fn) { +var internals = {}; + +internals.each = function (rawFileData, fn) { var keys = Object.keys(rawFileData); var iterList = keys.map(function (key) { return { @@ -23,11 +25,11 @@ function each(rawFileData, fn) { } return list; -} +}; -function getSummary(rawFileData) { +internals.getSummary = function (rawFileData) { - var list = each(rawFileData); + var list = internals.each(rawFileData); function getRedirects() { var _list = list('filter', function (key, value) { @@ -85,12 +87,12 @@ function getSummary(rawFileData) { }; return { - total: total, - tips: tips + 'total': total, + 'tips': tips }; -} +}; -function getTypeSummary(rawFileData) { +internals.getTypeSummary = function (rawFileData) { var types = { 'script': {}, 'style': {}, @@ -111,21 +113,21 @@ function getTypeSummary(rawFileData) { ); } - each(rawFileData, function (key, value) { + internals.each(rawFileData, function (key, value) { types[getType(value)][key] = value; }); var summary = {}; - each(types, function (key, value) { - summary[key] = getSummary(value); + internals.each(types, function (key, value) { + summary[key] = internals.getSummary(value); }); return { - types: types, - summary: summary + 'types': types, + 'summary': summary }; -} +}; /* "entries": [ @@ -263,8 +265,8 @@ module.exports = function (harvested, outputFn, nextValidator, options) { function doneHandler() { // output - var rawFileDataSummary = getSummary(rawFileData); - rawFileDataSummary.typed = getTypeSummary(rawFileData); + var rawFileDataSummary = internals.getSummary(rawFileData); + rawFileDataSummary.typed = internals.getTypeSummary(rawFileData); outputFn('har', 'rawFileData', rawFileData); outputFn('har', 'rawFileDataSummary', rawFileDataSummary); diff --git a/lib/rule/preprocessor/screenshots.js b/lib/rule/preprocess/screenshots.js similarity index 87% rename from lib/rule/preprocessor/screenshots.js rename to lib/rule/preprocess/screenshots.js index bc309f6..4cdf1b3 100644 --- a/lib/rule/preprocessor/screenshots.js +++ b/lib/rule/preprocess/screenshots.js @@ -2,9 +2,11 @@ var fs = require('fs'); var async = require('async'); var moment = require('moment'); +var internals = {}; + var REG_EXP_FILENAME = /^(\d+)x(\d+)_(\d+)/; -function getImages(harvested, output, next, options) { +internals.getImages = function (harvested, output, next, options) { fs.readdir(options.outputDirectory, function (err, folder) { if (err) { return next(); @@ -30,6 +32,7 @@ function getImages(harvested, output, next, options) { 'active': index === last, 'path': options.outputDirectory + '/' + filename, 'filename': filename, + 'link': '/screenshots/'+options.id+'/'+filename, 'index': index +1, 'id': options.id, 'total': list.length, @@ -55,13 +58,13 @@ function getImages(harvested, output, next, options) { }); }); -} +}; module.exports = { dependencies: ['screenshots'], preprocess: function (harvested, output, next, options) { - if (harvested && harvested.screenshots && options.outputDirectory) { - getImages.apply(this, Array.prototype.slice.call(arguments)); + if (harvested && options.outputDirectory) { + internals.getImages.apply(this, Array.prototype.slice.call(arguments)); } else { next(); } diff --git a/lib/rule/validator/codeUsage.js b/lib/rule/validate/codeUsage.js similarity index 100% rename from lib/rule/validator/codeUsage.js rename to lib/rule/validate/codeUsage.js diff --git a/lib/rule/validator/errors.js b/lib/rule/validate/common.js similarity index 100% rename from lib/rule/validator/errors.js rename to lib/rule/validate/common.js diff --git a/lib/rule/validate/css.js b/lib/rule/validate/css.js new file mode 100644 index 0000000..9823890 --- /dev/null +++ b/lib/rule/validate/css.js @@ -0,0 +1,90 @@ +var parseCSS = require('css-parse'); + +var internals = {}; + +internals.formatCssStyle = function (v) { + return ' ' + v.property + ': ' + v.value + '; \n'; +}; + +internals.getCode = function (rule) { + return rule.selectors.join(', ') + + ' {\n' + rule.declarations.map(internals.formatCssStyle).join('') + '}\n'; +}; + +// only margin and padding is allowed +internals.declartionVialoation = function (declarations) { + return declarations.some(function (value) { + if (value.type === 'declaration') { + // usages of other than margin and padding + if (!(value.property.indexOf('margin') === 0 || value.property.indexOf('padding') === 0)) { + return true; + } + } + }); +}; + +internals.toBoolean = function(expression){ + return !!(expression); +} + +var RE_VALID_FIRST_SELECTOR = /^[#|\.]{1}/; + +internals.hasSelectorViolation = function (rule) { + return internals.toBoolean( + rule && + rule.selectors && + rule.selectors.length > 0 && + !rule.selectors[0].match(RE_VALID_FIRST_SELECTOR) && + internals.declartionVialoation(rule.declarations) + ); +}; + +internals.reportErrorOnStyleContent = function (config, report) { + return function (styleContent) { + + var parsed; + + try { + parsed = parseCSS(styleContent); + } catch (e) { + return report.info('CSS/Styling might be malformed: ' + e.message); + } + + if (!parsed || !parsed.stylesheet || !parsed.stylesheet.rules) { + return; + } + parsed.stylesheet.rules.forEach(function (rule) { + if (internals.hasSelectorViolation(rule) === false) { + return; + } + var method = config.strictRules ? 'error' : 'warn'; + report[method]('Styling from style-tag without class or ID prefix found: \"' + + rule.selectors.join(', ') + '\"', { + 'code': internals.getCode(rule) + }); + + }); + }; +}; + +internals.validateRules = function (harvested, report, next) { + if (Array.isArray(harvested.css.styles)) { + harvested.css.styles.forEach( + internals.reportErrorOnStyleContent(this, report) + ); + } else { + report.debug('Found no styling/styles to inspect'); + } + next(); +}; + +module.exports = { + dependencies: ['css'], + validate: function (harvested, report, next) { + if (harvested && harvested.css) { + internals.validateRules.apply(this, Array.prototype.slice.call(arguments)); + } else { + next(); + } + } +}; diff --git a/lib/rule/validate/gardr.js b/lib/rule/validate/gardr.js new file mode 100644 index 0000000..3923972 --- /dev/null +++ b/lib/rule/validate/gardr.js @@ -0,0 +1,164 @@ +var internals = {}; + +internals.validateWrapper = function (wrapper, report) { + if (wrapper.css.position !== 'static' || wrapper.css.visability !== '') { + report.error('Do not style outside Banner, wrapper element has received som styling'); + } +}; + +internals.validateBannerCSS = function (banner, data, report, globalOptions) { + var gardrSize = data ? ('Gardr reported size width ' + data.frameOutput.width + ' x height ' + data.frameOutput.height) : ''; + + if (banner.found === false) { + report.error('Banner not identified. ' + gardrSize); + return; + } + + if (banner.name) { + var css = banner.css; + + report.info('Banner identified, size width ' + + css.width + ' x height ' + css.height + '. ' + gardrSize); + + internals.validateWidthAndHeight(css, globalOptions, report); + + if (this.enforceStyling === true) { + if (css.display !== 'block') { + report.error('Banner should use display:block. Currently it is ' + css.display); + } + + if (css.position !== 'static') { + var method = css.position === 'relative' ? 'warn' : 'error'; + report[method]('Banner should have position: "static", but instead it has position: \"' + + css.position + '\". Please use a inner container if position "relative" or "absolute" is needed.'); + } + } + } +}; + +internals.validateWidthAndHeight = function (css, opt, report) { + var numHeight = parseInt(css.height, 10); + var numWidth = parseInt(css.width, 10); + + if (opt.height.max && opt.height.min === opt.height.max){ + if (numHeight !== opt.height.min){ + report.error('Banner height needs to be '+opt.height.min+'px. Currently it is ' + css.height); + } + } else if (!(numHeight >= opt.height.min && numHeight <= opt.height.max)) { + report.error('Banner height needs to be between '+opt.height.min + 'px and ' + opt.height.max + + 'px. Currently it is ' + css.height); + } + + + if (opt.width.max && opt.width.min === opt.width.max) { + if (opt.width.min === '100%') { + if (numWidth !== opt.viewport.width){ + report.error( + 'Banner width should use 100%(' + opt.viewport.width + + ') width. Currently it is ' + css.width + ); + } + + } else if (numWidth !== opt.width.max) { + report.error( + 'Banner should use ' + opt.width.max + + ' px width. Currently it is ' + css.width + ); + } else { + // ? + } + } else if (!(numWidth >= opt.width.min && numWidth >= opt.width.min)) { + report.error( + 'Banner width needs to be between ' + + opt.width.min + 'px and ' + opt.width.max + + 'px high. Currently it is ' + css.width + ); + } +}; + +internals.findWindowOpenError = function (list) { + return list.filter(function (entry) { + return entry.target !== 'new_window'; + }); +}; + +internals.windowOpenErrors = function (list, report) { + var errors = internals.findWindowOpenError(list); + if (errors && errors.length > 0) { + var message = 'Window open called with wrong target, check url' + errors[0].url + ' and target ' + errors[0].target; + report.error(message, { + trace: errors.map(function (entry) { + return entry.trace; + }) + }); + } + return errors.length === 0; +}; + +var RE_WINDOW_OPEN = /.*(window\.open\()+(.*)(new_window)+/gmi; + +internals.validateBannerDom = function (banner, data, windowOpened, report) { + if (banner.found === false) { + return; + } + if (this.iframeNotAllowed === true && + banner.html && banner.html.indexOf(' -1) { + report.warn('Please do not use iframes inside iframe, gardr iframe is your sandbox.'); + } + + if (this.enforceSpec === true) { + if (windowOpened && windowOpened.length > 0) { + var noErrorsFound = internals.windowOpenErrors(windowOpened, report); + if (noErrorsFound) { + // if window open was registered and no errors found, we do not need to check for clickhandler. + return; + } + } + + if (!banner.clickHandler || banner.clickHandler === '') { + report.error('Missing clickhandler on banner html element/tag ' + banner.name + '.'); + } else if (banner.clickHandler) { + var matches = banner.clickHandler.match(RE_WINDOW_OPEN); + if (!matches) { + report.error('Missing onclick handler on banner wrapper element, and no click registered in simulation.'); + } + } + } +}; + +internals.valdiateTags = function (illegal, report) { + if (illegal && illegal.length > 0) { + report.warn('Found illegal tags/usages', { + list: illegal.map(function (v) { + return v.html.join(',\n'); + }) + }); + } +}; + +internals.validateRules = function (harvested, report, next, globalOptions) { + + var gardr = harvested.gardr; + var actions = harvested.actions; + + internals.validateWrapper.call(this, gardr.dom.wrapper, report); + internals.validateBannerCSS.call(this, gardr.dom.banner, gardr.data, report, globalOptions); + internals.validateBannerDom.call(this, gardr.dom.banner, gardr.data, (actions && actions.windowOpened), report); + + if (this.illegalTags) { + internals.valdiateTags.call(this, gardr.dom.illegal, report); + } + + next(); +}; + +module.exports = { + dependencies: ['actions', 'gardr'], + validate: function (harvested, report, next) { + if (harvested && harvested.gardr && harvested.gardr.dom) { + internals.validateRules.apply(this, Array.prototype.slice.call(arguments)); + } else { + next(); + } + } +}; diff --git a/lib/rule/validate/jquery.js b/lib/rule/validate/jquery.js new file mode 100644 index 0000000..182e21c --- /dev/null +++ b/lib/rule/validate/jquery.js @@ -0,0 +1,49 @@ +var internals = {}; + +internals.validateLatest = function (harvested, report, next /*, globalOptions*/ ) { + var jq = harvested.jquery; + var key = jq.versionObj && jq.versionObj.sortKey; + var isOk = jq.versions && jq.versions.some(function (o) { + return key === o.sortKey; + }); + + if (!isOk) { + var suggestion = jq.versions.map(function (v) { + return 'v' + [v.major, v.minor, v.patch].join('.'); + }).join(' or '); + report.error('Wrong jQuery version: ' + jq.version + '. Please use version ' + suggestion); + } else { + report.info('Correct jQuery version ' + jq.version); + } + next(); +}; + +internals.validate = function (harvested, report, next) { + // validate jquery animate + var data = harvested.jquery; + if (data && data.animate && data.animate.length > 0) { + data.animate.forEach(function (collection) { + if (collection.length > 0) { + var trace = collection.map(function (v) { + return v.trace; + }); + report.error('Usage of jquery animate detected, please use CSS animations instead', { + 'trace': trace + }); + } + }); + } + + if (data && data.versions) { + internals.validateLatest.apply(this, Array.prototype.slice.call(arguments)); + } else { + next(); + } +}; + +module.exports = { + 'dependencies': ['jquery'], + 'preprocessors': ['jquery'], + 'validateLatest': internals.validateLatest, + 'validate': internals.validate +}; diff --git a/lib/rule/validate/log.js b/lib/rule/validate/log.js new file mode 100644 index 0000000..99a6f17 --- /dev/null +++ b/lib/rule/validate/log.js @@ -0,0 +1,7 @@ +module.exports = { + dependencies: ['log'], + preprocessors: ['log'], + validate: function validate(harvested, report, next/*, globalOptions*/) { + next(); + } +}; diff --git a/lib/rule/validate/sizes.js b/lib/rule/validate/sizes.js new file mode 100644 index 0000000..e2217b1 --- /dev/null +++ b/lib/rule/validate/sizes.js @@ -0,0 +1,233 @@ +/* */ +var pathLib = require('path'); + +var internals = {}; + +internals.getTraceOfTypes = function (obj) { + return { + trace: Object.keys(obj).map(function (str) { + return { + 'sourceURL': str + }; + }) + }; +}; + +internals.validEntries = function (data, entry) { + var _data = data[entry.select]; + return _data && _data.total && _data.total.requests > 0; +}; + +var RE_LAST_URL_SECTION = /([^\/]+)\/?$/i; +var RE_QUERY = /\?.*$/; +internals.getName = function (str) { + var output; + if (typeof str !== 'string') { + return str; + } + str = str.trim().replace(RE_QUERY, ''); + + output = pathLib.basename(str); + + if (!output) { + var m = str.match(RE_LAST_URL_SECTION); + if (m && m[1]) { + output = m[1]; + } + } + + if (!output) { + output = pathLib.dirname(str); + } + + return !output ? str : output; +}; + +internals.formatRaw = function (o) { + if (!o) { + return; + } + var ignoreKeys = ['base64Content']; + return Object.keys(o).map(function (key) { + var raw = o[key]; + var output = {}; + Object.keys(raw).filter(function (key) { + if (ignoreKeys.indexOf(key) === -1) { + output[key] = raw[key]; + } + }); + var title = internals.getName(output.url); + output.title = title && title.substring(0, 30); + return output; + }); +}; + +internals.outputEntries = function (data, entry) { + var _data = data.summary[entry.select]; + var raw = internals.formatRaw(data.types[entry.select]); + return { + 'title': _data.total.requests + ' ' + entry.name[0] + (_data.total.requests > 1 ? entry.name[1] : ''), + 'data': _data.total, + 'raw': raw, + 'hasRaw': raw && raw.length > 0 + }; +}; + +//increaseThreshold + +internals.getSummaryView = function () { + return [{ + 'name': ['Script', 's'], + 'select': 'script' + }, { + 'name': ['CSS', ' files'], + 'select': 'style' + }, { + 'name': ['Image', 's'], + 'select': 'image' + }, { + 'name': ['Other / Undefined', ' files'], + 'select': 'other' + }, { + 'name': ['Requesterror', 's'], + 'select': 'errors' + }]; +}; + +internals.filterUserEntry = function (entry) { + return entry.sourceURL.indexOf('user-entry.js') === -1; +}; + +internals.outputSummary = function (summary, report) { + + var totalSummaryView; + + if (summary.typed && summary.typed.summary) { + totalSummaryView = internals.getSummaryView() + .filter(internals.validEntries.bind(null, summary.typed.summary)) + .map(internals.outputEntries.bind(null, summary.typed)); + } + + report.meta(summary.total.rawRequests + ' Requests', { + 'decrease': summary.total.size, + 'hasSummary': totalSummaryView && totalSummaryView.length > 0, + 'summary': totalSummaryView + }); + + if (summary.total.size === 0 || summary.total.size < this.minimumPayloadSize) { + report.error('Total payload size (' + summary.total.size + ') is zero or close to zero to be able to serve a valid banner/displayad.'); + } + + if (summary.typed && summary.typed.types && summary.typed.types.script) { + var scripts = internals.getTraceOfTypes(summary.typed.types.script); + var scriptsLength = scripts.trace.filter(internals.filterUserEntry).length; + if (scriptsLength > this.maxRequests.script) { + report.warn('Please do not use (' + scriptsLength + + ') more than maximum ' + this.maxRequests.script + ' external javascript files', scripts); + } + } + + if (summary.typed && summary.typed.types && summary.typed.types.style) { + var styles = internals.getTraceOfTypes(summary.typed.types.style); + if (styles.trace.length > this.maxRequests.style) { + report.error('Please do not use (' + styles.trace.length + + ') more than maximum 0 external CSS files', styles); + } + } + + if (summary.total.requestErrors > this.maxRequests.errors) { + report.error('There are ' + summary.total.requestErrors + ' request error' + + (summary.total.requestErrors > 1 ? 's' : ''), + internals.getTraceOfTypes(summary.typed.types.errors)); + } +}; + +internals.outputOnSize = function (summary, report /*, options*/ ) { + var metas = report.getResult().meta; + var collected = metas.reduce(internals.collectTotalMeta, { + 'total': 0, + 'threshold': 0 + }); + + report.meta('Total' + (collected.total > 0 ? ' rest value' : ' over threshold'), { + 'restValue': collected.total, + 'threshold': collected.threshold, + 'success': collected.total >= 0, + 'error': collected.total < 0 + }); + + if (collected.total < 0) { + report.error('Total payload size ' + Math.abs(collected.total) + ' bytes over the threshold, ' + collected.threshold + ' bytes is the maximum size'); + } else { + report.info('Total payload size ' + summary.total.size + ' is verified and within the limit of ' + collected.threshold + '.'); + } +}; + +internals.collectTotalMeta = function (totalObj, value) { + if (value.data) { + if (value.data.increaseThreshold) { + totalObj.total += value.data.increaseThreshold; + totalObj.threshold += value.data.increaseThreshold; + } else if (value.data.decrease) { + totalObj.total -= value.data.decrease; + } + } + return totalObj; +}; + +var RE_ADFORM = /(Adform\.Bootstrap|Adform\.RMB|Adform\.DHTML|EngagementTracker)/i; +internals.outputAdformThreshold = function (rawFileDataSummary, report /*, options*/ ) { + if (!rawFileDataSummary || !rawFileDataSummary.typed || !rawFileDataSummary.typed.types || !rawFileDataSummary.typed.types.script) { + return; + } + + var size = 0; + var data = rawFileDataSummary.typed.types.script; + Object.keys(data).forEach(function (scriptUrlKey) { + if (RE_ADFORM.test(scriptUrlKey)) { + size += data[scriptUrlKey].bodyLength; + } + }); + + if (size) { + report.meta('Adform', { 'increaseThreshold': size }); + } +}; + +internals.validate = function (harvested, report, next, globalOptions) { + + var baseDesc = 'Base'; + if (globalOptions && globalOptions.format){ + baseDesc += ' for \"' + globalOptions.format.id + '\"'; + } + report.meta(baseDesc, { 'increaseThreshold': this.thresholdBytes}); + + if (this.giveExtraThreshold.jQuery === true && !! (harvested.jquery && harvested.jquery.version)) { + report.meta('jQuery', { + 'increaseThreshold': this.giveExtraThreshold.jQueryThreshold + }); + } + + var summary = harvested.har.rawFileDataSummary; + if (summary) { + if (this.giveExtraThreshold.AdForm) { + internals.outputAdformThreshold.call(this, summary, report, globalOptions); + } + internals.outputSummary.call(this, summary, report, globalOptions); + internals.outputOnSize.call(this, summary, report, globalOptions); + } else { + report.error('Something went wrong validating sizes. Missing har file data summary.'); + } + next(); +}; + +module.exports = { + dependencies: ['har', 'jquery'], + validate: function (harvested, report, next /*, globalOptions*/ ) { + if (harvested) { + internals.validate.apply(this, Array.prototype.slice.call(arguments)); + } else { + next(); + } + } +}; diff --git a/lib/rule/validator/timers.js b/lib/rule/validate/timers.js similarity index 75% rename from lib/rule/validator/timers.js rename to lib/rule/validate/timers.js index 96eb2b6..1228643 100644 --- a/lib/rule/validator/timers.js +++ b/lib/rule/validate/timers.js @@ -1,9 +1,20 @@ -var METHODS = ['setTimeout', 'setInterval', 'requestAnimationFrame']; -var MAX_CALLS = [20, 1, 0]; +var internals = {}; -function validate(harvested, report, next) { +internals.isHttp = function (entry) { + return (entry && entry.trace && entry.trace.sourceURL && entry.trace.sourceURL.indexOf('http') === 0); +}; + +internals.validate = function (harvested, report, next) { + + var METHODS = ['setTimeout', 'setInterval', 'requestAnimationFrame']; + var MAX_CALLS = [ + this.setTimeout||20, + this.setInterval||1, + this.requestAnimationFrame||0 + ]; if (!(harvested.timers)) { + report.debug('No timers present.'); return next(); } @@ -25,7 +36,7 @@ function validate(harvested, report, next) { } function collectionInCollection(collection /*, index, list*/) { - collection = collection.filter(isHttp); + collection = collection.filter(internals.isHttp); var msg; var trace = {trace: collection.map(function(v){ return v.trace; @@ -41,13 +52,9 @@ function validate(harvested, report, next) { } }); next(); -} - -function isHttp(entry) { - return (entry && entry.trace && entry.trace.sourceURL && entry.trace.sourceURL.indexOf('http') === 0); -} +}; module.exports = { dependencies: ['timers'], - validate: validate + validate: internals.validate }; diff --git a/lib/rule/validate/touch.js b/lib/rule/validate/touch.js new file mode 100644 index 0000000..bc4c767 --- /dev/null +++ b/lib/rule/validate/touch.js @@ -0,0 +1,58 @@ + +var internals = {}; + +var allowedToPreventKeys = ['swipeLeft', 'swipeRight']; +var notAllowedToSwipe = ['swipeTop', 'swipeBottom']; + +internals.verify = function (touchEvents, mapper){ + return Object.keys(touchEvents).some(function(touchEventType){ + var events = touchEvents[touchEventType]; + return events && events.some(mapper); + }); +}; + +internals.validate = function (harvested, report, next/*, globalOptions*/) { + + if (!(harvested.touch)) { + return next(); + } + + var data = harvested.touch && harvested.touch.touchEventData; + var detectedLegalUsage = false; + var detectedWrongUsage = false; + if (data){ + Object.keys(data).forEach(function(swipeKey){ + var result; + if (allowedToPreventKeys.indexOf(swipeKey) > -1){ + result = internals.verify(data[swipeKey], function(entry){ + return (entry.defaultPrevented === true); + }); + if (result === true){ + detectedLegalUsage = true; + } + } else if (notAllowedToSwipe.indexOf(swipeKey) > -1){ + result = internals.verify(data[swipeKey], function(entry){ + return (entry.defaultPrevented === true || entry.returnValue === false); + }); + if (result === true){ + detectedWrongUsage = true; + report.error('Detected illegal swipe usage. Please only use horizontal touch events +/- 30%'); + } + } + }); + } else { + report.debug('Validator error. Missing harvested touchevent data'); + } + + if (detectedLegalUsage && detectedWrongUsage !== true){ + report.info('Detected swipe events inside code'); + } + + + next(); +}; + +module.exports = { + dependencies: ['touch'], + validate: internals.validate +}; diff --git a/lib/rule/validator/css.js b/lib/rule/validator/css.js deleted file mode 100644 index b2520a3..0000000 --- a/lib/rule/validator/css.js +++ /dev/null @@ -1,72 +0,0 @@ -var parseCSS = require('css-parse'); - -function formatCssStyle(v) { - return ' ' + v.property + ': ' + v.value + '; \n'; -} - -function getCode(rule) { - return rule.selectors.join(', ') + ' {\n' + rule.declarations.map(formatCssStyle).join('') + '}\n'; -} - -// only margin and padding is allowed -function declartionVialoation(declarations){ - return declarations.some(function(value){ - if (value.type === 'declaration'){ - // usages of other than margin and padding - if (!(value.property.indexOf('margin') === 0 || value.property.indexOf('padding') === 0)){ - return true; - } - } - }); -} - -var RE_VALID_FIRST_SELECTOR = /^[#|\.]{1}/; -function hasSelectorViolation(rule) { - return rule && rule.selectors && !rule.selectors[0].match(RE_VALID_FIRST_SELECTOR) && declartionVialoation(rule.declarations); -} - - -function reportErrorOnStyleContent(report) { - return function (styleContent) { - - var parsed; - - try{ - parsed = parseCSS(styleContent); - } catch(e){ - return report.info('CSS/Styling might be malformed: '+e.message); - } - - if (!parsed || !parsed.stylesheet || !parsed.stylesheet.rules) { - return; - } - parsed.stylesheet.rules.filter(function (rule) { - if (hasSelectorViolation(rule)) { - report.error('Styling from style-tag without class or ID prefix found: \"' + rule.selectors.join(', ') + '\"', { - 'code': getCode(rule) - }); - } - }); - }; -} - -function validateRules(harvested, report, next) { - var handler = reportErrorOnStyleContent(report); - - if (harvested.css && Array.isArray(harvested.css.styles)) { - harvested.css.styles.forEach(handler); - } - next(); -} - -module.exports = { - dependencies: ['css'], - validate: function (harvested, report, next) { - - if (harvested) { - validateRules(harvested, report, next); - } else { - next(); - } - } -}; diff --git a/lib/rule/validator/gardr.js b/lib/rule/validator/gardr.js deleted file mode 100644 index 0dce6a5..0000000 --- a/lib/rule/validator/gardr.js +++ /dev/null @@ -1,122 +0,0 @@ -function validateWrapper(wrapper, report) { - if (wrapper.css.position !== 'static' || wrapper.css.visability !== '') { - report.error('Do not style outside bannercontainer, wrapper element has received som styling'); - } -} - -function validateBannerCSS(banner, data, report, globalOptions) { - var gardrSize = data ? ('Gardr reported size width ' + data.frameOutput.width + ' x height ' + data.frameOutput.height) : ''; - - if (banner.found === false) { - report.warn('Bannercontainer not found / identified. ' + gardrSize); - return; - } - - if (banner.name) { - var css = banner.css; - - report.info('Bannercontainer found / identified, size width ' + css.width + ' x height ' + css.height + '. ' + gardrSize); - - var numHeight = parseInt(css.height, 10); - var heightOpt = parseInt(globalOptions.options.format.height, 10); - - if (numHeight !== heightOpt) { - report.error('Bannercontainer needs to be '+heightOpt+'px high. Currently it is ' + css.height); - } - - // should be viewport width - // TODO check against options - var numWidth = parseInt(css.width, 10); - if (numWidth !== parseInt(globalOptions.width, 10)){ - report.error('Bannercontainer should use 100%('+globalOptions.width+') width. Currently it is ' + css.width); - } - - if (css.display !== 'block'){ - report.error('Bannercontainer should use display:block. Currently it is ' + css.display); - } - - if (css.position !== 'static') { - report.error('Bannercontainer should have position: "static", but instead it has position: "' + css.position + '". Please use a inner container if position "relative" or "absolute" is needed.'); - } - } -} - -function findWindowOpenError(list){ - return list.filter(function(entry){return entry.target !== 'new_window';}); -} - -function windowOpenErrors(list, report){ - var errors = findWindowOpenError(list); - if (errors && errors.length > 0) { - var message = 'Window open called with wrong target, check url' + errors[0].url + ' and target ' + errors[0].target; - report.error(message, { - trace: errors.map(function (entry) { - return entry.trace; - }) - }); - } - return errors.length === 0; -} - - -var RE_WINDOW_OPEN = /.*(window\.open\()+(.*)(new_window)+/gmi; -function validateBannerDom(banner, data, windowOpened, report){ - if (banner.found === false) { - return; - } - - if (banner.html && banner.html.indexOf('-1){ - report.warn('Please do not use iframes inside iframe, gardr iframe is your sandbox.'); - } - - - - if (windowOpened && windowOpened.length > 0){ - var noErrorsFound = windowOpenErrors(windowOpened, report); - if (noErrorsFound){ - // if window open was registered and no errors found, we do not need to check for clickhandler. - return; - } - } - - if (!banner.clickHandler || banner.clickHandler === ''){ - report.error('Missing clickhandler on banner html element/tag ' + banner.name + '.'); - } else if (banner.clickHandler){ - var matches = banner.clickHandler.match(RE_WINDOW_OPEN); - if (!matches){ - report.error('Missing onclick handler on banner wrapper element, and no click registered in simulation.'); - } - } -} - -function valdiateTags(illegal, report){ - if (illegal && illegal.length > 0){ - report.warn('Found illegal tags/usages', {list: illegal.map(function(v){return v.html.join(',\n');})}); - } -} - -function validateRules(harvested, report, next, globalOptions) { - - var gardr = harvested.gardr; - var actions = harvested.actions; - if (gardr && gardr.dom) { - var dom = gardr.dom; - validateWrapper(dom.wrapper, report); - validateBannerCSS(dom.banner, gardr.data, report, globalOptions); - validateBannerDom(dom.banner, gardr.data, actions && actions.windowOpened, report); - valdiateTags(dom.illegal, report); - } - - next(); -} - -module.exports = { - dependencies: ['actions', 'gardr'], - validate: function (harvested, report, next, options) { - if (harvested) { - validateRules(harvested, report, next, options); - } else { - next(); - } - } -}; diff --git a/lib/rule/validator/jquery.js b/lib/rule/validator/jquery.js deleted file mode 100644 index f2fe0dc..0000000 --- a/lib/rule/validator/jquery.js +++ /dev/null @@ -1,48 +0,0 @@ - -function validateLatest(harvested, report, next) { - var jq = harvested.jquery; - var key = jq.versionObj.sortKey; - var isOk = jq.versions.some(function (o) { - return key === o.sortKey; - }); - - if (!isOk) { - var suggestion = jq.versions.map(function (v) { - return 'v' + [v.major, v.minor, v.patch].join('.'); - }).join(' or '); - report.error('Wrong jQuery version: ' + jq.version + '. Please use version ' + suggestion); - } else { - report.info('Correct jQuery version ' + jq.version); - } - next(); -} - - -module.exports = { - dependencies: ['jquery'], - validateLatest: validateLatest, - validate: function (harvested, report, next/*, globalOptions*/) { - // validate jquery animate - var data = harvested.jquery; - if (data && data.animate && data.animate.length > 0) { - data.animate.forEach(function (collection) { - if (collection.length > 0) { - var trace = collection.map(function (v) { - return v.trace; - }); - report.error('Usage of jquery animate detected, please use CSS animations instead', { - 'trace': trace - }); - } - }); - } - - if (data && data.versions){ - validateLatest(harvested, report, next); - } else { - next(); - } - - - } -}; diff --git a/lib/rule/validator/log.js b/lib/rule/validator/log.js deleted file mode 100644 index 0bf4bb5..0000000 --- a/lib/rule/validator/log.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - dependencies: ['log'], - validate: function validate(harvested, report, next) { - next(); - } -}; diff --git a/lib/rule/validator/sizes.js b/lib/rule/validator/sizes.js deleted file mode 100644 index 7a75563..0000000 --- a/lib/rule/validator/sizes.js +++ /dev/null @@ -1,222 +0,0 @@ -/* */ -var pathLib = require('path'); - -function getTraceOfTypes(obj) { - return { - trace: Object.keys(obj).map(function (str) { - return { - 'sourceURL': str - }; - }) - }; -} - -function validEntries(data, entry){ - var _data = data[entry.select]; - return _data && _data.total && _data.total.requests > 0; -} - -var RE_LAST_URL_SECTION = /([^\/]+)\/?$/i; -var RE_QUERY = /\?.*$/; -function getName(str){ - var output; - if (typeof str !== 'string'){ - return str; - } - str = str.trim().replace(RE_QUERY, ''); - - output = pathLib.basename(str); - - if (!output){ - var m = str.match(RE_LAST_URL_SECTION); - if (m && m[1]){ - output = m[1]; - } - } - - if (!output){ - output = pathLib.dirname(str); - } - - return !output ? str : output; -} - -function formatRaw(o){ - if (!o){return;} - var ignoreKeys = ['base64Content']; - return Object.keys(o).map(function(key){ - var raw = o[key]; - var output = {}; - Object.keys(raw).filter(function(key){ - if (ignoreKeys.indexOf(key) === -1){ - output[key] = raw[key]; - } - }); - var title = getName(output.url); - output.title = title && title.substring(0, 30); - return output; - }); -} - -function outputEntries(data, entry){ - var _data = data.summary[entry.select]; - var raw = formatRaw(data.types[entry.select]); - return { - title: _data.total.requests + ' ' + entry.name[0] + (_data.total.requests > 1 ? entry.name[1] : ''), - data: _data.total, - raw: raw, - hasRaw: raw && raw.length > 0 - }; -} - -//increaseThreshold - -function getSummaryView(){ - return [ - { - 'name': ['Script','s'], - 'select': 'script' - }, - { - 'name': ['CSS',' files'], - 'select': 'style' - }, - { - 'name': ['Image','s'], - 'select': 'image' - }, - { - 'name': ['Other / Undefined',' files'], - 'select': 'other' - }, - { - 'name': ['Requesterror', 's'], - 'select': 'errors' - } - ]; -} - -function outputSummary(summary, report) { - - var totalSummaryView; - - if (summary.typed && summary.typed.summary) { - totalSummaryView = getSummaryView().filter(validEntries.bind(null, summary.typed.summary)) - .map(outputEntries.bind(null, summary.typed)); - } - - report.meta(summary.total.rawRequests + ' Requests', { - 'decrease': summary.total.size, - 'hasSummary': totalSummaryView && totalSummaryView.length > 0, - 'summary': totalSummaryView - }); - - if (summary.total.size === 0 || summary.total.size < 100) { - report.error('Total payload size (' + summary.total.size + ') is zero or close to zero to be able to serve a valid banner/displayad.'); - } - - if (summary.typed && summary.typed.types && summary.typed.types.script) { - var scripts = getTraceOfTypes(summary.typed.types.script); - var scriptsLength = scripts.trace.filter(function(entry){return entry.sourceURL.indexOf('user-entry.js') === -1;}).length; - if (scriptsLength > 2) { - report.warn('Please do not use (' + scriptsLength + ') more than maximum 2 external javascript files', scripts); - } - } - - if (summary.typed && summary.typed.types && summary.typed.types.style) { - var styles = getTraceOfTypes(summary.typed.types.style); - if (styles.trace.length > 0) { - report.error('Please do not use (' + styles.trace.length + ') more than maximum 0 external CSS files', styles); - } - } - - if (summary.total.requestErrors > 0) { - report.error('There are ' + summary.total.requestErrors + ' request error' + (summary.total.requestErrors > 1 ? 's' : ''), getTraceOfTypes(summary.typed.types.errors)); - } -} - -function outputOnSize(summary, report, options){ - var metas = report.getResult().meta; - var collected = metas.reduce(collectTotalMeta, {'total': 0, 'threshold': 0}); - - report.meta('Total'+(collected.total > 0 ? ' rest value' : ' over threshold'), { - restValue: collected.total, - threshold: collected.threshold, - success: collected.total >= 0, - error: collected.total < 0 - }); - - if (collected.total < 0) { - report.error('Total payload size ' + collected.total + ' bytes is to high, ' + collected.threshold + ' bytes is the maximum for ' + options.target); - } else { - report.info('Total payload size ' + summary.total.size + ' is verified and within the limit of ' + collected.threshold + '.'); - } -} - -function collectTotalMeta(totalObj, value){ - if (value.data){ - if (value.data.increaseThreshold){ - totalObj.total += value.data.increaseThreshold; - totalObj.threshold += value.data.increaseThreshold; - } else if (value.data.decrease){ - totalObj.total -= value.data.decrease; - } - } - return totalObj; -} - -var RE_ADFORM = /(Adform\.Bootstrap|Adform\.RMB|Adform\.DHTML|EngagementTracker)/i; -function outputAdformThreshold(rawFileDataSummary, report/*, options*/){ - if (!rawFileDataSummary || !rawFileDataSummary.typed || !rawFileDataSummary.typed.types || !rawFileDataSummary.typed.types.script){ - return; - } - - var size = 0; - var data = rawFileDataSummary.typed.types.script; - Object.keys(data).forEach(function(scriptUrlKey){ - if (RE_ADFORM.test(scriptUrlKey)){ - size += data[scriptUrlKey].bodyLength; - } - }); - - if (size){ - report.meta('Adform', { - 'increaseThreshold': size - }); - } -} - - -function validate(harvested, report, next, globalOptions) { - - report.meta('Base for ' + globalOptions.target, { - 'increaseThreshold': (globalOptions.target === 'tablet' ? 100000 : 50000) - }); - - if (!!(harvested.jquery && harvested.jquery.version)) { - report.meta('jQuery', { - 'increaseThreshold': 33369 - }); - } - - var summary = harvested.har.rawFileDataSummary; - if (summary) { - outputAdformThreshold(summary, report, globalOptions); - outputSummary(summary, report, globalOptions); - outputOnSize(summary, report, globalOptions); - } else { - report.error('Something went wrong. Missing file data summary.'); - } - next(); -} - -module.exports = { - dependencies: ['har', 'jquery'], - validate: function (harvested, report, next/*, globalOptions*/) { - if (harvested) { - validate.apply(this, Array.prototype.slice.call(arguments)); - } else { - next(); - } - } -}; diff --git a/lib/rule/validator/touch.js b/lib/rule/validator/touch.js deleted file mode 100644 index 88a1b99..0000000 --- a/lib/rule/validator/touch.js +++ /dev/null @@ -1,52 +0,0 @@ -var allowedToPreventKeys = ['swipeLeft', 'swipeRight']; -var notAllowedToSwipe = ['swipeTop', 'swipeBottom']; - -function verify(touchEvents, mapper){ - return Object.keys(touchEvents).some(function(touchEventType){ - var events = touchEvents[touchEventType]; - return events && events.some(mapper); - }); -} - -function validate(harvested, report, next/*, globalOptions*/) { - - if (!(harvested.touch)) { - return next(); - } - - var data = harvested.touch.touchEventData; - var detectedLegalUsage = false; - var detectedWrongUsage = false; - Object.keys(data).forEach(function(swipeKey){ - var result; - if (allowedToPreventKeys.indexOf(swipeKey) > -1){ - result = verify(data[swipeKey], function(entry){ - return (entry.defaultPrevented === true); - }); - if (result === true){ - detectedLegalUsage = true; - } - } else if (notAllowedToSwipe.indexOf(swipeKey) > -1){ - result = verify(data[swipeKey], function(entry){ - return (entry.defaultPrevented === true || entry.returnValue === false); - }); - if (result === true){ - detectedWrongUsage = true; - report.error('Detected illegal swipe usage. Please only use horizontal touch events +/- 30%'); - } - } - - }); - - if (detectedLegalUsage && detectedWrongUsage !== true){ - report.info('Detected swipe events inside code'); - } - - - next(); -} - -module.exports = { - dependencies: ['touch'], - validate: validate -}; diff --git a/lib/spawn.js b/lib/spawn.js index deb183f..19444b5 100644 --- a/lib/spawn.js +++ b/lib/spawn.js @@ -1,20 +1,14 @@ var childProcess = require('child_process'); -var hoek = require('hoek'); var path = require('path'); var phantomjs = require('phantomjs'); -//var options = ''; var main = path.join(__dirname, '.', 'phantom', 'main.js'); -var defaults = require('./phantom/defaults.js'); -module.exports = function (options, handle, done) { - var merged = hoek.applyToDefaults(defaults, options); - - merged = JSON.stringify(merged); - - childProcess.execFile(phantomjs.path, [main, merged, options], function (err, stdout/*, stderr*/) { +module.exports = function (data, handle, done) { + var merged = JSON.stringify(data); + childProcess.execFile(phantomjs.path, [main, merged], function (err, stdout/*, stderr*/) { if (err) { handle(stdout, done, err); } else if (typeof stdout !== 'undefined'){ diff --git a/lib/validate.js b/lib/validate.js index f0ef575..1b8d642 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -3,11 +3,12 @@ var async = require('async'); var path = require('path'); var deepFreeze = require('deep-freeze'); +var internals = {}; var RE_LAST_URL_SECTION = /([^\/]+)\/?$/i; var RE_QUERY = /\?.*$/; -function formatTraceData(output, data){ +internals.formatTraceData = function (output, data){ if (data) { // as validators may deal with freezed data, lets copy out input. output.data = JSON.parse(JSON.stringify(data)); @@ -48,17 +49,17 @@ function formatTraceData(output, data){ } } return output; -} +}; var KEYS = ['info', 'debug', 'warn', 'error', 'success', 'meta']; -function createReportHelper(result) { +internals.createReportHelper = function (result) { return function entryReporter(validatorFileName) { var reportHelpers = {}; KEYS.forEach(function (key) { result[key] = result[key] || []; reportHelpers[key] = function (message, data) { - result[key].push(formatTraceData({ + result[key].push(internals.formatTraceData({ 'message': message, 'validatorFileName': validatorFileName, 'validatorName': path.basename(validatorFileName, '.js') @@ -70,9 +71,9 @@ function createReportHelper(result) { }); return reportHelpers; }; -} +}; -function filterDataByDependencies(harvested, dependencies, fileName) { +internals.filterDataByDependencies = function (harvested, dependencies, fileName) { var res = { 'common': harvested.common }; @@ -85,28 +86,42 @@ function filterDataByDependencies(harvested, dependencies, fileName) { }); } return Object.freeze(res); -} +}; + +internals.preprocess = function (harvested, options, callback) { + var files = options.preprocess; + -function preprocess(harvested, options, callback) { - var files = options.preprocessorFiles; if (!files || !Array.isArray(files) || files.length === 0) { return callback(); } - async.mapSeries(options.preprocessorFiles, function (fileName, done) { + async.mapSeries(files, function (entry, done) { var mod; - if (typeof fileName !== 'string') { - mod = fileName; + var fileName; + if (typeof entry === 'object') { + if (entry.path){ + fileName = entry.path; + mod = require(fileName); + } else { + mod = entry; + fileName = mod.name; + } + } else { - mod = require(fileName); + mod = require(entry); + fileName = entry; } + + if (!mod || !mod.preprocess) { + console.log('missing', mod.name); return done(); } mod.dependencies = mod.dependencies || []; - var filtered = filterDataByDependencies(harvested, mod.dependencies, fileName); + var filtered = internals.filterDataByDependencies(harvested, mod.dependencies, fileName); function outputter(context, key, value) { if (typeof value === 'undefined' && context && key) { @@ -120,56 +135,80 @@ function preprocess(harvested, options, callback) { filtered[context][key] = value; } - mod.preprocess(filtered, outputter, done, options); + var current = path.basename(fileName, '.js'); + try{ + mod.preprocess.call(options.config[current], filtered, outputter, done, options, options.config[current]); + } catch(e){ + done(e); + } }, callback); -} +}; -function validate(harvested, options, callback) { +internals.validate = function (harvested, options, callback) { var resultData = {}; - var validators = options.validatorFiles; - var entryReporter = createReportHelper(resultData); + var validators = options.validate; + var entryReporter = internals.createReportHelper(resultData); if (!Array.isArray(validators)) { - throw new Error('Validators should be an list'); + throw new Error('Validate property / validators should be an list'); } - preprocess(harvested, options, function () { + internals.preprocess(harvested, options, function (err) { + if (err){ + return callback(err); + } // make read-only deepFreeze(harvested); - async.mapSeries(validators, function (fileName, done) { - var mod; + async.mapSeries(validators, function (entry, done) { + var mod, fileName; - if (typeof fileName !== 'string') { - mod = fileName; + if (typeof entry === 'object') { + if (entry.path){ + fileName = entry.path; + } else if (entry.code) { + // ? code prop + } else { + mod = entry; + } } else { - mod = require(fileName); + fileName = entry; + } + if (!mod){ + try{ + mod = require(fileName); + }catch(e){ + return done(new Error('Error loading validation module '+fileName)); + } } if (!mod || !mod.validate) { - return done(new Error('Missing validation module')); + return done(new Error('Missing validation method on module '+fileName)); } - var filtered = filterDataByDependencies(harvested, mod.dependencies, fileName); + var current = path.basename(fileName, '.js'); + var filtered = internals.filterDataByDependencies(harvested, mod.dependencies, fileName); try{ - mod.validate(filtered, entryReporter(fileName), done, options); + mod.validate.call( + options.config[current], filtered, entryReporter(fileName), done, options, options.config[current] + ); } catch(e){ + console.log('Calling validation function failed on '+current, e.message/*, e.stack*/); done(e); } }, function validationDone(err) { if (err){ - console.log('validationDone error', err); + //console.log('validationDone error via ', err.message, err.stack+''); } callback(err, harvested, resultData, options); }); }); - -} +}; module.exports = { - validate: validate, - filterDataByDependencies: filterDataByDependencies, - createReportHelper: createReportHelper, - deepFreeze: deepFreeze + 'validate': internals.validate, + 'filterDataByDependencies': internals.filterDataByDependencies, + 'createReportHelper': internals.createReportHelper, + 'deepFreeze': deepFreeze }; diff --git a/package.json b/package.json index 873e391..4149fd7 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "css-parse": "^1.7.0", "gardr": "~0.5.17-SNAPSHOT", "browserify": "^3.33.0", - "mock-phantom-touch-events": "0.0.3" + "mock-phantom-touch-events": "0.0.3", + "joi": "^2.8.0" }, "devDependencies": { "mocha": "~1.17.0", diff --git a/test/lib/validateHelpers.js b/test/lib/validateHelpers.js index a4074f7..d3fc2d1 100644 --- a/test/lib/validateHelpers.js +++ b/test/lib/validateHelpers.js @@ -1,21 +1,51 @@ +var hoek = require('hoek'); +var config = require('../../config/config.js'); var validate = require('../../lib/validate.js'); -function getTraceObject(name) { +var internals = {}; + +internals.getTraceObject = function (name) { return { - name: name + '', - time: Date.now(), - trace: { + 'name': name + '', + 'time': Date.now(), + 'trace': { sourceURL: 'http://dummyfile.js', line: '123' } }; -} +}; -function createReporter() { +internals.createReporter = function () { return validate.createReportHelper({})(this.test.title); -} +}; + +internals.applyType = function (type) { + return function (name, harvest, reporter, cb, mutateFn) { + if (!harvest) { + throw new Error('Testhelper ' + name + ' needs a harvest object'); + } + if (!reporter) { + throw new Error('Testhelper ' + name + ' needs a reporter'); + } + if (typeof cb !== 'function') { + throw new Error('Testhelper ' + name + ' needs a done/callback function. Instead saw:' + (typeof cb)); + } + var cloned = hoek.clone(config); + var ctx = cloned.config[name]; + + if (typeof mutateFn === 'function') { + mutateFn(ctx, cloned); + } + var path = '../../lib/rule/' + type + '/' + name + '.js'; + return require(path)[type] + .call(ctx, harvest, reporter, cb, cloned, ctx); + }; +}; module.exports = { - getTraceObject: getTraceObject, - createReporter: createReporter + 'callValidator': internals.applyType('validate'), + 'callPreprocessor': internals.applyType('preprocess'), + 'getTraceObject': internals.getTraceObject, + 'createReporter': internals.createReporter, + 'config': config }; diff --git a/test/phantom/createHooks.test.js b/test/phantom/createHooks.test.js index 6fc3f30..33d32c5 100644 --- a/test/phantom/createHooks.test.js +++ b/test/phantom/createHooks.test.js @@ -15,7 +15,7 @@ describe('createHooks', function () { var page = {}; var options = { - hooks: [fixture] + 'instrument': [fixture] }; createHooks(page, options, baseApi.createSubContext(this.test.title)); @@ -37,10 +37,10 @@ describe('createHooks', function () { }; } var options = { - hooks: [getFixture('key1'), getFixture('key2'), getFixture('key3'), getFixture('key4')] + 'instrument': [getFixture('key1'), getFixture('key2'), getFixture('key3'), getFixture('key4')] }; - times = options.hooks.length; + times = options.instrument.length; createHooks(page, options, baseApi); page.onBeforeExit.call(page, arg1, arg2); @@ -72,7 +72,7 @@ describe('createHooks', function () { var api = baseApi.createSubContext('common'); var called = 0; var options = { - hooks: [{ + instrument: [{ name: testTitle, onAlert: function(msg, _api){ if (msg && _api){ diff --git a/test/rule/actions.test.js b/test/rule/actions.test.js index d523e80..7eedf9c 100644 --- a/test/rule/actions.test.js +++ b/test/rule/actions.test.js @@ -2,9 +2,11 @@ var referee = require('referee'); var assert = referee.assert; //var refute = referee.refute; -var hook = require('../../lib/rule/hook/actions.js'); +var defaults = require('../../config/config.js').config.actions; +var instrument = require('../../lib/rule/instrument/actions.js'); //var help = require('../lib/validateHelpers.js'); + describe('Actions', function () { it('should trigger a click', function(done){ @@ -37,19 +39,19 @@ describe('Actions', function () { var api = { switchToIframe: function () {}, - evaluate: function (fn) { - return fn(); + evaluate: function (fn, arg1) { + return fn.call(this, arg1); }, set: function(key, value){ result.actions[key] = value; } }; - hook.onHalfTime(api); + instrument.onHalfTime(api, defaults); window.open('some url', 'some_target'); - hook.onBeforeExit(api); + instrument.onBeforeExit(api, defaults); setTimeout(function(){ assert.equals(calledRealImpl, 0); diff --git a/test/rule/codeUsage.test.js b/test/rule/codeUsage.test.js index 47868d4..1b13e49 100644 --- a/test/rule/codeUsage.test.js +++ b/test/rule/codeUsage.test.js @@ -2,8 +2,7 @@ var referee = require('referee'); var assert = referee.assert; //var refute = referee.refute; -var validator = require('../../lib/rule/validator/codeUsage.js'); -var help = require('../lib/validateHelpers.js'); +var help = require('../lib/validateHelpers.js'); function getHarvestFixture() { return { @@ -57,7 +56,9 @@ describe('Static code inspection / code usages', function () { it('should report on geolocation usage', function (done) { var reporter = help.createReporter.call(this); - validator.validate(getHarvestFixture(), reporter, handler, {}); + + help.callValidator('codeUsage', getHarvestFixture(), reporter, handler); + function handler() { var result = reporter.getResult(); assert.equals(result.error.length, 4, 'should report on geolocation usage'); diff --git a/test/rule/errors.test.js b/test/rule/common.test.js similarity index 66% rename from test/rule/errors.test.js rename to test/rule/common.test.js index f63b1be..e14948d 100644 --- a/test/rule/errors.test.js +++ b/test/rule/common.test.js @@ -1,11 +1,10 @@ var buster = require('referee'); var assert = buster.assert; -var refute = buster.refute; -var hook = require('../../lib/rule/hook/errors.js'); +var instrumentation = require('../../lib/rule/instrument/common.js'); -describe('Errors hook', function () { +describe('Common instrumentation', function () { it('should report error', function (done) { @@ -20,14 +19,15 @@ describe('Errors hook', function () { } }; - var result = hook.onError(message, trace, api); - assert(result, 'hook should return true'); + var result = instrumentation.onError(message, trace, api); + assert(result, 'instrumentation should return true'); }); }); -var validator = require('../../lib/rule/validator/errors.js'); -describe('Errors validator', function () { +var help = require('../lib/validateHelpers.js'); + +describe('Common validator', function () { it('should report usererrors as validations errors', function () { @@ -45,17 +45,17 @@ describe('Errors validator', function () { } }; - validator.validate(harvested, report, function(){}); + help.callValidator('common', harvested, report, function(){}); assert.equals(called, 0); harvested.common.errors.push({message: 'some error'}); - validator.validate(harvested, report, function(){}); + help.callValidator('common', harvested, report, function(){}); assert.equals(called, 1); harvested.common.systemErrors.push({message: 'another error'}); - validator.validate(harvested, report, function(){}); + help.callValidator('common', harvested, report, function(){}); assert.equals(called, 3); }); diff --git a/test/rule/css.test.js b/test/rule/css.test.js index b3825f0..bc0a5ff 100644 --- a/test/rule/css.test.js +++ b/test/rule/css.test.js @@ -2,9 +2,11 @@ var referee = require('referee'); var assert = referee.assert; //var refute = referee.refute; -var cssHOOK = require('../../lib/rule/hook/css.js'); +var config = require('../../config/config.js'); +var defaults = config.config.css; +var instrumentation = require('../../lib/rule/instrument/css.js'); -describe('CSS hook', function () { +describe('CSS instrumentation', function () { it('it should collect styles and filter', function(){ @@ -13,15 +15,14 @@ describe('CSS hook', function () { 'set' : function(key, value){ result[key] = value; }, - 'evaluate' : function(fn){ - return fn(); + 'evaluate' : function(fn, arg1){ + return fn(arg1); }, 'switchToIframe' : function(){ } }; - function dom(content){ return { innerHTML: content @@ -34,19 +35,15 @@ describe('CSS hook', function () { } }; - cssHOOK.onBeforeExit(api); + instrumentation.onBeforeExit(api, defaults); global.document = null; assert.isArray(result.styles); assert.equals(result.styles.length, 1); - - }); - }); -var cssValidator = require('../../lib/rule/validator/css.js'); var help = require('../lib/validateHelpers.js'); describe('CSS validator', function(){ @@ -68,14 +65,13 @@ describe('CSS validator', function(){ var reporter = help.createReporter.call(this); - cssValidator.validate(harvest, reporter, function(){ - + help.callValidator('css', harvest, reporter, function(){ var result = reporter.getResult(); - - assert.equals(result.error.length, 3, 'should filter tags with usages except margin/padding'); - + assert.equals( + result.error.length, 3, 'should filter tags with usages except margin/padding'); done(); }); + }); }); diff --git a/test/rule/fixtures/screenshots/980x225_1396016764954.png b/test/rule/fixtures/screenshots/980x225_1396016764954.png new file mode 100644 index 0000000..d68e101 Binary files /dev/null and b/test/rule/fixtures/screenshots/980x225_1396016764954.png differ diff --git a/test/rule/fixtures/screenshots/980x225_1396016836143.png b/test/rule/fixtures/screenshots/980x225_1396016836143.png new file mode 100644 index 0000000..d68e101 Binary files /dev/null and b/test/rule/fixtures/screenshots/980x225_1396016836143.png differ diff --git a/test/rule/fixtures/screenshots/980x225_1396016837366.png b/test/rule/fixtures/screenshots/980x225_1396016837366.png new file mode 100644 index 0000000..d68e101 Binary files /dev/null and b/test/rule/fixtures/screenshots/980x225_1396016837366.png differ diff --git a/test/rule/gardr.test.js b/test/rule/gardr.test.js index a744cbf..9398a1b 100644 --- a/test/rule/gardr.test.js +++ b/test/rule/gardr.test.js @@ -1,11 +1,9 @@ var referee = require('referee'); var assert = referee.assert; -var refute = referee.refute; -var help = require('../lib/validateHelpers.js'); -var hook = require('../../lib/rule/hook/gardr.js'); +var instrumentation = require('../../lib/rule/instrument/gardr.js'); -describe('Gardr hooks', function () { +describe('Gardr instrumentation', function () { it('should store probes', function () { @@ -30,13 +28,13 @@ describe('Gardr hooks', function () { } }; calls++; - res = fn(); + var res = fn(); global.window = null; return res; } }; - hook.onPageOpen(api); + instrumentation.onPageOpen(api); assert.equals(calls, 2); assert(arg, 'should have collected initManager argument'); @@ -45,17 +43,7 @@ describe('Gardr hooks', function () { }); -var validator = require('../../lib/rule/validator/gardr.js'); - -var defaultOptions = { - width: 980, - height: 225, - options: { - format: { - height: 225 - } - } -}; +var help = require('../lib/validateHelpers.js'); describe('Gardr validator', function () { @@ -87,12 +75,12 @@ describe('Gardr validator', function () { var reporter = help.createReporter.call(this); - validator.validate(harvest, reporter, function () { + help.callValidator('gardr', harvest, reporter, function () { var result = reporter.getResult(); assert.equals(result.error.length, 4); done(); - }, defaultOptions); + }); }); @@ -128,21 +116,19 @@ describe('Gardr validator', function () { var harvest = getValid('function(){window.open(url, "new_window");}'); var reporter = help.createReporter.call(this); - var options = { - width: 980, - height: 225, - options: { - format: { - height: 400 - } - } - }; - validator.validate(harvest, reporter, function () { + function mutate(context, options){ + options.width.min = 400; + options.width.max = 400; + } + + function handler() { var result = reporter.getResult(); - assert.equals(result.error.length, 1, '400 is height format, other heights should throw error'); + assert.equals(result.error.length, 1, '400 is height format, other heights should generate error'); done(); - }, options); + } + + help.callValidator('gardr', harvest, reporter, handler, mutate); }); @@ -152,12 +138,12 @@ describe('Gardr validator', function () { var reporter = help.createReporter.call(this); - validator.validate(harvest, reporter, function () { + help.callValidator('gardr', harvest, reporter, function () { var result = reporter.getResult(); assert.equals(result.error.length, 1); done(); - }, defaultOptions); + }); }); @@ -167,35 +153,32 @@ describe('Gardr validator', function () { var reporter = help.createReporter.call(this); - validator.validate(harvest, reporter, function () { + help.callValidator('gardr', harvest, reporter, function () { var result = reporter.getResult(); assert.equals(result.error.length, 0); - }, defaultOptions); + }); harvest = getValid('window.open(url, "new_window")'); - validator.validate(harvest, reporter, function () { + help.callValidator('gardr', harvest, reporter, function () { var result = reporter.getResult(); assert.equals(result.error.length, 0); done(); - }, defaultOptions); + }); }); it('should error on invalid ref', function (done) { - var harvest = getValid('function(){open(url, "_blank");}'); - var reporter = help.createReporter.call(this); - validator.validate(harvest, reporter, function () { + help.callValidator('gardr', harvest, reporter, function () { var result = reporter.getResult(); - assert.equals(result.error.length, 1); done(); - }, defaultOptions); + }); }); @@ -205,16 +188,16 @@ describe('Gardr validator', function () { var reporter = help.createReporter.call(this); - validator.validate(harvest, reporter, function () { + help.callValidator('gardr', harvest, reporter, function () { var result = reporter.getResult(); assert.equals(result.error.length, 1); done(); - }, defaultOptions); + }); }); it('should call next if missing data', function (done) { - validator.validate(null, null, done, defaultOptions); + help.callValidator('gardr', {}, {}, done); }); }); diff --git a/test/rule/har.test.js b/test/rule/har.test.js index 1bfc6e3..953ecd6 100644 --- a/test/rule/har.test.js +++ b/test/rule/har.test.js @@ -6,7 +6,7 @@ var buster = require('referee'); var assert = buster.assert; var refute = buster.refute; -var hook = require('../../lib/rule/hook/har.js'); +var instrumentation = require('../../lib/rule/instrument/har.js'); var hooksApi = require('../../lib/phantom/hooksApi.js'); describe('HAR hook', function () { @@ -16,12 +16,12 @@ describe('HAR hook', function () { var result = {}; var api = hooksApi({}, {}, result, 'har'); - hook.onLoadStarted(); - hook.onResourceRequested({id: 1}); - hook.onResourceReceived({id: 1, stage: 'start'}); - hook.onResourceReceived({id: 1, stage: 'end'}); - hook.onPageOpen(); - hook.onBeforeExit(api); + instrumentation.onLoadStarted(); + instrumentation.onResourceRequested({id: 1}); + instrumentation.onResourceReceived({id: 1, stage: 'start'}); + instrumentation.onResourceReceived({id: 1, stage: 'end'}); + instrumentation.onPageOpen(); + instrumentation.onBeforeExit(api); var res = result.har.input.resources; @@ -37,7 +37,7 @@ var proxyquire = require('proxyquire'); describe('HAR preprocessor', function () { - var preprocessor = proxyquire('../../lib/rule/preprocessor/har.js', { + var preprocessor = proxyquire('../../lib/rule/preprocess/har.js', { '../../createHAR.js': function (options, harInput) { return harInput; } @@ -119,7 +119,7 @@ describe('HAR preprocessor', function () { }); } - var processResources = require('../../lib/rule/preprocessor/processResources.js'); + var processResources = require('../../lib/rule/preprocess/processResources.js'); it('should populate real sizes and collect contents', function(done){ var harvested = { diff --git a/test/rule/jquery.test.js b/test/rule/jquery.test.js index 814bbb0..21e1eb8 100644 --- a/test/rule/jquery.test.js +++ b/test/rule/jquery.test.js @@ -5,9 +5,9 @@ var refute = referee.refute; var proxyquire = require('proxyquire'); var help = require('../lib/validateHelpers.js'); -var hook = require('../../lib/rule/hook/jquery.js'); +var instrumentation = require('../../lib/rule/instrument/jquery.js'); -describe('jQuery hook', function () { +describe('jQuery instrumentation', function () { it('should call wrap', function () { @@ -18,28 +18,28 @@ describe('jQuery hook', function () { } }; - hook.onResourceReceived({ + instrumentation.onResourceReceived({ url: 'someUrl', stage: 'end' }, api); assert.equals(calls, 0); - hook.onResourceReceived({ + instrumentation.onResourceReceived({ url: 'jquery.js', stage: 'start' }, api); assert.equals(calls, 0); - hook.onResourceReceived({ + instrumentation.onResourceReceived({ url: 'jquery.js', stage: 'end' }, api); assert.equals(calls, 1); - hook.onResourceReceived({ + instrumentation.onResourceReceived({ url: 'jquery.js', stage: 'end' }, api); @@ -68,7 +68,7 @@ describe('jQuery hook', function () { } }; - hook.onBeforeExit(apiShim); + instrumentation.onBeforeExit(apiShim); assert.equals(calls, 2); assert.equals(result.jquery.version, key); @@ -82,13 +82,13 @@ function shimLatest(cb) { { major: 2, minor: 0, patch: 3, sortKey: 20003} ]); } -var jqueryPreprocessor = proxyquire('../../lib/rule/preprocessor/jquery.js', { +var jqueryPreprocessor = proxyquire('../../lib/rule/preprocess/jquery.js', { '../lib/getLatestJquery.js': { 'getLatest': shimLatest } }); -var jqueryValidator = require('../../lib/rule/validator/jquery.js'); +var jqueryValidator = require('../../lib/rule/validate/jquery.js'); describe('jQuery validator', function () { diff --git a/test/rule/log.test.js b/test/rule/log.test.js index 4a6bc49..5386f3f 100644 --- a/test/rule/log.test.js +++ b/test/rule/log.test.js @@ -1,10 +1,10 @@ var buster = require('referee'); var assert = buster.assert; -var refute = buster.refute; +//var refute = buster.refute; var hooksApi = require('../../lib/phantom/hooksApi.js'); -var logHook = require('../../lib/rule/hook/log.js'); -var proc = require('../../lib/rule/preprocessor/log.js'); +var intrumentation = require('../../lib/rule/instrument/log.js'); +var proc = require('../../lib/rule/preprocess/log.js'); describe('Log ', function(){ @@ -14,7 +14,7 @@ describe('Log ', function(){ var message = 'msg'+Math.random()*Date.now(); - logHook.onConsoleMessage(message, null, null, api); + intrumentation.onConsoleMessage(message, null, null, api); assert.equals(result.log.logs[0].message, message); }); diff --git a/test/rule/screenshots.test.js b/test/rule/screenshots.test.js index 1dadd63..9609fb0 100644 --- a/test/rule/screenshots.test.js +++ b/test/rule/screenshots.test.js @@ -1,61 +1,82 @@ +var pathLib = require('path'); var referee = require('referee'); var assert = referee.assert; -var refute = referee.refute; +//var refute = referee.refute; -var hook = require('../../lib/rule/hook/screenshots.js'); -var help = require('../lib/validateHelpers.js'); +var help = require('../lib/validateHelpers.js') +var defaults = require('../../config/config.js').config.screenshots; +var instrumentation = require('../../lib/rule/instrument/screenshots.js'); -describe('screenshots', function () { +describe('Screenshots instrumentation', function () { it('should take images forever', function (done) { var called = 0; var options = { outputDirectory: '/a', - width: 1, - height: 2 + viewport: { + width: 1, + height: 2 + } }; var api = { - getOptions: function(){ + getOptions: function () { return options; }, - getPNG: function(){ - return Math.random()*1000; + getPNG: function () { + return Math.random() * 1000; }, - renderToFile: function(path){ + renderToFile: function (path) { assert.equals(called, 1); assert.equals(path.indexOf('/a/1x2_'), 0); - setTimeout(function(){ - assert.equals(called, 2); + setTimeout(function () { + assert.equals(called, 2, 'should have been called 2 times'); done(); - }, 1); + }, defaults.ms); } }; - global.window = { - setTimeout: function (fn, time) { + setTimeout: function (fn) { called++; - if (called === 1){ + if (called === 1) { fn(); } } }; - hook.onPageOpen(api); + instrumentation.onPageOpen(api, defaults); global.window = 0; }); +}); +describe('Screenshots preprocessor', function () { - describe('validator', function(){ - - it.skip('is missing',function(){ + it('should preprocess data', function (done) { + var harvested = { + 'common': { + startTime: +new Date() + } + }; - }); + function mutateOptions(config, opt){ + opt.outputDirectory = pathLib.join(__dirname, 'fixtures', 'screenshots'); + } + var called = 0; + var outputData = {}; + var output = function(key, value){ + called++; + outputData[key] = value; + }; + help.callPreprocessor('screenshots', harvested, output, function () { + assert.equals(called, 3); + assert.equals(outputData.hasScreenshots, true); + assert(outputData.firstImage); + assert.isArray(outputData.images); + assert.equals(outputData.images.length, 3); + done(); + }, mutateOptions); }); - }); - - diff --git a/test/rule/size.test.js b/test/rule/sizes.test.js similarity index 89% rename from test/rule/size.test.js rename to test/rule/sizes.test.js index 1af0d68..ce560c4 100644 --- a/test/rule/size.test.js +++ b/test/rule/sizes.test.js @@ -2,9 +2,8 @@ var referee = require('referee'); var assert = referee.assert; //var refute = referee.refute; var help = require('../lib/validateHelpers.js'); -var validator = require('../../lib/rule/validator/sizes.js'); -describe('Size validator', function () { +describe('Sizes validator', function () { it('should report on external files', function (done) { var harvested = { @@ -42,9 +41,8 @@ describe('Size validator', function () { } }; var reporter = help.createReporter.call(this); - var options = {}; - validator.validate(harvested, reporter, handler, options); + help.callValidator('sizes', harvested, reporter, handler); function handler() { var report = reporter.getResult(); @@ -79,16 +77,13 @@ describe('Size validator', function () { } }; var reporter = help.createReporter.call(this); - var options = { - target: 'tablet' - }; - validator.validate(harvested, reporter, handler, options); + help.callValidator('sizes', harvested, reporter, handler); function handler() { var report = reporter.getResult(); - assert.equals(report.error.length, 0, 'expect 99 kb to not generate an error if target is tablet'); + assert.equals(report.error.length, 0, 'expect 99 kb to not generate an error '); assert.equals(report.info.length, 1); assert.equals(report.meta.length, 3); @@ -122,16 +117,17 @@ describe('Size validator', function () { } }; var reporter = help.createReporter.call(this); - var options = { - target: 'mobile' - }; - validator.validate(harvested, reporter, handler, options); + function mutate(context){ + context.thresholdBytes = 50000; + } + + help.callValidator('sizes', harvested, reporter, handler, mutate); function handler() { var report = reporter.getResult(); - assert.equals(report.error.length, 1, 'expect 99 kb to generate an error if target is mobile'); + assert.equals(report.error.length, 1, 'expect 99 kb to generate an error'); assert.equals(report.info.length, 0); assert.equals(report.meta.length, 3); diff --git a/test/rule/timers.test.js b/test/rule/timers.test.js index 037ad70..33d4437 100644 --- a/test/rule/timers.test.js +++ b/test/rule/timers.test.js @@ -5,8 +5,8 @@ var assert = referee.assert; var help = require('../lib/validateHelpers.js'); var HOOKS = require('../../lib/phantom/createHooks.js').HOOKS; -var hooks = require('../../lib/rule/hook/timers.js'); -var timers = require('../../lib/rule/validator/timers.js'); +var instrumentation = require('../../lib/rule/instrument/timers.js'); +var timers = require('../../lib/rule/validate/timers.js'); function getTraceList(targetNum, i){ var res = []; @@ -24,14 +24,14 @@ function getTraceList(targetNum, i){ } -describe('timers hooks', function(){ +describe('timers instrumentation', function(){ it('should return an object', function(){ - assert.isObject(hooks); + assert.isObject(instrumentation); }); - it('should only use hooks that exist', function(){ - Object.keys(hooks).forEach(function(hookKey){ + it('should only use instrumentation that exist', function(){ + Object.keys(instrumentation).forEach(function(hookKey){ assert(HOOKS.indexOf(hookKey) !== -1, hookKey + ' is not Valid'); }); }); diff --git a/test/rule/touch.test.js b/test/rule/touch.test.js index bdde115..ff8dda6 100644 --- a/test/rule/touch.test.js +++ b/test/rule/touch.test.js @@ -2,18 +2,17 @@ var referee = require('referee'); var assert = referee.assert; var help = require('../lib/validateHelpers.js'); -var HOOKS = require('../../lib/phantom/createHooks.js').HOOKS; -var hooks = require('../../lib/rule/hook/touch.js'); -var touch = require('../../lib/rule/validator/touch.js'); +var HOOKS = require('../../lib/phantom/createHooks.js').HOOKS; +var intrumentation = require('../../lib/rule/instrument/touch.js'); -describe('Touch/Swipe hooks', function () { +describe('Touch/Swipe intrumentation', function () { it('should return an object', function () { - assert.isObject(hooks); + assert.isObject(intrumentation); }); - it('should only use hooks that exist', function () { - Object.keys(hooks).forEach(function (hookKey) { + it('should only use intrumentation that exist', function () { + Object.keys(intrumentation).forEach(function (hookKey) { assert(HOOKS.indexOf(hookKey) !== -1, hookKey + ' is not Valid'); }); }); @@ -26,7 +25,7 @@ describe('Touch/Swipe validator', function () { var reporter = help.createReporter.call(this); - touch.validate(harvested, reporter, function () { + help.callValidator('touch', harvested, reporter, function () { assert.equals(reporter.getResult().error.length, 1); assert.equals(reporter.getResult().info.length, 0); done(); @@ -51,7 +50,7 @@ describe('Touch/Swipe validator', function () { var reporter = help.createReporter.call(this); - touch.validate(harvested, reporter, function () { + help.callValidator('touch', harvested, reporter, function () { var result = reporter.getResult(); assert.equals(result.error.length, 0, 'should not report errors on valid touch data object'); assert.equals(result.info.length, 1); diff --git a/test/runner/helpers.test.js b/test/runner/helpers.test.js index 5251cc1..aa5d647 100644 --- a/test/runner/helpers.test.js +++ b/test/runner/helpers.test.js @@ -12,26 +12,26 @@ describe('collect options', function () { it('should create a spec file path array', function () { // the api says that runner should provide a function to retrieve a spec, not reflected in the test description - var files = helpers.collectSpec({ - timers: true, - latestJQuery: true, - hooky: HOOKY_PATH - }); + var files = helpers.collectSpec([ + 'timers', + 'latestJQuery', + {name: 'hooky', path: HOOKY_PATH} + ]); assert.equals(files.length, 3); - assert.equals(files[2], HOOKY_PATH); + assert.equals(files[2].path, HOOKY_PATH); }); it('should create a validator file path array', function () { // should provide a list of validator result files - var files = helpers.collectValidator({ - timers: true, - latestJQuery: true, - valy: VALIDATOR_PATH - }); + var files = helpers.collectValidator([ + 'timers', + 'latestJQuery', + {name: 'valy', path: VALIDATOR_PATH} + ]); assert.equals(files.length, 3); - assert.equals(files[2], VALIDATOR_PATH); + assert.equals(files[2].path, VALIDATOR_PATH); }); it('should return error on missing hook or validator files', function (done) { diff --git a/test/runner/runner.test.js b/test/runner/runner.test.js index f1b82af..8bac03d 100644 --- a/test/runner/runner.test.js +++ b/test/runner/runner.test.js @@ -6,18 +6,20 @@ var refute = buster.refute; var proxyquire = require('proxyquire'); var EXPECTED_VALID_REPORT_OBJECT = { - har: {}, - clientHar: {}, - probes: {} + 'key1': {}, + 'key2': {}, + 'key3': {} }; -var runner = proxyquire('../../lib/index.js', { +var mockedRunner = proxyquire('../../lib/index.js', { './spawn.js': function (options, handler, done) { var result = JSON.stringify(EXPECTED_VALID_REPORT_OBJECT); handler(result, done); }, - './validate.js': function(data, validators, done){ - done(null, {}); + './validate.js': { + validate: function (data, validators, done) { + done(null, {}); + } } }); @@ -25,21 +27,18 @@ var HOOKY_PATH = path.resolve(path.join(__dirname, 'fixtures', 'customhook', 'ho describe('Runner (phantomJs)', function () { - it('should require a hooks key', function (done) { - runner.run({ - hooks: null - }, function (err, reportObj) { - refute.isNull(reportObj); + it('should require a option object', function (done) { + mockedRunner.run(null, function (err, reportObj) { + assert(err); + refute(reportObj); done(); }); }); - it('should require a hooks object with hooks or validators', function (done) { - runner.run({ - parentUrl: 'valid', - hooks: { - notValid: true - } + it('should require a intrument object with hooks or validators', function (done) { + mockedRunner.run({ + 'parentUrl': 'valid', + 'instrument': ['notValid'] }, function (err, reportObj) { assert(err, 'Expected a error'); refute(reportObj); @@ -48,21 +47,26 @@ describe('Runner (phantomJs)', function () { }); it('should call spawn when options are valid', function (done) { - // The description does not match the test, spawn is an internal unknown to runner - runner.run({ - hooks: {} - }, function (err, reportObj) { - refute(err); + var options = { + // test gets default (all) validators + // 'instrument': [], + // 'validate': [], + // 'preprocess': [] + }; + var called = 0; + mockedRunner.run(options, function (err, reportObj) { + called++; + assert.equals(called, 1, 'should not call callback more than once'); + refute(err, 'should not return error'); assert.isObject(reportObj); done(); }); }); - describe('handleResult', function () { it('should return an error when parsing invalid json result', function (done) { var strInput1 = 'ssomeasdøasldøsad'; - runner.handleResult(strInput1, function (err, dataObj) { + mockedRunner.handleResult(strInput1, function (err, dataObj) { assert.isObject(err); assert.isNull(dataObj); done(); @@ -73,7 +77,7 @@ describe('Runner (phantomJs)', function () { // feature is to return a data object when given correct input var input1 = EXPECTED_VALID_REPORT_OBJECT; var strInput1 = JSON.stringify(input1); - runner.handleResult(strInput1, function (err, dataObj) { + mockedRunner.handleResult(strInput1, function (err, dataObj) { assert.isNull(err); assert.isObject(dataObj); assert.equals(dataObj, input1); @@ -83,12 +87,12 @@ describe('Runner (phantomJs)', function () { it('should parse result with systemError:true as error', function (done) { var input1 = { - systemError: { + 'systemError': { message: 'huzzlas' } }; var strInput1 = JSON.stringify(input1); - runner.handleResult(strInput1, function (err, dataObj) { + mockedRunner.handleResult(strInput1, function (err, dataObj) { assert.isNull(dataObj); assert.isObject(err); assert.equals(err, input1.systemError); @@ -96,8 +100,8 @@ describe('Runner (phantomJs)', function () { }); }); - it('should not try to parse undefined', function(){ - runner.handleResult(undefined, function(err, dataObj){ + it('should not try to parse undefined', function () { + mockedRunner.handleResult(undefined, function (err, dataObj) { assert(err); refute(dataObj); }); @@ -105,17 +109,16 @@ describe('Runner (phantomJs)', function () { }); - describe('full tests', function(){ + describe('full tests', function () { var runner = require('../../lib/index.js'); - it('should run with default config', function(done){ + it('should run with default config', function (done) { var options = { - hooks: {}, - pageRunTime: 25 + 'pageRunTime': 25 }; - runner.run(options, function(err, result){ - if (err){ - console.log(err); + runner.run(options, function (err, result) { + if (err) { + console.log('TEST RUN ERROR', err); } refute(err); assert(result); @@ -123,15 +126,15 @@ describe('Runner (phantomJs)', function () { }); }); - it('should run with log hooks', function(done){ + it('should run with log instrumentation', function (done) { this.timeout(3000); var options = { - hooks: {log: true}, - pageRunTime: 25 + 'instrument': ['log'], + 'pageRunTime': 25 }; - runner.run(options, function(err, result){ - if (err){ - console.log(err); + runner.run(options, function (err, result) { + if (err) { + console.log('TEST RUN ERROR:', err); } refute(err); assert(result.log.logs, 'expected a log'); @@ -139,17 +142,31 @@ describe('Runner (phantomJs)', function () { }); }); - it('should run with multiple hooks', function(done){ + it('should run with multiple instrumentations', function (done) { this.timeout(3000); var options = { - hooks: {har: true, errors: true, log: true, hooky: HOOKY_PATH }, - preprocessors: {har: true, log: true }, - validators: {errors: true, log: true }, - pageRunTime: 100 + 'instrument': [ + 'har', + 'common', + 'log', + { + name: 'hooky', + path: HOOKY_PATH + } + ], + 'preprocess': [ + 'har', + 'log' + ], + 'validate': [ + 'common', + 'log' + ], + 'pageRunTime': 100 }; - runner.run(options, function(err, result){ - if (err){ - console.log(err); + runner.run(options, function (err, result) { + if (err) { + console.log('TEST RUN ERROR', err); } refute(err); assert(result.log.logs, 'expected a log'); diff --git a/test/runner/spawn.test.js b/test/runner/spawn.test.js index d6a219d..0b0469f 100644 --- a/test/runner/spawn.test.js +++ b/test/runner/spawn.test.js @@ -24,7 +24,7 @@ describe('spawn', function () { var input = {pageUrl: 'about:blank', spec: {a: 'a'}}; this.spawn(input, function( argMock){ refute.equals(input, argMock); - assert(argMock.args[1].indexOf('headers') >-1, 'Smoketest options failed'); + assert.equals(JSON.stringify(input), argMock.args[1]); done(); }); }); diff --git a/test/runner/validate.test.js b/test/runner/validate.test.js index 203a27e..b4d5c31 100644 --- a/test/runner/validate.test.js +++ b/test/runner/validate.test.js @@ -6,14 +6,15 @@ var helpers = require('../../lib/helpers.js'); var validatorLib = require('../../lib/validate.js'); var validate = validatorLib.validate; +//var CUSTOM_HOOK_PATH = path.resolve(path.join(__dirname, 'fixtures', 'customhook', 'hooky.js')); var VALIDATOR_PATH_1 = path.resolve(path.join(__dirname, 'fixtures', 'customvalidator', 'validator1.js')); var VALIDATOR_PATH_2 = path.resolve(path.join(__dirname, 'fixtures', 'customvalidator', 'validator2.js')); describe('Validate', function () { - var files = helpers.collectValidator({ - 'val1': VALIDATOR_PATH_1, - 'val2': VALIDATOR_PATH_2 - }); + var files = helpers.collectValidator([ + {name: 'validator1', path: VALIDATOR_PATH_1}, + {name: 'validator2', path: VALIDATOR_PATH_2} + ]); it('should throw when missing validators', function(){ assert.exception(function(){ @@ -22,30 +23,33 @@ describe('Validate', function () { assert.exception(function(){ - validate({}, {}, function(){}); + validate({}, [], function(){}); }); }); it('should throw on missing validator', function(){ var invalidFiles = ['INVALID_PATH']; - assert.exception(function(){ - validate({}, {validatorFiles: invalidFiles}, function(){}); + validate({}, {validate: invalidFiles}, function(err){ + assert(err); }); }); it('should not require objects, but return error on missing validate function', function(done){ - validate({}, {validatorFiles: [{}]}, function(err){ + var options = { + validate: ['INVALID_MODULE'] + }; + validate({}, options, function(err){ assert(err); done(); }); - }) + }); it('should run a set of validators on probed data', function (done) { assert.equals(files.length, 2); - assert.equals(files[0], VALIDATOR_PATH_1); - assert.equals(files[1], VALIDATOR_PATH_2); + assert.equals(files[0].path, VALIDATOR_PATH_1); + assert.equals(files[1].path, VALIDATOR_PATH_2); var harvested = { 'common': { @@ -55,9 +59,12 @@ describe('Validate', function () { 'hooky2': {} }; - validate(harvested, { - validatorFiles: files - }, function (err, harvested, report) { + var options = { + 'validate': files, + config: {} + }; + + validate(harvested, options, function (err, harvested, report) { refute(err); assert.isObject(report); assert.equals(report.info.length, 1); @@ -69,7 +76,7 @@ describe('Validate', function () { }); - it('should only provide depedencies', function (done) { + it('should only provide depedencies properties from hasvested data to validation function', function (done) { var data = { 'custom': { @@ -79,8 +86,10 @@ describe('Validate', function () { 'filterOut': true }; var validators = { - 'validatorFiles': [{ - validate: function (harvested, report, next) { + 'config': {}, + 'instrument': [{name: 'custom'}], + 'validate': [{ + 'validate': function (harvested, report, next) { refute(harvested.filterOut, 'should only provide dependencies'); assert.equals(harvested.custom.data, data.custom.data); next(); @@ -100,13 +109,21 @@ describe('Validate', function () { 'data': 1 }, 'common': {}, + 'hooky': {}, 'filterOut': true }; var called = 0; var options = { - 'validatorFiles': [], - 'preprocessorFiles': [{ + 'config': {}, + 'instrument': [ + {name: 'custom'} + ], + 'validate': [{ + name: 'validate1', + path: VALIDATOR_PATH_1 + }], + 'preprocess': [{ preprocess: function (harvested, output, next) { called++; output('custom', 'key', 'value'); @@ -116,11 +133,12 @@ describe('Validate', function () { next(); }, dependencies: ['custom'], - name: 'preprocessX' + name: 'custom' }] }; validate(data, options, function(err, harvested){ + refute(err); assert.equals(called, 1, 'expect preprocessors to be called 1 time'); assert.equals(harvested.custom.key, 'value'); assert.equals(harvested.custom.key2, 'value2'); @@ -129,15 +147,16 @@ describe('Validate', function () { }); - it('should throw if trying to output on non-key-dependcies', function(){ + it('should error if trying to output on non-key-dependcies', function(){ var data = { 'common': {} }; var options = { - 'validatorFiles': [], - 'preprocessorFiles': [{ + 'config': {}, + 'validate': [], + 'preprocess': [{ preprocess: function (harvested, output, next) { output('custom', 'key', 'value'); next(); @@ -147,12 +166,10 @@ describe('Validate', function () { }] }; - assert.exception(function(){ - validate(data, options, function(){}); + validate(data, options, function(err){ + assert(err, 'should return an error'); + assert.isObject(err, 'error should be a object'); }); - - - }); describe('Reporthelpers', function(){