From dc4231b56212824d202eb7fdce53fb377dcbc113 Mon Sep 17 00:00:00 2001 From: macbre Date: Fri, 11 Jan 2019 21:01:30 +0100 Subject: [PATCH] jQuery: port the module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /jquery.html ✓ should be generated ✓ should have "jQueryVersion" metric properly set ✓ should have "jQueryOnDOMReadyFunctions" metric properly set ✓ should have "jQueryWindowOnLoadFunctions" metric properly set ✓ should have "jQuerySizzleCalls" metric properly set ✓ should have "jQueryEventTriggers" metric properly set ✗ should have "eventsBound" metric properly set » expected 7, got 0 (strictEqual) // /home/macbre/src/git/phantomas/node_modules/vows/lib/assert/macros.js:14 ✗ should have "eventsDispatched" metric properly set » expected 1, got 0 (strictEqual) // /home/macbre/src/git/phantomas/node_modules/vows/lib/assert/macros.js:14 ✗ should have "eventsScrollBound" metric properly set » expected 2, got 0 (strictEqual) // /home/macbre/src/git/phantomas/node_modules/vows/lib/assert/macros.js:14 ✓ should have "jQueryVersionsLoaded" offender(s) properly set ✓ should have "jQueryWindowOnLoadFunctions" offender(s) properly set ✓ should have "jQueryEventTriggers" offender(s) properly set ✓ should have "jQuerySizzleCalls" offender(s) properly set ✓ should have "jQueryDOMReads" offender(s) properly set ✓ should have "jQueryDOMWrites" offender(s) properly set ✓ should have "jQueryDOMWriteReadSwitches" offender(s) properly set /jquery-multiple.html ✓ should be generated ✓ should have "requests" metric properly set ✓ should have "jsCount" metric properly set ✓ should have "domains" metric properly set ✓ should have "jQueryVersion" metric properly set ✓ should have "jQueryVersionsLoaded" metric properly set ✓ should have "jQueryOnDOMReadyFunctions" metric properly set ✓ should have "jQueryVersionsLoaded" offender(s) properly set --- modules/jQuery/jQuery.js | 155 +++---------------------------------- modules/jQuery/scope.js | 130 +++++++++++++++++++++++++++++++ test/integration-spec.yaml | 24 ++++++ test/webroot/jquery.html | 6 +- 4 files changed, 169 insertions(+), 146 deletions(-) create mode 100644 modules/jQuery/scope.js diff --git a/modules/jQuery/jQuery.js b/modules/jQuery/jQuery.js index cfd730986..655d39375 100644 --- a/modules/jQuery/jQuery.js +++ b/modules/jQuery/jQuery.js @@ -20,156 +20,23 @@ module.exports = function(phantomas) { phantomas.setMetric('jQueryDOMWrites'); // @desc number of DOM write operations phantomas.setMetric('jQueryDOMWriteReadSwitches'); // @desc number of read operations that follow a series of write operations (will cause repaint and can cause reflow) - return; // TODO - - // spy calls to jQuery functions - phantomas.on('init', function() { - phantomas.evaluate(function() { - (function(phantomas) { - // read & write DOM operations (issue #436) - function spyReadsAndWrites(jQuery) { - var TYPE_SET = 'write', - TYPE_GET = 'read'; - - function report(type, funcName, context, args) { - var caller = phantomas.getCaller(1), - contextPath = phantomas.getDOMPath(context); - - args = (typeof args !== 'undefined') ? Array.prototype.slice.apply(args) : undefined; - - phantomas.emit('jQueryOp', type, funcName, args, contextPath, caller); - } - - // "complex" getters and setters - [ - 'attr', - 'css', - 'prop', - ].forEach(function(funcName) { - phantomas.spy(jQuery.fn, funcName, function(propName, val) { - // setter when called with two arguments or provided with key/value set - var isSet = (typeof val !== 'undefined') || (propName.toString() === '[object Object]'); - report(isSet ? TYPE_SET : TYPE_GET, funcName, this[0], arguments); - }); - }); - - // simple getters and setters - [ - 'height', - 'innerHeight', - 'innerWidth', - 'offset', - 'outerHeight', - 'outerWidth', - 'text', - 'width', - 'scrollLeft', - 'scrollTop' - ].forEach(function(funcName) { - phantomas.spy(jQuery.fn, funcName, function(val) { - // setter when called with an argument - var isSet = (typeof val !== 'undefined'); - report(isSet ? TYPE_SET : TYPE_GET, funcName, this[0], arguments); - }); - }); - - // setters - [ - 'addClass', - 'removeAttr', - 'removeClass', - 'removeProp', - 'toggleClass', - ].forEach(function(funcName) { - phantomas.spy(jQuery.fn, funcName, function(val) { - report(TYPE_SET, funcName, this[0], [val]); - }); - }); - // getters - [ - 'hasClass', - 'position', - ].forEach(function(funcName) { - phantomas.spy(jQuery.fn, funcName, function(val) { - report(TYPE_GET, funcName, this[0], arguments); - }); - }); - } - - phantomas.spyGlobalVar('jQuery', function(jQuery) { - var version; - - if (!jQuery || !jQuery.fn) { - phantomas.log('jQuery: unable to detect version!'); - return; - } - - // Tag the current version of jQuery to avoid multiple reports of jQuery being loaded - // when it's actually only restored via $.noConflict(true) - see comments in #435 - if (jQuery.__phantomas === true) { - phantomas.log('jQuery: this instance has already been seen by phantomas'); - return; - } - jQuery.__phantomas = true; - - // report the version of jQuery - version = jQuery.fn.jquery; - phantomas.emit('jQueryLoaded', version); - - // jQuery.ready.promise - // works for jQuery 1.8.0+ (released Aug 09 2012) - phantomas.spy(jQuery.ready, 'promise', function() { - phantomas.incrMetric('jQueryOnDOMReadyFunctions'); - phantomas.addOffender('jQueryOnDOMReadyFunctions', phantomas.getCaller(3)); - }) || phantomas.log('jQuery: can not measure jQueryOnDOMReadyFunctions (jQuery used on the page is too old)!'); - - // Sizzle calls - jQuery.find - // works for jQuery 1.3+ (released Jan 13 2009) - phantomas.spy(jQuery, 'find', function(selector, context) { - phantomas.incrMetric('jQuerySizzleCalls'); - phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (phantomas.getDOMPath(context) || 'unknown')); - }) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!'); - - // jQuery events triggers (issue #440) - phantomas.spy(jQuery.event, 'trigger', function(ev, data, elem) { - var path = phantomas.getDOMPath(elem), - type = ev.type || ev; - - phantomas.log('Event: triggered "%s" on "%s"', type, path); - - phantomas.incrMetric('jQueryEventTriggers'); - phantomas.addOffender('jQueryEventTriggers', '"%s" on "%s"', type, path); - }) || phantomas.log('jQuery: can not measure jQueryEventTriggers (jQuery used on the page is too old)!'); - - // jQuery events bound to window' onLoad event (#451) - phantomas.spy(jQuery.fn, 'on', function(eventName) { - if ((eventName === 'load') && (this[0] === window)) { - phantomas.incrMetric('jQueryWindowOnLoadFunctions'); - phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2)); - } - }) || phantomas.log('jQuery: can not measure jQueryWindowOnLoadFunctions (jQuery used on the page is too old)!'); - - spyReadsAndWrites(jQuery); - }); - })(window.__phantomas); - }); - }); + // inject JS code + phantomas.on('init', () => phantomas.injectJs(__dirname + '/scope.js')); // store the last resource that was received // try to report where given jQuery version was loaded from - phantomas.on('recv', function(entry) { + phantomas.on('recv', entry => { if (entry.isJS) { lastUrl = entry.url; } }); - phantomas.on('jQueryLoaded', function(version) { - phantomas.log('jQuery: loaded v' + version); + phantomas.on('jQueryLoaded', version => { phantomas.setMetric('jQueryVersion', version); // report multiple jQuery "instances" (issue #435) phantomas.incrMetric('jQueryVersionsLoaded'); - phantomas.addOffender('jQueryVersionsLoaded', 'v%s', version); + phantomas.addOffender('jQueryVersionsLoaded', {version: version, url: lastUrl}); phantomas.log('jQuery: v%s (probably loaded from <%s>)', version, lastUrl); }); @@ -177,23 +44,25 @@ module.exports = function(phantomas) { // jQuery read & write operations (issue #436) var lastOp; - phantomas.on('jQueryOp', function(type, funcName, args, contextPath, caller) { - phantomas.log('jQuery: %s op from $.%s(%j) on "%s" - %s', type, funcName, args, contextPath, caller); + phantomas.on('jQueryOp', (type, functionName, args, contextPath, caller) => { + const offenderDetails = {functionName, arguments: JSON.stringify(args), contextPath}; + + phantomas.log('jQuery: %s op from $.%s(%j) on "%s" - %s', type, functionName, args, contextPath, caller); if (type === 'read') { phantomas.incrMetric('jQueryDOMReads'); - phantomas.addOffender('jQueryDOMReads', '$.%s(%j) on "%s"', funcName, args, contextPath); + phantomas.addOffender('jQueryDOMReads', offenderDetails); // This read operation may follow a write operation // In this case browser needs to perform all buffered write operations // in order to update the DOM - this can cause repaints and reflows if (lastOp === 'write') { phantomas.incrMetric('jQueryDOMWriteReadSwitches'); - phantomas.addOffender('jQueryDOMWriteReadSwitches', 'before $.%s(%j) on "%s"', funcName, args, contextPath); + phantomas.addOffender('jQueryDOMWriteReadSwitches', offenderDetails); } } else { phantomas.incrMetric('jQueryDOMWrites'); - phantomas.addOffender('jQueryDOMWrites', '$.%s(%j) on "%s"', funcName, args, contextPath); + phantomas.addOffender('jQueryDOMWrites', offenderDetails); } lastOp = type; diff --git a/modules/jQuery/scope.js b/modules/jQuery/scope.js new file mode 100644 index 000000000..f8594e33e --- /dev/null +++ b/modules/jQuery/scope.js @@ -0,0 +1,130 @@ +(function(phantomas) { + // read & write DOM operations (issue #436) + function spyReadsAndWrites(jQuery) { + var TYPE_SET = 'write', + TYPE_GET = 'read'; + + function report(type, funcName, context, args) { + var caller = phantomas.getCaller(1), + contextPath = phantomas.getDOMPath(context); + + args = (typeof args !== 'undefined') ? Array.prototype.slice.apply(args) : undefined; + + phantomas.emit('jQueryOp', type, funcName, args, contextPath, caller); + } + + // "complex" getters and setters + [ + 'attr', + 'css', + 'prop', + ].forEach(function(funcName) { + phantomas.spy(jQuery.fn, funcName, function(propName, val) { + // setter when called with two arguments or provided with key/value set + var isSet = (typeof val !== 'undefined') || (propName.toString() === '[object Object]'); + report(isSet ? TYPE_SET : TYPE_GET, funcName, this[0], arguments); + }); + }); + + // simple getters and setters + [ + 'height', + 'innerHeight', + 'innerWidth', + 'offset', + 'outerHeight', + 'outerWidth', + 'text', + 'width', + 'scrollLeft', + 'scrollTop' + ].forEach(function(funcName) { + phantomas.spy(jQuery.fn, funcName, function(val) { + // setter when called with an argument + var isSet = (typeof val !== 'undefined'); + report(isSet ? TYPE_SET : TYPE_GET, funcName, this[0], arguments); + }); + }); + + // setters + [ + 'addClass', + 'removeAttr', + 'removeClass', + 'removeProp', + 'toggleClass', + ].forEach(function(funcName) { + phantomas.spy(jQuery.fn, funcName, function(val) { + report(TYPE_SET, funcName, this[0], [val]); + }); + }); + // getters + [ + 'hasClass', + 'position', + ].forEach(function(funcName) { + phantomas.spy(jQuery.fn, funcName, function(val) { + report(TYPE_GET, funcName, this[0], arguments); + }); + }); + } + + phantomas.spyGlobalVar('jQuery', function(jQuery) { + var version; + + if (!jQuery || !jQuery.fn) { + phantomas.log('jQuery: unable to detect version!'); + return; + } + + // Tag the current version of jQuery to avoid multiple reports of jQuery being loaded + // when it's actually only restored via $.noConflict(true) - see comments in #435 + if (jQuery.__phantomas === true) { + phantomas.log('jQuery: this instance has already been seen by phantomas'); + return; + } + jQuery.__phantomas = true; + + // report the version of jQuery + version = jQuery.fn.jquery; + phantomas.emit('jQueryLoaded', version); + + // jQuery.ready.promise - works for jQuery 1.8.0+ (released Aug 09 2012) + // jQuery.read - works for jQuery 3.0.0+ + phantomas.spy(jQuery.ready, 'promise', function() { + phantomas.incrMetric('jQueryOnDOMReadyFunctions'); + phantomas.addOffender('jQueryOnDOMReadyFunctions', phantomas.getCaller(3)); + }) || phantomas.spy(jQuery, 'ready', function() { + phantomas.incrMetric('jQueryOnDOMReadyFunctions'); + phantomas.addOffender('jQueryOnDOMReadyFunctions', phantomas.getCaller(0)); + }) || phantomas.log('jQuery: can not measure jQueryOnDOMReadyFunctions (jQuery used on the page is too old)!'); + + // Sizzle calls - jQuery.find + // works for jQuery 1.3+ (released Jan 13 2009) + phantomas.spy(jQuery, 'find', function(selector, context) { + phantomas.incrMetric('jQuerySizzleCalls'); + phantomas.addOffender('jQuerySizzleCalls', {selector, element: (phantomas.getDOMPath(context) || 'unknown')}); + }) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!'); + + // jQuery events triggers (issue #440) + phantomas.spy(jQuery.event, 'trigger', function(ev, data, elem) { + var path = phantomas.getDOMPath(elem), + type = ev.type || ev; + + phantomas.log('Event: triggered "%s" on "%s"', type, path); + + phantomas.incrMetric('jQueryEventTriggers'); + phantomas.addOffender('jQueryEventTriggers', {type, element: path}); + }) || phantomas.log('jQuery: can not measure jQueryEventTriggers (jQuery used on the page is too old)!'); + + // jQuery events bound to window' onLoad event (#451) + phantomas.spy(jQuery.fn, 'on', function(eventName) { + if ((eventName === 'load') && (this[0] === window)) { + phantomas.incrMetric('jQueryWindowOnLoadFunctions'); + phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2) || phantomas.getCaller(0)); + } + }) || phantomas.log('jQuery: can not measure jQueryWindowOnLoadFunctions (jQuery used on the page is too old)!'); + + spyReadsAndWrites(jQuery); + }); +})(window.__phantomas); \ No newline at end of file diff --git a/test/integration-spec.yaml b/test/integration-spec.yaml index e426a35d6..c9cf05215 100644 --- a/test/integration-spec.yaml +++ b/test/integration-spec.yaml @@ -107,6 +107,25 @@ eventsBound: 7 eventsDispatched: 1 eventsScrollBound: 2 + offenders: + jQueryVersionsLoaded: + - version: "2.1.1" + url: 'http://127.0.0.1:8888/static/jquery-2.1.1.min.js' + jQueryWindowOnLoadFunctions: + - http://127.0.0.1:8888/jquery.html:49:13 + jQueryEventTriggers: + - { type: 'click', element: 'body > div#foo > span.bar' } + - { element: '#document', type: 'ready' } + jQuerySizzleCalls: + - { selector: '#foo .bar', element: '#document' } + jQueryDOMReads: + - { functionName: 'css', arguments: '["color"]', contextPath: 'body > div#foo > span.bar' } + jQueryDOMWrites: + - { functionName: 'css', arguments: '[{"color":"red","background":"green"}]', contextPath: 'body > div#foo > span.bar' } + - { functionName: 'css', arguments: '[{"background-color":"blue"}]', contextPath: 'body > div#foo' } + - { functionName: 'css', arguments: '[{"background-color":"blue"}]', contextPath: 'body > div#foo' } + jQueryDOMWriteReadSwitches: + - { functionName: 'css', arguments: '["color"]', contextPath: 'body > div#foo > span.bar' } # multiple jQuery "instances" (issue #435) - url: "/jquery-multiple.html" metrics: @@ -116,6 +135,11 @@ jQueryVersion: "1.11.1" # the last loaded version jQueryVersionsLoaded: 3 jQueryOnDOMReadyFunctions: 1 + offenders: + jQueryVersionsLoaded: + - { version: "2.1.1", url: 'http://127.0.0.1:8888/static/jquery-2.1.1.min.js' } + - { version: "2.1.1", url: 'http://code.jquery.com/jquery-2.1.1.js' } + - { version: "1.11.1", url: 'http://code.jquery.com/jquery-1.11.1.js' } # --no-externals handling (issue #535) - url: "/jquery-multiple.html" label: "/jquery-multiple.html (with --no-externals)" diff --git a/test/webroot/jquery.html b/test/webroot/jquery.html index 884ad039e..5a976f79d 100644 --- a/test/webroot/jquery.html +++ b/test/webroot/jquery.html @@ -24,7 +24,7 @@ click(function() { $(this).parent().css({'background-color': 'blue'}); }). - load(function() { + on('load', function() { // foo }); @@ -46,8 +46,8 @@ }); }); - $(window).load(function() { - console.log('window loaded'); + $(window).on('load', function() { + // console.log('window loaded'); });