Files
meteor/packages/ui/domrange_tests.js
2013-09-09 10:45:34 -07:00

1101 lines
30 KiB
JavaScript

var DomRange = UI.DomRange;
var parseHTML = UI.DomBackend.parseHTML;
// fake component; DomRange host
var Comp = function (which) {
this.which = which;
new DomRange(this);
};
var isStartMarker = function (n) {
return (n.$ui && n === n.$ui.dom.start);
};
var isEndMarker = function (n) {
return (n.$ui && n === n.$ui.dom.end);
};
var inDocument = function (range, func) {
var onscreen = document.createElement("DIV");
onscreen.style.display = 'none';
document.body.appendChild(onscreen);
DomRange.insert(range.component, onscreen);
try {
func(range);
} finally {
document.body.removeChild(onscreen);
}
};
var htmlRange = function (html) {
var r = new DomRange;
_.each(parseHTML(html), function (node) {
r.add(node);
});
return r;
};
Tinytest.add("ui - DomRange - basic", function (test) {
var r = new DomRange;
r.which = 'R';
// `r.start` and `r.end` -- accessed via
// `r.startNode() and `r.endNode()` -- are adjacent empty
// text nodes used as markers. They are initially created
// in a DocumentFragment or other offscreen container.
// At all times, the members of a DomRange have the same
// parent element (`r.parentNode()`), though this element
// may change (typically just once when the DomRange is
// first put into the DOM).
var rStart = r.startNode();
var rEnd = r.endNode();
test.isTrue(isStartMarker(rStart));
test.isTrue(isEndMarker(rEnd));
test.equal(rStart.nextSibling, rEnd);
test.isTrue(rStart.parentNode);
test.equal(r.parentNode(), rStart.parentNode);
test.equal(typeof r.members, 'object');
test.equal(_.keys(r.members).length, 0);
test.equal(rStart.$ui, r);
test.equal(rEnd.$ui, r);
// add a node
var div = document.createElement("DIV");
r.add(div);
test.equal(_.keys(r.members).length, 1);
test.equal(div.previousSibling, rStart);
test.equal(div.nextSibling, rEnd);
test.equal(div.$ui, r);
test.equal(div.$ui.dom, r);
// add a subrange
var s = new DomRange;
s.which = 'S';
var span = document.createElement("SPAN");
s.add(span);
r.add(s);
test.equal(_.keys(r.members).length, 2);
test.isFalse(r.owner);
test.equal(s.owner, r);
// DOM should go: rStart, DIV, sStart, SPAN, sEnd, rEnd.
test.equal(span.previousSibling, s.startNode());
test.equal(span.nextSibling, s.endNode());
test.equal(span.nextSibling.nextSibling, rEnd);
test.equal(span.previousSibling.previousSibling,
div);
test.equal(span.$ui, s);
// eachMember
var buf = [];
r.eachMember(function (node) {
buf.push(node.nodeName);
}, function (range) {
buf.push('range ' + range.which);
});
buf.sort();
test.equal(buf, ['DIV', 'range S']);
// removal
s.remove();
test.isFalse(s.owner);
// sStart, SPAN, sEnd are gone from the DOM.
test.equal(rStart.nextSibling, div);
test.equal(rEnd.previousSibling, div);
// `r` still has two members
test.equal(_.keys(r.members).length, 2);
// until we refresh
r.refresh();
test.equal(_.keys(r.members).length, 1);
// remove all
r.removeAll();
test.equal(rStart.nextSibling, rEnd);
test.equal(_.keys(r.members).length, 0);
});
Tinytest.add("ui - DomRange - shuffling", function (test) {
var r = new DomRange;
var B = document.createElement("B");
var I = document.createElement("I");
var U = document.createElement("U");
r.add('B', B);
r.add('I', I);
r.add('U', U);
var spellDom = function () {
var frag = r.parentNode();
var str = '';
_.each(frag.childNodes, function (n) {
if (n.nodeType === 3 || isStartMarker(n) ||
isEndMarker(n)) {
if (isStartMarker(n))
str += '(';
else if (isEndMarker(n))
str += ')';
else
str += '-';
} else {
if (n.$ui.which)
str += n.$ui.which;
else
str += (n.nodeName || '?');
}
});
return str;
};
test.equal(spellDom(), '(BIU)');
r.moveBefore('B');
test.equal(spellDom(), '(IUB)');
r.moveBefore('I', 'U');
test.equal(spellDom(), '(IUB)');
r.moveBefore('I', 'B');
test.equal(spellDom(), '(UIB)');
r.moveBefore('B', 'U');
test.equal(spellDom(), '(BUI)');
r.moveBefore('U', null);
test.equal(spellDom(), '(BIU)');
test.equal(B.$ui, r);
// add some member rangers, with host objects
var X = new Comp('X');
var Y = new Comp('Y');
var Z = new Comp('Z');
r.add('X', X, 'I');
X.dom.add(document.createElement("SPAN"));
Y.dom.add(document.createElement("SPAN"));
Z.dom.add(document.createElement("SPAN"));
r.add('Y', Y, 'U');
r.add('Z', Z);
test.equal(spellDom(), '(B(X)I(Y)U(Z))');
r.add([document.createElement('A'),
document.createElement('A')], 'X');
test.equal(spellDom(), '(BAA(X)I(Y)U(Z))');
r.moveBefore('I', 'X');
r.moveBefore('X', 'B');
r.moveBefore('Z', 'U');
r.moveBefore('U', 'Y');
test.equal(spellDom(), '((X)BAAIU(Y)(Z))');
r.moveBefore('Z', 'X');
r.moveBefore('Y', 'X');
test.equal(spellDom(), '((Z)(Y)(X)BAAIU)');
test.equal(r.get('X'), X);
test.equal(r.get('Y'), Y);
test.equal(r.get('Z'), Z);
test.equal(r.get('B'), B);
test.equal(r.get('I'), I);
test.equal(r.get('U'), U);
test.isFalse(r.owner);
test.equal(X.dom.owner, r);
test.equal(Y.dom.owner, r);
test.equal(Z.dom.owner, r);
r.remove('Y');
test.equal(spellDom(), '((Z)(X)BAAIU)');
test.equal(r.get('Y'), null);
r.remove('X');
test.equal(spellDom(), '((Z)BAAIU)');
r.removeAll();
test.equal(spellDom(), '()');
});
Tinytest.add("ui - DomRange - nested", function (test) {
var r = new DomRange;
var spellDom = function () {
var frag = r.parentNode();
var str = '';
_.each(frag.childNodes, function (n) {
var ui = n.$ui;
if (isStartMarker(n))
str += (ui.which ? ui.which : '(');
else if (isEndMarker(n))
str += (ui.which ? ui.which.toLowerCase() : ')');
else
str += '?';
});
return str;
};
// nest empty ranges; should work even though
// there are no element nodes
var A,B,C,D,E,F;
test.equal(spellDom(), '()');
r.add(A = new Comp('A'));
test.equal(spellDom(), '(Aa)');
r.add('B', B = new Comp('B'));
r.add('C', C = new Comp('C'), 'B');
test.equal(spellDom(), '(AaCcBb)');
r.get('B').dom.add('D', D = new Comp('D'));
D.dom.add('E', new Comp('E'));
test.equal(spellDom(), '(AaCcBDEedb)');
B.dom.add('F', F = new Comp('F'));
test.equal(spellDom(), '(AaCcBDEedFfb)');
r.moveBefore('B', 'C');
test.equal(spellDom(), '(AaBDEedFfbCc)');
B.dom.moveBefore('D', null);
test.equal(spellDom(), '(AaBFfDEedbCc)');
r.moveBefore('C', 'B');
test.equal(spellDom(), '(AaCcBFfDEedb)');
D.dom.remove('E');
test.equal(spellDom(), '(AaCcBFfDdb)');
r.remove('B');
test.equal(spellDom(), '(AaCc)');
test.isFalse(r.owner);
test.equal(A.dom.owner, r);
test.equal(C.dom.owner, r);
});
Tinytest.add("ui - DomRange - external moves", function (test) {
// In this one, uppercase letters are div elements,
// lowercase letters are marker text nodes, as follows:
//
// a-X-b - c-d-Y-Z-e-f - g-h-i-W-j-k-l V
//
// In other words, one DomRange containing an element (X),
// then two nested DomRanges containing two elements (Y,Z),
// etc.
var wsp = function () {
return document.createTextNode(' ');
};
var X = document.createElement("DIV");
X.id = 'X';
var Y = document.createElement("DIV");
Y.id = 'Y';
var Z = document.createElement("DIV");
Z.id = 'Z';
var W = document.createElement("DIV");
W.id = 'W';
var V = document.createElement("DIV");
V.id = 'V';
var ab = new Comp('ab');
ab.dom.add(wsp());
ab.dom.add('X', X);
ab.dom.add(wsp());
var cf = new Comp('cf');
var de = new Comp('de');
de.dom.add(wsp());
de.dom.add('Y', Y);
de.dom.add(wsp());
de.dom.add('Z', Z);
de.dom.add(wsp());
cf.dom.add(wsp());
cf.dom.add('de', de);
cf.dom.add(wsp());
var gl = new Comp('gl');
var hk = new Comp('hk');
var ij = new Comp('ij');
ij.dom.add(wsp());
ij.dom.add('W', W);
ij.dom.add(wsp());
// i-W-j
test.equal(ij.dom.getNodes().length, 5);
gl.dom.add(wsp());
gl.dom.add('hk', hk);
gl.dom.add(wsp());
// g-hk-l
test.equal(gl.dom.getNodes().length, 6);
hk.dom.add(wsp());
hk.dom.add('ij', ij);
hk.dom.add(wsp());
// h-i-W-j-k
test.equal(hk.dom.getNodes().length, 9);
// g-h-i-W-j-k-l
test.equal(gl.dom.getNodes().length, 13);
var r = new DomRange;
r.dom.add('ab', ab);
r.dom.add(wsp());
r.dom.add('cf', cf);
r.dom.add(wsp());
r.dom.add('gl', gl);
r.dom.add('V', V);
var spellDom = function () {
var frag = r.parentNode();
var str = '';
_.each(frag.childNodes, function (n) {
var ui = n.$ui;
if (isStartMarker(n))
str += (ui.which ? ui.which.charAt(0) : '(');
else if (isEndMarker(n))
str += (ui.which ? ui.which.charAt(1) : ')');
else if (n.nodeType === 3)
str += '-';
else
str += (n.id || '?');
});
return str;
};
var strip = function (str) {
return str.replace(/[^-\w()]+/g, '');
};
test.equal(spellDom(),
strip('(a-X-b - c-d-Y-Z-e-f - g-h-i-W-j-k-l V)'));
test.equal(ab.dom.owner, r);
test.equal(cf.dom.owner, r);
test.equal(de.dom.owner, cf);
test.equal(gl.dom.owner, r);
test.equal(hk.dom.owner, gl);
test.equal(ij.dom.owner, hk);
// all right, now let's mess around with these elements!
$([Y,Z]).insertBefore(X);
// jQuery lifted Y,Z right out and stuck them before X
test.equal(spellDom(),
strip('(a-YZX-b - c-d---e-f - g-h-i-W-j-k-l V)'));
r.moveBefore('cf', 'ab');
// the move causes a refresh of `ab` and `cf` and their
// descendent members, re-establishing proper organization
// (ignoring whitespace textnodes)
test.equal(spellDom(),
strip('(- cdYZef aX-b ------- g-h-i-W-j-k-l V)'));
$(W).insertBefore(X);
test.equal(spellDom(),
strip('(- cdYZef aWX-b ------- g-h-i--j-k-l V)'));
$(Z).insertBefore(W);
test.equal(spellDom(),
strip('(- cdYef aZWX-b ------- g-h-i--j-k-l V)'));
r.moveBefore('ab', 'cf');
// WOW! `ab` and `cf` have been fixed. Here's what
// happened:
// - Because `cf` is serving as an insertion point, it
// is refreshed first, and it recursively refreshes
// `de`. This causes `e` and then `f` to move to the
// right of `Z`. There's still `a` floating in the middle.
// - Then `ab` is refreshed. This moves `a` to right before
// `X`.
// - Finally, `aX-b` is moved before `c`.
test.equal(spellDom(),
strip('(- aX-b cdYZef W ------- g-h-i--j-k-l V)'));
r.moveBefore('ab', 'gl');
// Because `gl` is being used as a reference point,
// it is refreshed to contain `W`.
// Because the `-` that was initial came from `ab`,
// it is recaptured.
test.equal(spellDom(),
strip('(cdYZef a-X-b ghiWjkl ------------- V)'));
$(Z).insertBefore(X);
test.equal(spellDom(),
strip('(cdYef a-ZX-b ghiWjkl ------------- V)'));
r.moveBefore('gl', 'cf');
// Note that the `a` is still misplaced here.
test.equal(spellDom(),
strip('(ghiWjkl cdY a-ZefX-b ------------- V)'));
r.moveBefore('cf', 'V');
test.equal(spellDom(),
strip('(ghiWjkl X-b ------------- cdY a-Zef V)'));
$(X).insertBefore(Y);
// holy crap, now `aXb` is a mess. Really `a` and `b`
// are in the completely wrong place.
test.equal(spellDom(),
strip('(ghiWjkl -b ------------- cdXY a-Zef V)'));
r.moveBefore('gl', 'ab');
// Now `c` and `d` are wrong. It looks like `cdYZef`
// also includes `W` and `X`.
test.equal(spellDom(),
strip('(-------------- cd ghiWjkl aXbY-Zef V)'));
// However, remove `cf` will do a refresh first.
r.remove('cf');
test.equal(spellDom(),
strip('(-------------- ghiWjkl aXb V)'));
$(X).insertBefore(W);
r.parentNode().appendChild(W);
test.equal(spellDom(),
strip('(-------------- ghiXjkl ab V) W'));
r.moveBefore('ab', 'gl');
test.equal(spellDom(),
strip('(-------------- V) aXb ghiWjkl'));
r.remove('V');
test.equal(spellDom(),
strip('(--------------) aXb ghiWjkl'));
// Manual refresh is required for move-to-end
// (or add-at-end) if elements may have moved externally,
// because the `end` pointer could be totally wrong.
// Otherwise, the order of `ab` and `gl` would swap,
// meaning the DomRange operations would do something
// different from the jQuery operations.
//
// See `range.getInsertionPoint`.
// Same as `r.refresh()` but tests
// the convenience function `DomRange.refresh(element)`:
DomRange.refresh(r.parentNode());
r.moveBefore('gl', null);
test.equal(spellDom(),
strip('-------------- (aXb ghiWjkl)'));
});
Tinytest.add("ui - DomRange - tables", function (test) {
var range = function (x) {
new DomRange(x);
if (x.el) {
x.dom.add(x.el);
if (x.content)
DomRange.insert(x.content, x.el);
}
return x;
};
var tr, td;
var table = range({
el: document.createElement('table'),
content: tr = range({
el: document.createElement('tr'),
content: td = range({
el: document.createElement('td')
})
})
});
// TBODY got inserted automatically.
// This tests DomRange.insert.
test.equal(table.el.childNodes.length, 1);
test.equal(table.el.firstChild.nodeName, 'TBODY');
// TBODY contains [start, TR, end]
test.equal(table.el.firstChild.childNodes.length, 3);
test.equal(table.el.firstChild.childNodes[1], tr.el);
test.equal(tr.el.childNodes.length, 3);
test.equal(tr.el.childNodes[1], td.el);
// start over
$(table.el).empty();
test.equal(table.el.childNodes.length, 0);
table.content = range({});
DomRange.insert(table.content, table.el);
// table has two children (start/end markers), no elements
test.equal(table.el.childNodes.length, 2);
test.notEqual(table.el.firstChild.nodeType, 1);
test.notEqual(table.el.lastChild.nodeType, 1);
// shazam, adding a TR should move the whole range
// into a TBODY. This tests range.add(node).
table.content.dom.add(document.createElement('tr'));
test.equal(table.el.childNodes.length, 1);
test.equal(table.el.firstChild.nodeName, 'TBODY');
test.equal(table.el.firstChild.childNodes.length, 3);
test.equal(table.el.firstChild.childNodes[1].nodeName, 'TR');
// start over.
$(table.el).empty();
test.equal(table.el.childNodes.length, 0);
table.content = range({});
DomRange.insert(table.content, table.el);
var a1 = range({});
var a2 = range({});
a1.dom.add(a2);
table.content.dom.add(a1);
// 6 marker nodes in table, no elements
test.equal(table.el.childNodes.length, 6);
test.equal($(table.el).find("*").length, 0);
// shazam, adding a TR to the innermost range
// should move all the ranges into a TBODY.
a2.dom.add(document.createElement('tr'));
test.equal(table.el.childNodes.length, 1);
test.equal(table.el.firstChild.nodeName, 'TBODY');
test.equal(table.el.firstChild.childNodes.length, 7);
test.equal(table.el.firstChild.childNodes[3].nodeName, 'TR');
// start over. this time test adding a range containing
// a TR.
$(table.el).empty();
test.equal(table.el.childNodes.length, 0);
table.content = range({});
DomRange.insert(table.content, table.el);
var b1 = range({});
var b2 = range({});
table.content.dom.add(b1);
b2.dom.add(document.createElement('tr'));
// 4 marker nodes in table, no elements
test.equal(table.el.childNodes.length, 4);
test.equal($(table.el).find("*").length, 0);
// shazam, adding b2, which contains a TR,
// should move all the ranges into a TBODY.
b1.dom.add(b2);
test.equal(table.el.childNodes.length, 1);
test.equal(table.el.firstChild.nodeName, 'TBODY');
test.equal(table.el.firstChild.childNodes.length, 7);
test.equal(table.el.firstChild.childNodes[3].nodeName, 'TR');
test.equal(b2.dom.parentNode().nodeName, 'TBODY');
test.equal(b1.dom.parentNode().nodeName, 'TBODY');
test.equal(table.content.dom.parentNode().nodeName, 'TBODY');
// start over. now test two TR ranges.
$(table.el).empty();
test.equal(table.el.childNodes.length, 0);
var c1 = range({});
var c2 = range({});
DomRange.insert(c1, table.el);
DomRange.insert(c2, table.el);
test.equal(table.el.childNodes.length, 4);
test.equal($(table.el).find("*").length, 0);
c2.dom.add(document.createElement('tr'));
test.equal(table.el.childNodes.length, 3);
test.equal($(table.el).find("> *").length, 1);
test.equal($(table.el).find("> tbody").length, 1);
c1.dom.add(document.createElement('tr'));
// now there should be a single TBODY with two
// ranges in it containing TRs
test.equal(table.el.childNodes.length, 1);
test.equal(table.el.firstChild.nodeName, 'TBODY');
var tbody = table.el.firstChild;
test.equal(tbody.childNodes.length, 6);
test.equal($(tbody).find("> *").length, 2); // 2 elements
test.equal(tbody.childNodes[1].nodeName, 'TR');
test.equal(tbody.childNodes[4].nodeName, 'TR');
});
Tinytest.add("ui - DomRange - basic events", function (test) {
// test.equal doesn't work on arrays of DOM nodes, so
// we need this. It's `===` that descends recursively
// into any arrays.
var arrayEqual = function (a, b) {
test.equal(_.isArray(a), _.isArray(b));
if (_.isArray(a)) {
test.equal(a.length, b.length);
for (var i = 0; i < a.length; i++) {
arrayEqual(a[i], b[i]);
}
} else {
test.isTrue(a[i] === b[i]);
}
};
var q = new DomRange;
test.throws(function () {
// can't bind events before DomRange is added to
// the DOM
q.on('click', function (evt) {});
});
inDocument(
htmlRange("<span>Foo</span>"),
function (r) {
var buf = [];
r.on('click', 'span', function (evt) {
buf.push([evt.type, evt.target, evt.currentTarget]);
});
arrayEqual(buf, []);
var span = r.elements()[0];
span.click();
arrayEqual(buf, [['click', span, span]]);
});
inDocument(
htmlRange("<div><span>Foo</span></div>"),
function (r) {
var buf = [];
// test click with no selector; should only
// fire on the event target.
r.on('click', function (evt) {
buf.push([evt.type, evt.target, evt.currentTarget]);
});
arrayEqual(buf, []);
var span = r.$('span')[0];
span.click();
arrayEqual(buf, [['click', span, span]]);
});
inDocument(
htmlRange('<div id="yeah"><span>Foo</span></div>' +
'<div id="no">Bar</div>'),
function (r) {
var buf = [];
// test click on particular div, which is
// not the target or the bound element
r.on('click', '#yeah', function (evt) {
buf.push([evt.type, evt.target, evt.currentTarget]);
});
arrayEqual(buf, []);
r.$('#no')[0].click();
arrayEqual(buf, []);
var yeah = r.$('#yeah')[0];
yeah.click();
arrayEqual(buf, [['click', yeah, yeah]]);
});
inDocument(
new DomRange,
function (r) {
var s;
r.add(s = htmlRange('<div id="one"></div>'));
r.add(htmlRange('<div id="two"></div>'));
var one = r.$('#one')[0];
var two = r.$('#two')[0];
var buf = [];
// test that click must be in range to fire
// event handler
s.on('click', 'div', function (evt) {
buf.push([evt.type, evt.target, evt.currentTarget]);
});
arrayEqual(buf, []);
two.click();
arrayEqual(buf, []);
one.click();
arrayEqual(buf, [['click', one, one]]);
});
});
Tinytest.add("ui - DomRange - contains", function (test) {
inDocument(new DomRange, function (r) {
var s = htmlRange('<div id="one"><span>Foo</span></div>');
var t = new DomRange;
t.add(s);
r.add(t);
r.add(htmlRange('<div id="two"></div>'));
var one = r.$('#one')[0];
var two = r.$('#two')[0];
var span = r.$('span')[0];
test.isFalse(r.contains(r));
test.isTrue(r.contains(s));
test.isTrue(r.contains(t));
test.isTrue(r.contains(one));
test.isTrue(s.contains(one));
test.isTrue(t.contains(one));
test.isTrue(r.contains(two));
test.isFalse(s.contains(two));
test.isFalse(t.contains(two));
test.isTrue(r.contains(span));
test.isTrue(s.contains(span));
test.isTrue(t.contains(span));
test.isFalse(r.contains(r.parentNode()));
test.isFalse(r.contains(document.createElement("DIV")));
});
});
Tinytest.add("ui - DomRange - constructor", function (test) {
var r = new DomRange;
test.isTrue(r.dom === r);
test.isTrue(r.component === r);
var x = {};
var s = new DomRange(x);
test.isTrue(x.dom === s);
test.isTrue(s.component === x);
test.isTrue(s.parentNode());
test.isTrue(s.start.$ui === x);
test.isTrue(s.end.$ui === x);
var div = document.createElement('div');
s.add(div);
test.isTrue(div.$ui === x);
});
Tinytest.add("ui - DomRange - get", function (test) {
var r = new DomRange;
var a = document.createElement('div');
var b = document.createElement('div');
var c = document.createElement('div');
var d = document.createElement('div');
r.add(a);
r.add(null, b);
r.add('c', c);
test.throws(function () {
r.add(0, d);
});
test.throws(function () {
r.add(1, d);
});
test.throws(function () {
r.add('', d);
});
test.isTrue(r.get('toString') === null);
test.isTrue(r.get('__proto__') === null);
test.isTrue(r.get('_proto__') === null);
test.isTrue(r.get('blahblah') === null);
r.add('toString', d);
test.throws(function () {
r.get('');
});
test.throws(function () {
r.get(null);
});
test.throws(function () {
r.get(1);
});
test.equal(r.elements().length, 4);
test.isTrue(r.get('c') === c);
test.isTrue(r.get('toString') === d);
var x = {};
var s = new DomRange(x);
test.throws(function () {
r.add('s', s);
});
r.add('x', x);
test.isTrue(r.get('x') === x);
});
// This test targets IE 9 and 10, which allow properties
// to be attached to TextNodes but may lose them over time.
// Specifically, the JavaScript view of a TextNode seems to
// be only weakly retained by the TextNode itself, so if you
// hang an object graph off a TextNode, you need some other
// pointer to the TextNode or an object in the graph to
// retain it.
Tinytest.addAsync("ui - DomRange - IE TextNode GC", function (test, onComplete) {
var r = new DomRange;
var B = document.createElement('B');
B.id = 'ie_textnode_gc_test';
document.body.appendChild(B);
DomRange.insert(r, B);
r = null;
B = null;
// trigger GC...
if (typeof CollectGarbage === 'function')
CollectGarbage();
// come back later...
window.setTimeout(function () {
var B = document.getElementById("ie_textnode_gc_test");
test.isTrue(B.firstChild.$ui);
test.isTrue(B.lastChild.$ui);
window.BBB = B;
document.body.removeChild(B);
onComplete();
}, 500);
});
Tinytest.add("ui - DomRange - more TBODY", function (test) {
inDocument(htmlRange("<table></table>"), function (r) {
var table = r.elements()[0];
var tableContent = new DomRange;
var buf = [];
DomRange.insert(tableContent, table);
var trRange = htmlRange("<tr><td>Hello</td></tr>");
tableContent.add(trRange);
test.isTrue(tableContent.contains(trRange));
});
inDocument(htmlRange("<table></table>"), function (r) {
var table = r.elements()[0];
var tableContent = new DomRange;
var buf = [];
DomRange.insert(tableContent, table);
var trRange = htmlRange("<tr><td>Hello</td></tr>");
var tr = trRange.elements()[0];
tableContent.add('tr', tr);
test.equal(_.keys(tableContent.members).length, 1);
test.isTrue(tableContent.contains(tr));
tableContent.remove('tr');
// bizarrely, in IE 8, the `tr` still has some
// DocumentFragment as its parent even though `removeChild`
// has been called on it directly.
test.isFalse(tr.parentNode && tr.parentNode.nodeType === 1);
});
});
Tinytest.add("ui - DomRange - events in tables", function (test) {
inDocument(htmlRange("<table></table>"), function (r) {
var table = r.elements()[0];
var tableContent = new DomRange;
var buf = [];
DomRange.insert(tableContent, table);
tableContent.on('click', 'tr', function (evt) {
buf.push('click ' + evt.currentTarget.nodeName);
});
var trRange = htmlRange("<tr><td>Hello</td></tr>");
tableContent.add(trRange);
var tr = trRange.elements()[0];
test.equal(buf, []);
tr.click();
test.equal(buf, ['click TR']);
// XXX test something that would break if the event data
// is on the TABLE rather than the TBODY (the new
// parentNode of `tableContent`).
});
});
Tinytest.add("ui - DomRange - nested event order", function (test) {
inDocument(new DomRange, function (r) {
var a = new DomRange;
var b = new DomRange;
var c = new DomRange;
var d = new DomRange;
r.add(a);
a.add(b);
b.add(c);
c.add(d);
var div = document.createElement("DIV");
d.add(div);
var buf = [];
var appender = function (str) {
return function (evt) {
buf.push(str);
};
};
b.on('click', 'div', appender("B"));
a.on('click', 'div', appender("A"));
d.on('click', appender("D"));
c.on('click', 'div', appender("C"));
test.equal(buf, []);
div.click();
test.equal(buf, ['D', 'C', 'B', 'A']);
buf.length = 0;
b.on('click', appender("B2"));
d.on('click', 'div', appender("D2"));
div.click();
test.equal(buf, ['D', 'D2', 'C', 'B', 'B2', 'A']);
});
});
Tinytest.add("ui - DomRange - isParented", function (test) {
inDocument(new DomRange, function (r) {
test.equal(r.isParented, true);
var a = new DomRange;
var b = new DomRange;
var c = new DomRange;
var d = new DomRange;
var e = new DomRange;
var abcde = function (ap, bp, cp, dp, ep) {
test.equal(!! a.isParented, !! ap);
test.equal(!! b.isParented, !! bp);
test.equal(!! c.isParented, !! cp);
test.equal(!! d.isParented, !! dp);
test.equal(!! e.isParented, !! ep);
};
var div = document.createElement("DIV");
c.add(div);
abcde(0, 0, 0, 0, 0);
d.add(e);
abcde(0, 0, 0, 0, 0);
DomRange.insert(d, div);
abcde(0, 0, 0, 1, 1);
a.add(b);
abcde(0, 0, 0, 1, 1);
r.add(a);
abcde(1, 1, 0, 1, 1);
b.add(c);
abcde(1, 1, 1, 1, 1);
var container = r.parentNode();
test.equal(_.keys(container.$_uiranges).length, 1);
test.equal(_.keys(div.$_uiranges).length, 1);
d.remove();
test.equal(_.keys(div.$_uiranges).length, 0);
r.remove();
test.equal(_.keys(container.$_uiranges).length, 0);
});
});
Tinytest.add("ui - DomRange - structural removal", function (test) {
inDocument(new DomRange, function (r) {
var a = new DomRange;
test.isFalse(a.isRemoved);
r.add('a', a);
test.isFalse(a.isRemoved);
r.remove('a');
test.isTrue(a.isRemoved);
var b = new DomRange;
test.isFalse(b.isRemoved);
r.add(b);
test.isFalse(b.isRemoved);
r.removeAll();
test.isTrue(b.isRemoved);
var c = new DomRange;
var d = new DomRange;
var e = new DomRange;
c.add(d);
d.add(e);
r.add('c', c);
test.isFalse(c.isRemoved);
test.isFalse(d.isRemoved);
test.isFalse(e.isRemoved);
r.remove('c');
test.isTrue(c.isRemoved);
test.isTrue(d.isRemoved);
test.isTrue(e.isRemoved);
for (var scenario = 0; scenario < 2; scenario++) {
var f = new DomRange;
var g = document.createElement("DIV");
var h = new DomRange;
var i = document.createElement("DIV");
var j = document.createElement("DIV");
var k = new DomRange;
r.add('f', f);
f.add(g);
DomRange.insert(h, g);
h.add(i);
DomRange.insert(k, j);
i.appendChild(j);
test.isFalse(f.isRemoved);
test.isFalse(h.isRemoved);
test.isFalse(k.isRemoved);
if (scenario === 0)
r.removeAll();
else if (scenario === 1)
r.remove('f');
test.isTrue(f.isRemoved);
test.isTrue(h.isRemoved);
test.isTrue(k.isRemoved);
r.removeAll();
}
});
});
Tinytest.add("ui - DomRange - noticed removal", function (test) {
// TODO
//
// e.g. noticed via `eachMember` or `add`
});
Tinytest.add("ui - DomRange - jQuery removal", function (test) {
inDocument(htmlRange("<div></div>"), function (r) {
for (var scenario = 0; scenario < 3; scenario++) {
var f = document.createElement("DIV");
var g = document.createElement("DIV");
var h = new DomRange;
var i = document.createElement("DIV");
var j = document.createElement("DIV");
var k = new DomRange;
r.add(f);
f.appendChild(g);
DomRange.insert(h, g);
h.add(i);
DomRange.insert(k, j);
i.appendChild(j);
test.isFalse(h.isRemoved);
test.isFalse(k.isRemoved);
$(g).removeData();
test.isFalse(h.isRemoved);
test.isFalse(k.isRemoved);
if (scenario === 0)
$(g).remove();
else if (scenario === 1)
$(f).empty();
else if (scenario === 2)
$(f).html("<br>");
else if (scenario === 3)
$(g).detach();
if (scenario !== 3) {
test.isTrue(h.isRemoved);
test.isTrue(k.isRemoved);
} else {
// `detach` doesn't remove
test.isFalse(h.isRemoved);
test.isFalse(k.isRemoved);
}
r.removeAll();
}
});
});
// TO TEST STILL:
// - external remove element
// - double-add, double-remove
// - external entire remove
// - element adoption during move/remove/refresh
// - first arg of add must be string, errors on `0` for example.
// same with remove and move `id` arguments.
// - can't add multiple members with id, but can add array of 1.
// can add 0 with no id.
// - add a node or range with the same id as an old member
// works if that member is gone.
// - events (and other stuff) get moved when wrapping in TBODY
// - event unbinding
// - "noticed" removal due to `eachMembers`, `add`, etc.