public
Last active

A micro-library (4k minified) for DRY:ing up the boring boilerplate of user scripts.

  • Download Gist
README.md
Markdown

on.js

The fun part of user scripting is deciding what happens. The boring part is scavenging the DOM for bits of templated data, or elements you want to mod.

Have on.js do it for you! MIT licensed, or Copyheart: copying is an act of love. Please copy and share!

Quick Example

Want to sift away noise at retailmenot?

// hide the noise at http://www.retailmenot.com/view/vayama.com
on({ dom: [ 'css* #offers li.offer'
          , { deal:   'xpath .'
            , coupon: 'css? .crux .code'
            }
          ]
   , ready: nukeCouponless
   });

function nukeCouponless(offers) {
  offers.forEach(function sift(a) {
    if (!a.coupon)
      a.deal.style.display = 'none';
  });
}

What's the benefit?

Separate logic from page structure.

I could talk all night about what this buys you – here are some highlights:

  • Beautiful code

    Optimize for happiness! This is of course personal, but I love how clean on.js makes my user scripts.

    I can see what they do, at a moment's glance, and what data they depend on. Maintenance gets easier.

  • Sane native javascript types

    The DOM API:s are not only verbose, but also cripple results with bad types! From on.js array selectors, you get a real Array that you can operate on with .forEach, .map, .some, .every, .filter, .reduce, .slice, .indexOf, and all the other really awesome Array.prototype methods.

  • More complex scripts become feasible

    As a script's complexity grows, so does the burden of maintaining it. With on.js, this is still true – but it grows far slower!

    Seeing (and naming) the structure of the input your code operates on helps reading and writing the code operating on it.

  • Reuse one script's page structure awareness

    Maybe you later want to whip up another script using the same data, or even make a shell web scraper for it?

    Copy, paste, done! You're in business – in seconds.

  • Have your scripts magically self-deprecate

    Web sites change. This makes user scripts break, start doing the wrong thing, or filling your console with errors.

    When they change so your script no longer has the stuff it needs, your code will just never run instead of breaking the page.

    If you don't miss it, great! – that site has improved, and your script will never bother it.

    If you do – just update its on.js preamble, and your code just magically works again.

Tests

on.js comes with a test suite. It uses node.js, test'em and coffee-script. Once node is installed, run this to install the latter two:

npm install testem coffee-script -g

Then you can run it like this, for instance:

testem -l chrome

Incidentally, it provides pretty good docs about all the ways you can slice a page, and and how they work.

New tests are easy to add. Just stick them in the appropriate .coffee file in tests/, or make a new .js or .coffee file in the same directory, and test'em will pick it up, too.

They are currently written in jasmine, lightly tweaked for improved readability.

