Last active
August 31, 2015 21:55
-
-
Save jamesreggio/7b7a722d76bc997c3249 to your computer and use it in GitHub Desktop.
Script to probe for links on a page that may not function correctly inside of a WebView control
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This script will probe for links on a page that may not function correctly | |
// inside of a WebView control. These 'bad' links are identified by their use | |
// of window.open, which has undefined behavior in a single-window environment. | |
// | |
// To use the script, copy-paste the entirety into the console of a browser. | |
// (I've tested exclusively in Chrome.) Emulating a mobile user-agent may help | |
// to reduce the occurrence of false-positives. | |
// | |
// The script will simulate a click upon all matching selectors. It does its | |
// best to suppress navigation and other side-effects; however, this is not | |
// perfect. When the script completes, you'll likely need to click | |
// 'Stay on page' several times to prevent script-driven navigation. | |
// | |
// When the script is complete, the offending links will be logged to the | |
// console with with their accompanying problematic usage pattern and its | |
// severity. (Lower severity is worse.) These links will also be outlined in | |
// red, yellow, or green, depending upon severity. | |
// | |
// The most common mistake with window.open is to call it without supplying | |
// a URL. As a mobile web developer, you cannot assume your code will continue | |
// to run after a call to window.open, and thus you should code defensively so | |
// that the user sees something other than a blank screen in this scenario. | |
// Probe selector for bad links | |
// TODO: test for exceptions on null return | |
// TODO: test touch-related events | |
function probe(selector, verbose) { | |
var colors = ['red', 'yellow', 'yellow', 'green']; | |
var failures = {}; | |
// Lower severity is worse | |
function report(sev, msg, node) { | |
var key = '[' + sev + '] ' + msg; | |
failures[key] = failures[key] || []; | |
failures[key].push([node].concat(Array.prototype.slice.call(arguments, 3))); | |
} | |
mock( | |
{ | |
'window.open': function(url) { | |
var node = window.currentNode; | |
// Report initial problems | |
report(2, 'window.open', node, arguments); | |
if (!url) { | |
report(0, 'window.open (no url)', node, arguments); | |
} | |
// Return mock window with additional reporting | |
function illegal(msg) { | |
return function() { | |
report(1, 'window.open (' + msg + ')', node, arguments); | |
}; | |
} | |
return { | |
document: { | |
open: illegal('document.open'), | |
write: illegal('document.write'), | |
}, | |
location: { | |
replace: illegal('location.replace'), | |
}, | |
focus: illegal('focus'), | |
close: illegal('close'), | |
}; | |
}, | |
'window.print': function() { | |
report(3, 'window.print', node, arguments); | |
}, | |
}, | |
function() { | |
console.groupCollapsed('Probing selector for bad links:', selector); | |
// Suppress events | |
function click(e) { | |
verbose && console.log('Preventing default:', e.target); | |
e.preventDefault(); | |
} | |
window.addEventListener('click', click); | |
window.addEventListener('beforeunload', function(e) { | |
var msg = 'Navgiate?'; | |
e.returnValue = msg; | |
return msg; | |
}); | |
// Suppress navigation | |
var base = document.createElement('base'); | |
base.href = 'about:blank'; | |
document.head.appendChild(base); | |
// Simulate input | |
var elements = selector; | |
elements = document.querySelectorAll(elements); | |
elements = Array.prototype.slice.apply(elements); | |
elements.forEach(function(node) { | |
verbose && console.groupCollapsed('Clicking:', node); | |
window.currentNode = node; | |
var event = new MouseEvent('click', { | |
bubbles: true, | |
cancelable: true, | |
}); | |
try { | |
node.dispatchEvent(event); | |
} catch (e) { | |
report(0, 'exception', node, e); | |
} | |
verbose && console.groupEnd(); | |
}); | |
// Restore events and navigation | |
window.removeEventListener('click', click); | |
base.remove(); | |
// Report | |
console.log('Ending probe'); | |
console.groupEnd(); | |
if (!Object.keys(failures).length) { | |
console.log('No bad links found'); | |
} else { | |
Object.keys(failures).sort().forEach(function(key) { | |
var sev = parseInt(key.match(/\[(\d+)\]/)[1], 10); | |
var color = colors[sev]; | |
var nodes = failures[key].reduce(function(nodes, arr) { | |
var node = arr[0]; | |
if (!node.style.border) { | |
node.style.border = '3px solid ' + color; | |
} | |
nodes.push(node); | |
return nodes; | |
}, []); | |
console.log(key, nodes); | |
}); | |
verbose && console.log('All failures:', failures); | |
} | |
} | |
); | |
} | |
// Returns an accessor function for obj[prop]. | |
// When the accessor is invoked with an argument, the value is updated. | |
function accessor(obj, prop) { | |
return function(value) { | |
if (arguments.length) { | |
obj[prop] = value; | |
} | |
return obj[prop]; | |
}; | |
} | |
// Updates accessor with value, and returns the prior value. | |
function replace(accessor, value) { | |
var last = accessor(); | |
accessor(value); | |
return last; | |
} | |
// Walks the period-separated path string, beginning with window. | |
// Returns an accessor or throws an error if not found. | |
function walk(path) { | |
path = path.split('.'); | |
var node = window; | |
var part = path.shift(); | |
if (part === 'window') { | |
part = path.shift(); | |
} | |
while (path.length) { | |
if (!node[part]) { | |
throw new Error('Invalid path: ' + key); | |
} | |
node = node[part]; | |
part = path.shift(); | |
} | |
return accessor(node, part); | |
} | |
// Mocks the specified path/value pairs around the execution of fn. | |
function mock(mocks, fn) { | |
var last = Object.keys(mocks).reduce(function(last, key) { | |
last[key] = replace(walk(key), mocks[key]); | |
return last; | |
}, {}); | |
fn(); | |
Object.keys(last).forEach(function(key) { | |
replace(walk(key), last[key]); | |
}); | |
} | |
// Execute (change false to true for verbose logging) | |
probe('a, button:not([type=submit])', false); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment