// Helper class to check DOM MutationEvents // // Usage: // // * Create a new event checker: // var eventChecker = new MutationEventChecker; // * Set the attribute to watch // eventChecker.watchAttr(, ""); // * Set the events to expect (0..n) // eventChecker.expect("add", "modify"); // OR // eventChecker.expect("add modify"); // OR // eventChecker.expect(MutationEvent.ADDITION, MutationEvent.MODIFICATION); // // An empty string or empty set of arguments is also fine as a way of checking // that all expected events have been received and indicating no events are // expected from the following code, e.g. // // eventChecker.expect(""); // // changes that are not expected to generate events // eventChecker.expect("modify"); // // change that is expected to generate an event // ... // // * Either finish listening or set the next attribute to watch // eventChecker.finish(); // eventChecker.watchAttr(element, "nextAttribute"); // // In either case a check is performed that all expected events have been // received. // // * Event checking can be temporarily disabled with ignoreEvents(). The next // call to expect() will cause it to resume. function MutationEventChecker() { this.expectedEvents = []; this.watchAttr = function(element, attr) { if (this.attr) { this.finish(); } this.expectedEvents = []; this.element = element; this.attr = attr; this.oldValue = element.getAttribute(attr); this.giveUp = false; this.ignore = false; this.element.addEventListener('DOMAttrModified', this._listener, false); } this.expect = function() { if (this.giveUp) { return; } ok(this.expectedEvents.length == 0, "Expecting new events for " + this.attr + " but the following previously expected events have still not been " + "received: " + this._stillExpecting()); if (this.expectedEvents.length != 0) { this.giveUp = true; return; } this.ignore = false; if (arguments.length == 0 || arguments.length == 1 && arguments[0] == "") { return; } // Turn arguments object into an array var args = Array.prototype.slice.call(arguments); // Check for whitespace separated keywords if (args.length == 1 && typeof args[0] === 'string' && args[0].indexOf(' ') > 0) { args = args[0].split(' '); } // Convert strings to event Ids this.expectedEvents = args.map(this._argToEventId); } // Temporarily disable event checking this.ignoreEvents = function() { // Check all events have been received ok(this.giveUp || this.expectedEvents.length == 0, "Going to ignore subsequent events on " + this.attr + " attribute, but we're still expecting the following events: " + this._stillExpecting()); this.ignore = true; } this.finish = function() { // Check all events have been received ok(this.giveUp || this.expectedEvents.length == 0, "Finishing listening to " + this.attr + " attribute, but we're still expecting the following events: " + this._stillExpecting()); this.element.removeEventListener('DOMAttrModified', this._listener, false); this.attr = ""; } this._receiveEvent = function(e) { if (this.giveUp || this.ignore) { this.oldValue = e.newValue; return; } // Make sure we're expecting something at all if (this.expectedEvents.length == 0) { ok(false, 'Unexpected ' + this._eventToName(e.attrChange) + ' event when none expected on ' + this.attr + ' attribute.'); return; } var expectedEvent = this.expectedEvents.shift(); // Make sure we got the event we expected if (e.attrChange != expectedEvent) { ok(false, 'Unexpected ' + this._eventToName(e.attrChange) + ' on ' + this.attr + ' attribute. Expected ' + this._eventToName(expectedEvent) + ' (followed by: ' + this._stillExpecting() + ")"); // If we get events out of sequence, it doesn't make sense to do any // further testing since we don't really know what to expect this.giveUp = true; return; } // Common param checking is(e.target, this.element, 'Unexpected node for mutation event on ' + this.attr + ' attribute'); is(e.attrName, this.attr, 'Unexpected attribute name for mutation event'); // Don't bother testing e.relatedNode since Attr nodes are on the way // out anyway (but then, so are mutation events...) // Event-specific checking if (e.attrChange == MutationEvent.MODIFICATION) { ok(this.element.hasAttribute(this.attr), 'Attribute not set after modification'); is(e.prevValue, this.oldValue, 'Unexpected old value for modification to ' + this.attr + ' attribute'); isnot(e.newValue, this.oldValue, 'Unexpected new value for modification to ' + this.attr + ' attribute'); } else if (e.attrChange == MutationEvent.REMOVAL) { ok(!this.element.hasAttribute(this.attr), 'Attribute set after removal'); is(e.prevValue, this.oldValue, 'Unexpected old value for removal of ' + this.attr + ' attribute'); // DOM 3 Events doesn't say what value newValue will be for a removal // event but generally empty strings are used for other events when an // attribute isn't relevant ok(e.newValue === "", 'Unexpected new value for removal of ' + this.attr + ' attribute'); } else if (e.attrChange == MutationEvent.ADDITION) { ok(this.element.hasAttribute(this.attr), 'Attribute not set after addition'); // DOM 3 Events doesn't say what value prevValue will be for an addition // event but generally empty strings are used for other events when an // attribute isn't relevant ok(e.prevValue === "", 'Unexpected old value for addition of ' + this.attr + ' attribute'); ok(typeof(e.newValue) == 'string' && e.newValue !== "", 'Unexpected new value for addition of ' + this.attr + ' attribute'); } else { ok(false, 'Unexpected mutation event type: ' + e.attrChange); this.giveUp = true; } this.oldValue = e.newValue; } this._listener = this._receiveEvent.bind(this); this._stillExpecting = function() { if (this.expectedEvents.length == 0) { return "(nothing)"; } var eventNames = []; for (var i=0; i < this.expectedEvents.length; i++) { eventNames.push(this._eventToName(this.expectedEvents[i])); } return eventNames.join(", "); } this._eventToName = function(evtId) { switch (evtId) { case MutationEvent.MODIFICATION: return "modification"; case MutationEvent.ADDITION: return "addition"; case MutationEvent.REMOVAL: return "removal"; } } this._argToEventId = function(arg) { if (typeof arg === 'number') return arg; if (typeof arg !== 'string') { ok(false, "Unexpected event type: " + arg); return 0; } switch (arg.toLowerCase()) { case "mod": case "modify": case "modification": return MutationEvent.MODIFICATION; case "add": case "addition": return MutationEvent.ADDITION; case "removal": case "remove": return MutationEvent.REMOVAL; default: ok(false, "Unexpected event name: " + arg); return 0; } } }