on.js
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
/* coffee-script example usage - at https://github.com/johan/dotjs/commits/johan
 
on path_re: ['^/([^/]+)/([^/]+)(/?.*)', 'user', 'repo', 'rest']
query: true
dom:
keyboard: 'css .keyboard-shortcuts'
branches: 'css+ .js-filter-branches h4 a'
dates: 'css* .commit-group-heading'
tracker: 'css? #gauges-tracker[defer]'
johan_ci: 'xpath* //li[contains(@class,"commit")][.//a[.="johan"]]'
ready: (path, query, dom) ->
 
...would make something like this call, as the path regexp matched, and there
were DOM matches for the two mandatory "keyboard" and "branches" selectors:
 
ready( { user: 'johan', repo: 'dotjs', rest: '/commits/johan' }
, {} // would contain all query args (if any were present)
, { keyboard: Node<a href="#keyboard_shortcuts_pane">
, branches: [ Node<a href="/johan/dotjs/commits/coffee">
, Node<a href="/johan/dotjs/commits/dirs">
, Node<a href="/johan/dotjs/commits/gh-pages">
, Node<a href="/johan/dotjs/commits/johan">
, Node<a href="/johan/dotjs/commits/jquery-1.8.2">
, Node<a href="/johan/dotjs/commits/master">
]
, dates: [ Node<h3 class="commit-group-heading">Oct 07, 2012</h3>
, Node<h3 class="commit-group-heading">Aug 29, 2012</h3>
, ...
]
, tracker: null
, johan_ci: [ Node<li class="commit">, ... ]
}
)
 
A selector returns an array of matches prefixed for "css*" and "css+" (ditto
xpath), and a single result if it is prefixed "css" or "css?":
 
If your script should only run on pages with a particular DOM node (or set of
nodes), use the 'css' or 'css+' (ditto xpath) forms - and your callback won't
get fired on pages that lack them. The 'css?' and 'css*' forms would run your
callback but pass null or [] respectively, on not finding such nodes. You may
recognize the semantics of x, x?, x* and x+ from regular expressions.
 
(see http://goo.gl/ejtMD for a more thorough discussion of something similar)
 
The dom property is recursively defined so you can make nested structures.
If you want a property that itself is an object full of matched things, pass
an object of sub-dom-spec:s, instead of a string selector:
 
on dom:
meta:
base: 'xpath? /head/base
title: 'xpath string(/head/title)'
commits: 'css* li.commit'
ready: (dom) ->
 
You can also deconstruct repeated templated sections of a page into subarrays
scraped as per your specs, by picking a context node for a dom spec. This is
done by passing a two-element array: a selector resolving what node/nodes you
look at and a dom spec describing how you want it/them deconstructed for you:
 
on dom:
meta:
[ 'xpath /head',
base: 'xpath? base
title: 'xpath string(title)'
]
commits:
[ 'css* li.commit',
avatar_url: ['css img.gravatar', 'xpath string(@src)']
author_name: 'xpath string(.//*[@class="author-name"])'
]
ready: (dom) ->
 
The mandatory/optional selector rules defined above behave as you'd expect as
used for context selectors too: a mandatory node or array of nodes will limit
what pages your script gets called on to those that match it, so your code is
free to assume it will always be there when it runs. An optional context node
that is not found will instead result in that part of your DOM being null, or
an empty array, in the case of a * selector.
 
Finally, there is the xpath! keyword, which is similar to xpath, but it also
mandates that whatever is returned is truthy. This is useful when you use the
xpath functions returning strings, numbers and of course booleans, to assert
things about the pages you want to run on, like 'xpath! count(//img) = 0', if
you never want the script to run on pages with inline images, say.
 
After you have called on(), you may call on.dom to do page scraping later on,
returning whatever matched your selector(s) passed. Mandatory selectors which
failed to match at this point will return undefined, optional selectors null:
 
on.dom('xpath //a[@id]') => undefined or <a id="...">
on.dom('xpath? //a[@id]') => null or <a id="...">
on.dom('xpath+ //a[@id]') => undefined or [<a id="...">, <a id="...">, ...]
on.dom('xpath* //a[@id]') => [] or [<a id="...">, <a id="...">, ...]
 
A readable way to detect a failed mandatory match is on.dom(...) === on.FAIL;
 
Github pjax hook: for re-running a script's on() block for every pjax request
to a site - add a pushstate hook as per http://goo.gl/LNSv1 -- and be sure to
make your script reentrant, so that it won't try to process the same elements
again, if they are still sitting around in the page (see ':not([augmented])')
 
*/
 
