describe("Ext.ComponentQuery", function() { var cq, cm, EA, result, root, child1, child2, child3, child4, child5, child6, child7, child8, child9, child10, child11, child12, setup = function(o, parent) { if (o.items) { for (var i = 0; i < o.items.length; i++) { setup(o.items[i], o); } } Ext.apply(o, { getItemId: function() { return this.itemId !== undefined ? this.itemId : this.id; }, getId: function() { return this.id; }, getRefItems: function(deep) { var items = this.items || [], len = items.length, i = 0, item; if (deep) { for (; i < len; i++) { item = items[i]; if (item.getRefItems) { items = items.concat(item.getRefItems(true)); } } } return items; }, getRefOwner: function() { return this.ownerCt; }, hasCls: function(cls) { return this.cls == cls; }, isHidden: function() { return this.hidden; }, isXType: function(type) { return EA.contains(this.type.split('/'), type); }, ownerCt: parent, self: { $config: { configs: {} } } }); cm.register(o); }; function expectChildren() { var args = Array.prototype.slice.apply(arguments), result = args.shift(), len = args.length, i, expected, actual; expect(result.length).toBe(len); for (i = 0, len = args.length; i < len; i++) { expected = args[i]; actual = result[i]; expect(actual.id).toBe(expected.id); } }; beforeEach(function() { cq = Ext.ComponentQuery; cm = Ext.ComponentManager; EA = Ext.Array; root = { id: 'root', cls: 'root-cls', type: 'A', items: [child1 = { $className: 'Foo', id: 'child1', cls: 'child1-cls', type: 'B/G/Z', foo: 'bar,baz' }, child2 = { $className: 'Bar.Baz.Qux', id: 'child2', cls: 'child2-cls', type: 'B/G/Z', bar: 'foo,bar,baz' }, child3 = { $className: 'Foo', id: 'child3', cls: 'child3-cls', type: 'B/C/D', layout: 'card', items: [child4 = { id: 'child4', cls: 'child4-cls', type: 'B/C/E', layout: 'hbox', items: [child5 = { id: 'child5', cls: 'child5-cls', type: 'B/C/F', items: [child7 = { id: 'child7', cls: 'child7-cls', type: 'B/G/H' }, child8 = { id: 'child8', cls: 'child8-cls', type: 'B/G/I' }, child9 = { id: 'child9', cls: 'child9-cls', type: 'B/G/J' }] }, child6 = { id: 'child6', cls: 'child6-cls', type: 'B/G/Z', hidden: true }, child10 = { id : 'child10', cls : 'child10-cls my-foo-cls', type : 'B' }, child11 = { id : 'child.11', cls : 'child11-cls my-foo-cls-test', type : 'B' }, child12 = { id: 'child.12', itemId: 'bobby.brown.goes.down', type: 'E.2-E.4', foo: '[foo(bar)!baz#qux\\fred*]', sq: "'single' 'quotes'", dq: '"double" "quotes"' }] }] }] }; setup(root); }); afterEach(function() { cm.all = {}; }); describe("parser", function() { it("should blow up if the intra-selector comma is escaped", function() { expect(function() { cq.query('#child3\\,F', root); }).toThrow('Invalid ComponentQuery selector: ",F"'); }); it("should blow up if a selector ends with unescaped comma", function() { expect(function() { cq.query('#child3,', root); }).toThrow('Invalid ComponentQuery selector: ""'); }); it("should blow up if a selector ends with an escaped comma", function() { expect(function() { cq.query('#child3\\,', root); }).toThrow('Invalid ComponentQuery selector: ","'); }); describe("missing quotes", function() { var warning; beforeEach(function() { spyOn(Ext.log, 'warn').andCallFake(function(msg) { warning = msg; }); warning = null; }); it("should warn on missing opening double quote", function() { cq.query('[foo=bar"]'); expect(warning).toMatch(/^ComponentQuery selector.*?unescaped \("\).*?end/); }); it("should warn on missing closing double quote", function() { cq.query('[foo="bar]'); expect(warning).toMatch(/^ComponentQuery selector.*?unescaped \("\).*?beginning/); }); it("should warn on missing opening single quote", function() { cq.query("[foo=bar']"); expect(warning).toMatch(/^ComponentQuery selector.*?unescaped \('\).*end/); }); it("should warn on missing closing single quote", function() { cq.query("[foo='bar]"); expect(warning).toMatch(/^ComponentQuery selector.*?unescaped \('\).*beginning/); }); }); }); describe("Query object", function() { describe("is", function() { it("should return true if the selector is empty", function() { var q = cq.parse(''); expect(q.is(root)).toBe(true); }); }); }); describe("is", function(){ var item; beforeEach(function() { item = Ext.getCmp('root'); }); afterEach(function(){ item = null; }); it("should return true if there is no selector", function(){ expect(cq.is(root)).toBe(true); }); it("should return true if component matches the selector", function(){ expect(cq.is(root, '[type=A]')).toBe(true); }); it("should return true if component matches any selector", function(){ expect(cq.is(root, 'button, #foo, #root, [type=A]')).toBe(true); }); it("should return false if the component doesn't match the selector", function(){ expect(cq.is(root, '#foo')).toBe(false); }); it("should work with the :not pseudo", function() { var comp = new Ext.Component({ foo: 1 }); expect(comp.is('[foo]:not([bar])')).toBe(true); }); it("should be able to run on destroyed components", function(){ var comp = new Ext.Component({ foo: 1 }); comp.destroy(); expect(comp.is('[foo]:not([bar])')).toBe(true); }); describe("hierarchy selectors", function() { it("should match a direct child", function(){ expect(cq.is(child6, '#child4 > #child6')).toBe(true); }); it("should return false if it's not a direct child", function() { expect(cq.is(child6, '#child3 > #child6')).toBe(false); }); it("should match deep children", function() { expect(cq.is(child6, '#child3 #child6')).toBe(true); }); it("should match an upward selector", function() { expect(cq.is(child3, '#child6 ^ #child3')).toBe(true); }); }); }); describe("simple query by xtype", function() { it("should select all six items of type G", function() { result = cq.query('G', root); expect(result.length).toEqual(6); expect(result[2].id).toEqual(child6.id); }); it("should allow escaped dots in xtype selector", function() { result = cq.query('E\\.2-E\\.4', root); expect(result.length).toBe(1); expect(result[0].id).toBe(child12.id); }); }); describe("simple query by xtype prefixed with dot", function() { beforeEach(function() { // Silence console warnings spyOn(Ext.log, 'warn'); }); it("should select all six items of type G", function() { result = cq.query('.G', root); expect(result.length).toEqual(6); expect(result[2].id).toEqual(child6.id); }); it("should allow escaped dots in xtype selector", function() { result = cq.query('.E\\.2-E\\.4', root); expect(result.length).toBe(1); expect(result[0].id).toBe(child12.id); }); }); describe("attributes starting with $", function(){ it("should match $className variable", function(){ result = cq.query('[$className=Foo]'); expect(result.length).toBe(2); expect(result[0].id).toBe('child1'); expect(result[1].id).toBe('child3'); }); it("should allow dots in attribute values", function() { result = cq.query('[$className=Bar.Baz.Qux]', root); expectChildren(result, child2); }); }); describe("query by id", function() { it("should select the second child", function() { result = cq.query('G#child2', root); expect(result.length).toEqual(1); expect(result[0].id).toEqual(child2.id); }); it("should select the fifth child", function() { result = cq.query('#child5', root); expect(result.length).toEqual(1); expect(result[0].id).toEqual(child5.id); }); it("should allow escaped dots in query-by-id selectors", function() { result = cq.query('#child\\.11', root); expectChildren(result, child11); }); it("should allow multiple escaped commas in #itemId selectors", function() { result = cq.query('#bobby\\.brown\\.goes\\.down', root); expectChildren(result, child12); }); }); describe("query by property", function() { it("should select the second child", function() { result = cq.query('G[cls=child2-cls]', root); expect(result.length).toEqual(1); expect(result[0].id).toEqual(child2.id); }); it("should select the sixth child", function () { result = cq.query('[hidden]', root); expect(result.length).toEqual(1); expect(result[0].id).toEqual(child6.id); }); describe("property value quotes", function() { it("should allow single quoted value", function() { result = cq.query("[id='child.12']", root); expectChildren(result, child12); }); it("should allow double quoted value", function() { result = cq.query('[id="child.12"]', root); expectChildren(result, child12); }); it("should allow double quotes in single quoted value", function() { result = cq.query('[dq=\'"double" "quotes"\']', root); expectChildren(result, child12); }); it("should allow single quotes in double quoted value", function() { result = cq.query("[sq=\"'single' 'quotes'\"]", root); expectChildren(result, child12); }); }); describe("matchers", function(){ it("should select the tenth child", function () { result = cq.query('[cls~=my-foo-cls]', root); expect(result.length).toEqual(1); expect(result[0].id).toEqual(child10.id); }); it("should select items where id starts with child1", function(){ result = cq.query('[id^=child1]', root); expectChildren(result, child1, child10); }); it("should select items where cls ends with 9-cls", function(){ result = cq.query('[cls$=9-cls]', root); expect(result.length).toBe(1); expect(result[0].cls).toBe('child9-cls'); }); it("should select items with commas in properties", function() { result = cq.query('[foo=bar\\,baz]'); expect(result.length).toEqual(1); expect(result[0].id).toEqual(child1.id); }); it("should allow multiple escaped commas", function() { result = cq.query('[bar=foo\\,bar\\,baz]', root); expectChildren(result, child2); }); it("should allow escaped metacharacters", function() { result = cq.query('[foo=\\[foo\\(bar\\)\\!baz\\#qux\\\\fred\\*\\]]', root); expectChildren(result, child12); }); describe("regexen", function() { it("should match everything with an empty regex", function() { result = cq.query('[cls/=]'); expect(result.length).toBe(12); }); describe("simple regexen", function() { it("should match regexen with text as pattern", function() { result = cq.query('[cls/=my-foo]'); expectChildren(result, child10, child11); }); it("should match regexen with simple alternation", function() { result = cq.query('[cls/=child3-cls|child4-cls|child5-cls]'); expectChildren(result, child5, child4, child3); }); }); describe("complex regexen", function() { it("should match regexen with pattern quantifiers", function() { result = cq.query('[cls/="child.{2}-cls"]'); expectChildren(result, child10, child11); }); it("should match regexen with grouping and alternating", function() { result = cq.query('[cls/="child(?:7|8)-cls"]'); expectChildren(result, child7, child8); }); it("should match regexen with character classes", function() { result = cq.query('[cls/="child\\[5-7\\]-cls"]'); expectChildren(result, child7, child5, child6); }); }); }); }); }); describe("query using mode ^", function() { it("should select the fourth child", function() { result = cq.query('G[cls=child8-cls]^#child4', root); expect(result.length).toEqual(1); expect(result[0].id).toEqual(child4.id); }); }); describe("query using mode ^ and >", function() { it("should select the sixth child", function() { result = cq.query('G[cls=child8-cls]^#child4>G', root); expect(result.length).toEqual(1); expect(result[0].id).toEqual(child6.id); }); }); describe("query using multiple selectors", function() { it("should select the third and fifth child", function() { result = cq.query('#child3,F', root); expect(result.length).toEqual(2); expect(result[0].id).toEqual(child3.id); expect(result[1].id).toEqual(child5.id); }); }); describe("query using member function", function() { it("should select the sixth child that is hidden", function() { result = cq.query('{isHidden()}', root); expect(result.length).toEqual(1); expect(result[0].id).toEqual(child6.id); }); }); describe("query using pseudo-class", function() { beforeEach(function() { cq.pseudos.cardLayout = function(items) { var result = [], c, i = 0, l = items.length; for (; i < l; i++) { if ((c = items[i]).layout === 'card') { result.push(c); } } return result; }; }); it("should select the third child with layout == 'card'", function() { result = cq.query('C:cardLayout', root); delete cq.pseudos.cardLayout; expect(result.length).toEqual(1); expect(result[0].id).toEqual(child3.id); }); it("should not select the sixth child which is filtered by :not()", function(){ result = cq.query(':not([hidden])', root); var all = root.getRefItems(true), getId = function(o){ return o.id; }, allIds = EA.map(all, getId), resultIds = EA.map(result, getId), diffIds = EA.difference(allIds, resultIds); expect(result.length).toEqual(all.length - 1); expect(diffIds.length).toEqual(1); expect(diffIds[0]).toEqual(child6.id); }); it("should accept back-to-back pseudo-class selectors with cumulative results", function(){ result = cq.query(':not(G):not(F)', root); expect(result.length).toEqual(5); expect(result[0].id).toEqual(child3.id); expect(result[1].id).toEqual(child4.id); expect(result[2].id).toEqual(child10.id); expect(result[3].id).toEqual(child11.id); expect(result[4].id).toEqual(child12.id); }); it("should accept member expression selectors", function() { result = cq.query(':not({isHidden()})', root); var all = root.getRefItems(true), getId = function(o){ return o.id; }, allIds = EA.map(all, getId), resultIds = EA.map(result, getId), diffIds = EA.difference(allIds, resultIds); expect(result.length).toEqual(all.length - 1); expect(diffIds.length).toEqual(1); expect(diffIds[0]).toEqual(child6.id); }); describe("focusable", function() { // https://sencha.jira.com/browse/EXTJS-16758 it("should not blow up when card item is not a component", function() { var container = new Ext.container.Container({ renderTo: Ext.getBody(), items: [new Ext.Widget()] }); expect(function() { container.query(':focusable'); }).not.toThrow(); container.destroy(); }); }); describe("first/last", function() { var items; beforeEach(function(){ items = [ new Ext.Component({ action: 'type1', id: 'id1' }), new Ext.container.Container({ action: 'type1', id: 'id2' }), new Ext.container.Container({ action: 'type2', id: 'id3' }), new Ext.Component({ action: 'type2', id: 'id4' }), new Ext.container.Container({ action: 'type2', id: 'id5' }) ]; }); afterEach(function(){ Ext.Array.forEach(items, function(item){ item.destroy(); }); items = null; }); describe("first", function(){ it("should return an empty array if no items match", function(){ var result = cq.query('button:first', items); expect(result).toEqual([]); }); it("should return the first matching component by type", function(){ var result = cq.query('container:first', items); expect(result).toEqual([items[1]]); }); it("should return the last matching component by attribute", function(){ var result = cq.query('[action=type2]:first', items); expect(result).toEqual([items[2]]); }); it("should return the first component", function(){ var result = cq.query('*:first', items); expect(result).toEqual([items[0]]); }); describe("no items/single item", function(){ it("should return an empty array if there are no items", function(){ var result = cq.query('*:first', []); expect(result).toEqual([]); }); it("should return an a single item if it matches", function(){ var c = new Ext.Component(); var result = cq.query('component:first', [c]); expect(result).toEqual([c]); c.destroy(); }); }); }); describe("last", function(){ it("should return an empty array if no items match", function(){ var result = cq.query('button:last', items); expect(result).toEqual([]); }); it("should return the last matching component by type", function(){ var result = cq.query('component:last', items); expect(result).toEqual([items[4]]); }); it("should return the first matching component by attribute", function(){ var result = cq.query('[action=type1]:last', items); expect(result).toEqual([items[1]]); }); it("should return the first component", function(){ var result = cq.query('*:last', items); expect(result).toEqual([items[4]]); }); describe("no items/single item", function(){ it("should return an empty array if there are no items", function(){ var result = cq.query('*:last', []); expect(result).toEqual([]); }); it("should return an a single item if it matches", function(){ var c = new Ext.Component(); var result = cq.query('component:last', [c]); expect(result).toEqual([c]); c.destroy(); }); }); }); }); }); describe('attribute value coercion', function() { var candidates = [{ att1: 0, att2: 0, att3: 0, att4: 0 }, { att1: null, att2: false, att3: true, att4: undefined }, { att1: 0, att2: 0, att3: 0, att4: 0 }]; if('should coerce "null" to match a null property value', function() { expect(cq.query('[att1=null]', candidates)).toBe(candidates[1]); }); if('should coerce "false" to match a Boolean property value', function() { expect(cq.query('[att2=false]', candidates)).toBe(candidates[1]); }); if('should coerce "true" to match a Boolean property value', function() { expect(cq.query('[att3=true]', candidates)).toBe(candidates[1]); }); if('should coerce "undefined" to match an undefined property value', function() { expect(cq.query('[att4=undefined]', candidates)).toBe(candidates[1]); }); }); describe('ownProperty tests', function() { var TestClass = Ext.define(null, { extend: 'Ext.Component', foo: 'bar', bletch: 0 }), candidates; // Only candidates[1] has *ownProperties* foo and bletch // And the value of bletch is zero, so by [bletch] will never match. // Test that [?bletch] tests for just *presence* of property in object. candidates = [new TestClass(), new TestClass({ foo: 'bar', bletch: 0 })]; it('should only match candidates [@foo=bar] with ownProperty "foo" equal to "bar"', function() { expect(Ext.ComponentQuery.query('[@foo=bar]', candidates).length).toBe(1); expect(Ext.ComponentQuery.query('[@foo=bar]', candidates)[0]).toBe(candidates[1]) expect(Ext.ComponentQuery.is(candidates[0], '[@foo=bar]')).toBe(false); expect(Ext.ComponentQuery.is(candidates[1], '[@foo=bar]')).toBe(true); }); it('should not match candidates [bletch] where bletch is a falsy property', function() { expect(Ext.ComponentQuery.query('[bletch]', candidates).length).toBe(0); expect(Ext.ComponentQuery.is(candidates[0], '[bletch]')).toBe(false); expect(Ext.ComponentQuery.is(candidates[1], '[bletch]')).toBe(false); }); it('should match candidates [?bletch] where bletch is a falsy property', function() { expect(Ext.ComponentQuery.query('[?bletch]', candidates).length).toBe(1); expect(Ext.ComponentQuery.query('[?bletch]', candidates)[0]).toBe(candidates[1]); expect(Ext.ComponentQuery.is(candidates[0], '[?bletch]')).toBe(false); expect(Ext.ComponentQuery.is(candidates[1], '[?bletch]')).toBe(true); }); }); describe('Querying floating descendants', function() { var c; beforeEach(function() { c = new Ext.container.Container({ items: { xtype: 'container', floating: true, id: 'floating-cq-child', items: { xtype: 'container', floating: true, id: 'floating-cq-grandchild', items: { floating: true, id: 'floating-cq-great-grandchild' } } }, renderTo: document.body }); }); afterEach(function() { c.destroy(); }); it('should find all descendants', function() { var d = c.query(); expect(d.length).toEqual(3); expect(d[0]).toBe(Ext.getCmp('floating-cq-child')); expect(d[1]).toBe(Ext.getCmp('floating-cq-grandchild')); expect(d[2]).toBe(Ext.getCmp('floating-cq-great-grandchild')); }); it('should find individual descendants', function() { var d = c.query('>*'); expect(d.length).toEqual(1); expect(d[0]).toBe(Ext.getCmp('floating-cq-child')); d = c.query('>>*'); expect(d.length).toEqual(1); expect(d[0]).toBe(Ext.getCmp('floating-cq-grandchild')); d = c.query('>>>*'); expect(d.length).toEqual(1); expect(d[0]).toBe(Ext.getCmp('floating-cq-great-grandchild')); }); }); describe('trimming spaces', function () { var c; beforeEach(function () { c = new Ext.container.Container({ items: { xtype: 'button', text: 'Test', action: 'selectVendors' }, renderTo: document.body }); }); afterEach(function () { c.destroy(); c = null; }); describe('single space', function () { describe("attribute matching expressions", function() { it('should trim leading space', function () { result = cq.query('[action =selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); result = cq.query('[action ^=selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); result = cq.query('[action $=selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); }); it('should trim trailing space', function () { result = cq.query('[action= selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); result = cq.query('[action*= selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); result = cq.query('[action~= selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); }); it('should trim both spaces', function () { result = cq.query('[action = selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); result = cq.query('[action *= selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); result = cq.query('[action ~= selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); }); }); }); describe('multiple spaces', function () { describe("attribute matching expressions", function() { it('should trim multiple leading spaces', function () { result = cq.query('[action =selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); result = cq.query('[action ^=selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); result = cq.query('[action $=selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); }); it('should trim multiple trailing spaces', function () { result = cq.query('[action= selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); result = cq.query('[action*= selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); result = cq.query('[action~= selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); }); it('should trim multiple spaces on both sides', function () { result = cq.query('[action = selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); result = cq.query('[action *= selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); result = cq.query('[action ~= selectVendors]', c); expect(result.length).toBe(1); expect(result[0].action).toBe('selectVendors'); }); }); describe("id matching expressions", function() { it("should trim leading spaces", function() { result = cq.query(' #child9', root); expect(result.length).toBe(1); expect(result[0].id).toBe(child9.id); }); it("should trim trailing spaces", function() { result = cq.query('#child9 ', root); expect(result.length).toBe(1); expect(result[0].id).toBe(child9.id); }); it("should trim spaces on both sides", function() { result = cq.query(' #child9 ', root); expect(result.length).toBe(1); expect(result[0].id).toBe(child9.id); }); }); describe("descendancy expressions", function() { it("should trim leading spaces", function() { result = cq.query(' [layout=card] [type=B/G/H]', root); expect(result.length).toBe(1); expect(result[0].id).toBe(child7.id); }); it("should trim trailing spaces", function() { result = cq.query('[type=B/G/J] ^ [layout=hbox] ', root); expect(result.length).toBe(1); expect(result[0].id).toBe(child4.id); }); it("should trim spaces on both sides", function() { result = cq.query(' #child4 > [type=B/C/F] ', root); expect(result.length).toBe(1); expect(result[0].id).toBe(child5.id); }); }); }); }); describe('pre- and postOrder', function () { var foo = false; afterEach(function () { foo = false; cq.cache.clear(); }); describe('preOrder', function () { it('should call the fn regardless of whether the selector has been cached', function () { expect(cq.cache.get('')).toBeUndefined(); cq.visitPreOrder('', this, function () { foo = true; }); expect(foo).toBe(true); }); }); describe('postOrder', function () { it('should call the fn regardless of whether the selector has been cached', function () { expect(cq.cache.get('')).toBeUndefined(); cq.visitPostOrder('', this, function () { foo = true; }); expect(foo).toBe(true); }); }); }); describe("selecting by attribute", function(){ var foo, bar; beforeEach(function(){ Ext.define('spec.Foo', { extend: 'Ext.Component', config: { bar: 1 }, baz: 2 }); foo = new spec.Foo({ jaz:3 }); Ext.define('spec.Bar', { extend: 'Ext.Component', config: { bar: 4 }, baz: 5 }); bar = new spec.Bar({ jaz: 6 }); }); afterEach(function() { Ext.undefine('spec.Foo'); Ext.undefine('spec.Bar'); }); it("should match instance config", function(){ result = cq.query('[bar=1]'); expect(result.length).toBe(1); expect(result[0]).toBe(foo); }); it("should match a property on the instance", function() { result = cq.query('[baz=2]'); expect(result.length).toBe(1); expect(result[0]).toBe(foo); }); it("should match an arbitrary property on the config object", function() { result = cq.query('[jaz=3]'); expect(result.length).toBe(1); expect(result[0]).toBe(foo); }); }); });