Skip to content

Commit

Permalink
jQuery: port the module
Browse files Browse the repository at this point in the history
  /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
  • Loading branch information
macbre committed Jan 11, 2019
1 parent bd6ae87 commit dc4231b
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 146 deletions.
155 changes: 12 additions & 143 deletions modules/jQuery/jQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,180 +20,49 @@ 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);
});

// 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;
Expand Down
130 changes: 130 additions & 0 deletions modules/jQuery/scope.js
Original file line number Diff line number Diff line change
@@ -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);
24 changes: 24 additions & 0 deletions test/integration-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)"
Expand Down
6 changes: 3 additions & 3 deletions test/webroot/jquery.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
click(function() {
$(this).parent().css({'background-color': 'blue'});
}).
load(function() {
on('load', function() {
// foo
});

Expand All @@ -46,8 +46,8 @@
});
});

$(window).load(function() {
console.log('window loaded');
$(window).on('load', function() {
// console.log('window loaded');
});
</script>
</body>

0 comments on commit dc4231b

Please sign in to comment.