function on(opts, plugins) {
var Object_toString = Object.prototype.toString
, Array_slice = Array.prototype.slice
, FAIL = 'dom' in on ? undefined : (function() {
var tests =
{ path_re: { fn: test_regexp }
, query: { fn: test_query }
, dom: { fn: test_dom
, my: { 'css*': $c
, 'css+': one_or_more($c)
, 'css?': $C
, 'css': not_null($C)
, 'xpath*': $x
, 'xpath+': one_or_more($x)
, 'xpath?': $X
, 'xpath!': truthy($x)
, 'xpath': not_null($X)
}
}
, inject: { fn: inject }
}
, name, test, me, my, mine
;
 
for (name in tests) {
test = tests[name];
me = test.fn;
if ((my = test.my))
for (mine in my)
me[mine] = my[mine];
on[name] = me;
}
})()
 
, input = [] // args for the callback(s?) the script wants to run
, rules = Object.create(opts) // wraps opts in a pokeable inherit layer
, debug = get('debug')
, script = get('name')
, ready = get('ready')
, load = get('load')
, pushState = get('pushstate')
, pjax_event = get('pjaxevent')
, name, rule, test, result, retry, plugin
;
 
if (typeof ready !== 'function' &&
typeof load !== 'function' &&
typeof pushState !== 'function') {
alert('no on function');
throw new Error('on() needs at least a "ready" or "load" function!');
}
 
if (plugins)
for (name in plugins)
if ((rule = plugins[name]) && (test = on[name]))
for (plugin in rule)
if (!(test[plugin])) {
on._parse_dom_rule = null;
test[plugin] = rule[plugin];
}
 
if (pushState && history.pushState &&
(on.pushState = on.pushState || []).indexOf(opts) === -1) {
on.pushState.push(opts); // make sure we don't re-register after navigation
initPushState(pushState, pjax_event);
}
 
try {
for (name in rules) {
rule = rules[name];
if (rule === undefined) continue; // was some callback or other non-rule
test = on[name];
if (!test) throw new Error('did not grok rule "'+ name +'"!');
result = test(rule);
if (result === FAIL) return false; // the page doesn't satisfy all rules
input.push(result);
}
}
catch(e) {
if (debug) console.warn("on(debug): we didn't run because " + e.message);
return false;
}
 
if (ready) {
ready.apply(opts, input.concat());
}
if (load) window.addEventListener('load', function() {
load.apply(opts, input.concat());
});
return input.concat(opts);
 
function get(x) { rules[x] = undefined; return opts[x]; }
function isArray(x) { return Object_toString.call(x) === '[object Array]'; }
function isObject(x) { return Object_toString.call(x) === '[object Object]'; }
function array(a) { return Array_slice.call(a, 0); } // array:ish => Array
function arrayify(x) { return isArray(x) ? x : [x]; } // non-array? => Array
function inject(fn, args) {
var script = document.createElement('script')
, parent = document.documentElement;
args = JSON.stringify(args || []).slice(1, -1);
script.textContent = '('+ fn +')('+ args +');';
parent.appendChild(script);
parent.removeChild(script);
}
 
function initPushState(callback, pjax_event) {
if (!history.pushState.armed) {
inject(function(pjax_event) {
function reportBack() {
var e = document.createEvent('Events');
e.initEvent('history.pushState', !'bubbles', !'cancelable');
document.dispatchEvent(e);
}
var pushState = history.pushState;
history.pushState = function on_pushState() {
if (pjax_event && window.$ && $.pjax)
$(document).one(pjax_event, reportBack);
else
setTimeout(reportBack, 0);
return pushState.apply(this, arguments);
};
}, [pjax_event]);
history.pushState.armed = pjax_event;
}
 
retry = function after_pushState() {
rules = Object.create(opts);
rules.load = rules.pushstate = undefined;
rules.ready = callback;
on(rules);
};
 
document.addEventListener('history.pushState', function() {
if (debug) console.log('on.pushstate', location.pathname);
retry();
}, false);
}
 
function test_query(spec) {
var q = unparam(this === on || this === window ? location.search : this);
if (spec === true || spec == null) return q; // decode the query for me!
throw new Error('bad query type '+ (typeof spec) +': '+ spec);
}
 
function unparam(query) {
var data = {};
(query || '').replace(/\+/g, '%20').split('&').forEach(function(kv) {
kv = /^\??([^=&]*)(?:=(.*))?/.exec(kv);
if (!kv) return;
var prop, val, k = kv[1], v = kv[2], e, m;
try { prop = decodeURIComponent(k); } catch (e) { prop = unescape(k); }
if ((val = v) != null)
try { val = decodeURIComponent(v); } catch (e) { val = unescape(v); }
data[prop] = val;
});
return data;
}
 
function test_regexp(spec) {
if (!isArray(spec)) spec = arrayify(spec);
var re = spec.shift();
if (typeof re === 'string') re = new RegExp(re);
if (!(re instanceof RegExp))
throw new Error((typeof re) +' was not a regexp: '+ re);
 
var ok = re.exec(this === on || this === window ? location.pathname : this);
if (ok === null) return FAIL;
if (!spec.length) return ok;
var named = {};
ok.shift(); // drop matching-whole-regexp part
while (spec.length) named[spec.shift()] = ok.shift();
return named;
}
 
function truthy(fn) { return function(s) {
var x = fn.apply(this, arguments); return x || FAIL;
}; }
 
function not_null(fn) { return function(s) {
var x = fn.apply(this, arguments); return x !== null ? x : FAIL;
}; }
 
function one_or_more(fn) { return function(s) {
var x = fn.apply(this, arguments); return x.length ? x : FAIL;
}; }
 
function $c(css) { return array(this.querySelectorAll(css)); }
function $C(css) { return this.querySelector(css); }
 
function $x(xpath) {
var doc = this.evaluate ? this : this.ownerDocument, next;
var got = doc.evaluate(xpath, this, null, 0, null), all = [];
switch (got.resultType) {
case 1/*XPathResult.NUMBER_TYPE*/: return got.numberValue;
case 2/*XPathResult.STRING_TYPE*/: return got.stringValue;
case 3/*XPathResult.BOOLEAN_TYPE*/: return got.booleanValue;
default: while ((next = got.iterateNext())) all.push(next); return all;
}
}
function $X(xpath) {
var got = $x.call(this, xpath);
return got instanceof Array ? got[0] || null : got;
}
 
function quoteRe(s) { return (s+'').replace(/([-$(-+.?[-^{|}])/g, '\\$1'); }
 
// DOM constraint tester / scraper facility:
// "this" is the context Node(s) - initially the document
// "spec" is either of:
// * css / xpath Selector "selector_type selector"
// * resolved for context [ context Selector, spec ]
// * an Object of spec(s) { property_name: spec, ... }
function test_dom(spec, context) {
// returns FAIL if it turned out it wasn't a mandated match at this level
// returns null if it didn't find optional matches at this level
// returns Node or an Array of nodes, or a basic type from some XPath query
function lookup(rule) {
switch (typeof rule) {
case 'string': break; // main case - rest of function
case 'object': if ('nodeType' in rule || rule.length) return rule;
// fall-through
default: throw new Error('non-String dom match rule: '+ rule);
}
if (!on._parse_dom_rule) on._parse_dom_rule = new RegExp('^(' +
Object.keys(on.dom).map(quoteRe).join('|') + ')\\s*(.*)');
var match = on._parse_dom_rule.exec(rule), type, func;
if (match) {
type = match[1];
rule = match[2];
func = test_dom[type];
}
if (!func) throw new Error('unknown dom match rule '+ type +': '+ rule);
return func.call(this, rule);
}
 
var results, result, i, property_name;
if (context === undefined) {
context = this === on || this === window ? document : this;
}
 
// validate context:
if (context === null || context === FAIL) return FAIL;
if (isArray(context)) {
for (results = [], i = 0; i < context.length; i++) {
result = test_dom.call(context[i], spec);
if (result !== FAIL)
results.push(result);
}
return results;
}
if (typeof context !== 'object' || !('nodeType' in context))
throw new Error('illegal context: '+ context);
 
// handle input spec format:
if (typeof spec === 'string') return lookup.call(context, spec);
if (isArray(spec)) {
context = lookup.call(context, spec[0]);
if (context === null || context === FAIL) return context;
return test_dom.call(context, spec[1]);
}
if (isObject(spec)) {
results = {};
for (property_name in spec) {
result = test_dom.call(context, spec[property_name]);
if (result === FAIL) return FAIL;
results[property_name] = result;
}
return results;
}
 
throw new Error("dom spec was neither a String, Object nor Array: "+ spec);
}
}
testem.json
JSON
1 2 3 4 5 6 7 8 9 10 11
{ "framework": "jasmine"
, "src_files":
[ "on.js"
, "tests/*.coffee"
]
, "serve_files":
[ "on.js"
, "tests/*.js"
]
, "before_tests": "coffee -c tests/*.coffee"
}
tests/.gitignore
1
*.js
tests/on.dom.coffee
CoffeeScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
section = ((describe) -> (name, test) -> describe "#{name}\t", test)(describe)
 
section 'on()', ->
fn = `on`
 
it 'should throw an error on no input', ->
try fn() catch e then err = e
expect(err).toNotBe undefined
 
it 'should expose an on.dom function after the first call', ->
expect(typeof `on`.dom).toBe 'function'
 
it 'should expose an on.query function after the first call', ->
expect(typeof `on`.query).toBe 'function'
 
it 'should expose an on.path_re function after the first call', ->
expect(typeof `on`.path_re).toBe 'function'
 
it 'should accept an object with "path_re", "dom", and/or "query" specs', ->
what = fn( ready: (->), path_re: '/', dom: 'css *', query: true)
expect(what).toEqual jasmine.any Array
expect(what.length).toBeGreaterThan 2
 
 
 
section 'on.dom(dom spec – see below for the three types of dom spec)', ->
it 'should be a function after the first on() call', ->
try `on()` catch e then err = e
expect(err).toNotBe undefined
 
it 'should expose on.dom.* functions once on.dom() has run once', ->
try
`on`.dom()
fns = Object.keys(`on`.dom).join(',')
expect(fns).toBe 'css*,css+,css?,css,xpath*,xpath+,xpath?,xpath!,xpath'
 
 
 
section 'on.dom(dom spec type 1: a selector string)', ->
root = document.documentElement
assertion = it
 
section 'on.dom("css… selection"): Array/Node, optional/not?', ->
section 'Array of Node:s (0+ occurrences):', ->
assertion 'on.dom("css* NotFound") => []', ->
expect(`on`.dom('css* NotFound')).toEqual []
 
assertion 'on.dom("css* html") => [html]', ->
expect(`on`.dom('css* html')).toEqual [root]
 
assertion 'on.dom("css* *") => document.all (but as a proper Array)', ->
what = `on`.dom("css* *")
dall = [].slice.call document.getElementsByTagName('*'), 0
expect(what).toEqual dall
 
 
section 'Array of Node:s (1+ occurrences):', ->
assertion 'on.dom("css+ html") => [html]', ->
expect(`on`.dom('css+ html')).toEqual [root]
 
assertion 'on.dom("css+ NotFound") => undefined', ->
expect(`on`.dom('css+ NotFound')).toBe undefined
 
 
section 'single optional Node, or null if not found:', ->
assertion 'on.dom("css? *") => root element (= first match)', ->
expect(`on`.dom('css? *')).toBe root
 
assertion 'on.dom("css? NotFound") => null (not found)', ->
expect(`on`.dom('css? NotFound')).toBe null
 
 
section 'single mandatory Node:', ->
assertion 'on.dom("css *") => the root element', ->
expect(`on`.dom('css *')).toBe root
 
assertion 'on.dom("css NotFound") => undefined (unsatisfied)', ->
expect(`on`.dom('css NotFound')).toBe undefined
 
 
 
section 'on.dom("xpath… selection"): Array/Node, optional/not?', ->
section 'xpath* => Array of Node:s (0+ occurrences):', ->
assertion 'on.dom("xpath* /*") => [root element]', ->
expect(`on`.dom('xpath* /*')).toEqual [root]
 
assertion 'on.dom("xpath* /NotFound") => []', ->
expect(`on`.dom('xpath* /NotFound')).toEqual []
 
 
section 'xpath+ => Array of Node:s (1+ occurrences):', ->
assertion 'on.dom("xpath+ /*") => [root element]', ->
expect(`on`.dom('xpath+ /*')).toEqual [root]
 
assertion 'on.dom("xpath+ /NotFound") => undefined', ->
expect(`on`.dom('xpath+ /NotFound')).toBe undefined
 
 
section 'xpath? => single optional Node, or null if missing:', ->
assertion 'on.dom("xpath? /NotFound") => null', ->
expect(`on`.dom('xpath? /NotFound')).toBe null
 
assertion 'on.dom("xpath? /*") => the root element', ->
expect(`on`.dom('xpath? /*')).toBe root
 
 
section 'xpath => single mandatory Node:', ->
assertion 'on.dom("xpath /*") => the root element', ->
expect(`on`.dom('xpath /*')).toBe root
 
assertion 'on.dom("xpath /NotFound") => undefined', ->
expect(`on`.dom('xpath /NotFound')).toBe undefined
 
assertion 'on.dom("xpath .") => the current document', ->
expect(`on`.dom('xpath .')).toBe document
 
 
section '…or queries yielding Number/String/Boolean answers:', ->
assertion 'on.dom("xpath count(/)") => 1', ->
expect(`on`.dom('xpath count(/)')).toBe 1
 
assertion 'on.dom("xpath count(/NotFound)") => 0', ->
expect(`on`.dom('xpath count(/NotFound)')).toBe 0
 
assertion 'on.dom("xpath name(/*)") => "html" or "HTML"', ->
expect(`on`.dom('xpath name(/*)')).toMatch /^(html|HTML)$/
 
assertion 'on.dom("xpath name(/)") => ""', ->
expect(`on`.dom('xpath name(/)')).toBe ''
 
assertion 'on.dom("xpath count(/*) = 1") => true', ->
expect(`on`.dom('xpath count(/*) = 1')).toBe true
 
assertion 'on.dom("xpath name(/*) = \'nope\'") => false', ->
expect(`on`.dom('xpath name(/*) = \'nope\'')).toBe false
 
 
section 'xpath! makes assertions, requiring truthy answers:', ->
assertion 'on.dom("xpath! count(/)") => 1', ->
expect(`on`.dom('xpath count(/)')).toBe 1
 
assertion 'on.dom("xpath! count(/NotFound)") => undefined', ->
expect(`on`.dom('xpath! count(/NotFound)')).toBe undefined
 
assertion 'on.dom("xpath! name(/*)") => "html"', ->
expect(`on`.dom('xpath! name(/*)')).toMatch /^(html|HTML)$/
 
assertion 'on.dom("xpath! name(/)") => undefined', ->
expect(`on`.dom('xpath! name(/)')).toBe undefined
 
assertion 'on.dom("xpath! count(/*) = 1") => true', ->
expect(`on`.dom('xpath! count(/*) = 1')).toBe true
 
assertion 'on.dom("xpath! name(/*) = \'nope\'") => undefined', ->
expect(`on`.dom('xpath! name(/*) = \'nope\'')).toBe undefined
 
 
 
section 'on.dom(dom spec type 2: an object showing the structure you want)', ->
html = document.documentElement
head = document.querySelector 'head'
try `on()` # ensures there's an on.dom to call
assertion = it
 
pluralize = (n, noun) -> "#{n} #{noun}#{if n is 1 then '' else 's'}"
 
assertion 'on.dom({}) => {} (fairly useless, but minimal, test case)', ->
expect(`on`.dom({})).toEqual {}
 
assertion 'on.dom({ h:"css head", H:"css html" }) => { h:head, H:html }', ->
expect(`on`.dom({ h:"css head", H:"css html" })).toEqual { h:head, H:html }
 
assertion 'on.dom({ h:"css head", f:"css? foot" }) => { h:head, f:null }', ->
expect(`on`.dom({ h:"css head", f:"css? foot" })).toEqual { h:head, f:null }
 
assertion 'on.dom({ h:"css head", f:"css foot" }) => undefined (no foot!)', ->
expect(`on`.dom({ h:"css head", f:"css foot" })).toEqual undefined
 
assertion 'on.dom({ x:"css* frame" }) => { x:[] } (frames optional here)', ->
expect(`on`.dom({ x:"css* frame" })).toEqual { x:[] }
 
assertion 'on.dom({ x:"css+ frame" }) => undefined (but mandatory here!)', ->
expect(`on`.dom({ x:"css+ frame" })).toBe undefined
 
assertion 'on.dom({ x:"css* script" }) => { x:[…all (>=0) script tags…] }', ->
what = `on`.dom({ x:"css* script" })
expect(what.x).toEqual jasmine.any Array
expect(what.x.every (s) -> s.nodeName is 'script')
 
assertion 'on.dom({ x:"css+ script" }) => { x:[…all (>0) script tags…] }', ->
what = `on`.dom({ x:"css+ script" })
expect(what.x).toEqual jasmine.any Array
expect(what.x.length).toBeGreaterThan 0
expect(what.x.every (s) -> s.nodeName.toLowerCase() is 'script').toBe true
 
assertion 'on.dom({ c:"xpath count(//script)" }) => {c:N} (any N is okay)', ->
what = `on`.dom({ c:"xpath count(//script)" })
expect(what).toEqual jasmine.any Object
expect(N = what.c).toEqual jasmine.any Number
console.log "on.dom({ c: count(…) }) found #{pluralize N, 'script'}"
delete what.c
expect(what).toEqual {}
 
assertion 'on.dom({ c:"xpath! count(//script)" }) => {c:N} (only N!=0 ok)', ->
what = `on`.dom({ c:"xpath! count(//script)" })
expect(what.c).toBeGreaterThan 0
delete what.c
expect(what).toEqual {}
 
assertion 'on.dom({ c:"xpath! count(//missing)" }) => undefined (as N==0)', ->
expect(`on`.dom({ c:"xpath! count(//missing)" })).toBe undefined
 
assertion 'on.dom({ c:"xpath! count(//*) and /html" }) => { c:true }', ->
expect(`on`.dom({ c:"xpath! count(//*) > 5 and /html" })).toEqual c: true
 
 
 
section 'on.dom(dom spec type 3: [context_spec, per_match_spec])', ->
html = document.documentElement
head = document.querySelector 'head'
try `on()` # ensures there's an on.dom to call
assertion = it
 
assertion 'on.dom(["css* script[src]", "xpath string(@src)"]) => ["url"…]', ->
what = `on`.dom(["css* script[src]", "xpath string(@src)"])
expect(what).toEqual jasmine.any Array
expect(what.every (s) -> typeof s is 'string').toBe true
 
assertion 'on.dom(["css? script:not([src])", "xpath string(.)"]) => "js…"', ->
what = `on`.dom(["css? script:not([src])", "xpath string(.)"])
expect(typeof what).toBe 'string'
desc = 'Code of first inline script tag'
console.log "#{desc}:\n#{what}\n(#{desc} ends.)"
 
assertion 'on.dom(["css? script:not([src])", "xpath! string(@src)"])' +
' => undefined (empty string is not truthy => not a match)', ->
what = `on`.dom(["css? script:not([src])", "xpath! string(@src)"])
expect(what).toBe undefined
 
assertion 'on.dom(["xpath /svg", "css* *"]) => undefined (not an svg doc)', ->
expect(`on`.dom(["xpath /svg", "css* *"])).toBe undefined
 
assertion 'on.dom([html, "xpath ."]) => html', ->
expect(`on`.dom([html, "xpath ."])).toBe html
 
assertion 'on.dom([[head, html], "xpath ."]) => [head, html]', ->
expect(`on`.dom([[head, html], "xpath ."])).toEqual [head, html]
 
 
section 'on.dom plugins:', ->
html = document.documentElement
assertion = it
fn = `on`
 
assertion 'on( { dom: "my_plugin", ready: ready = (x) -> }\n' +
' , { dom: "my_plugin": -> document.body }\n' +
' ) => ready(document.body)', ->
fn( { dom: "my_plugin", ready: ready = jasmine.createSpy 'ready' }
, { dom: "my_plugin": -> document.body }
)
expect(ready).toHaveBeenCalledWith(document.body)
 
assertion 'on.dom(["my_plugin", "xpath ."]) => body', ->
expect(`on`.dom(["my_plugin", "xpath ."])).toBe document.body
 
assertion 'on.dom(["my_plugin", "xpath .."]) => html', ->
expect(`on`.dom(["my_plugin", "xpath .."])).toBe html
 
assertion 'on.dom("xpath .") => document', ->
expect(`on`.dom("xpath .")).toBe document
 
###
assertion 'on( { dom: ["my_plugin", "xpath ."], ready: ready = (x) -> }\n' +
' , { dom: my_plugin: -> document.body })\n' +
'=> ready(body)', ->
ready = jasmine.createSpy 'ready'
fn( { dom: ["my_plugin", "xpath ."], ready: ready }
, { dom: my_plugin: -> document.body })
expect(ready).toHaveBeenCalledWith(document.body)
###
tests/on.query.coffee
CoffeeScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
section = ((describe) -> (name, test) -> describe "#{name}\t", test)(describe)
 
section 'on.query', ->
q_was = location.search
query = (q) ->
if location.search isnt q
url = location.href.replace /(\?[^#]*)?(#.*)?$/, "#{q}$2"
history.replaceState history.state, document.title, url
 
it 'should be a function after the first on() call', ->
try `on()`
expect(typeof `on`.query).toBe 'function'
 
it 'on.query() => {} for a missing query string', ->
query ''
expect(`on`.query()).toEqual {}
 
it 'on.query() => {} for an empty query string ("?")', ->
query '?'
expect(`on`.query()).toEqual {}
 
it 'on.query() => { a:"", x:"0" } for a query string "?a=&x=0"', ->
query '?a=&x=0'
expect(`on`.query()).toEqual
a: ''
x: '0'
 
it 'on.query() => { ugh:undefined } for a query string "?ugh"', ->
query '?ugh'
result = `on`.query()
expect('ugh' of result).toBe true
expect(result).toEqual {} # FIXME - better test framework?
expect(result.ugh).toBe `undefined`
query q_was # reset, for good measure

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.