UNPKG

extjs-gpl

Version:

GPL licensed version of Sencha Ext JS

1,231 lines (1,099 loc) 238 kB
/* global expect, jasmine, Ext, MockAjaxManager, spyOn */ describe("Ext.app.ViewModel", function() { var viewModel, scheduler, session, spy; function bindDeepNotify (key, fn, scope) { var bind = viewModel.bind(key, fn || spy, scope); bind.deep = true; viewModel.notify(); return bind; } function bindNotify (key, fn, scope) { var bind = viewModel.bind(key, fn || spy, scope); viewModel.notify(); return bind; } function setNotify (key, value) { viewModel.set(key, value); viewModel.notify(); } function notify() { viewModel.notify(); } function reset () { for (var i = 0, len = arguments.length; i < len; ++i) { arguments[i].reset(); } } function expectArgs (newVal, oldVal) { if (arguments.length === 1) { expectArgsForCall(spy.mostRecentCall, newVal); } else { expectArgsForCall(spy.mostRecentCall, newVal, oldVal); } } function expectArgsForCall(theCall, newVal, oldVal) { var args = theCall.args; expect(args[0]).toBe(newVal); if (arguments.length > 2) { expect(args[1]).toBe(oldVal); } } function makeRecord(Type, id, data) { data = Ext.apply({ id: id }, data); return new Type(data, session); } function makeSession() { session = new Ext.data.Session({ scheduler: { // Make a huge tickDelay, we'll control it by forcing ticks tickDelay: 9999 } }); } function createViewModel(withSession, cfg) { if (withSession && !session) { makeSession(); } viewModel = new Ext.app.ViewModel(Ext.apply({ id: 'rootVM', session: session }, cfg)); scheduler = viewModel.getScheduler(); } function complete(data) { Ext.Ajax.mockComplete({ status: 200, responseText: Ext.encode(data) }); } function completeNotify(data) { complete(data); notify(); } beforeEach(function() { Ext.data.Store.prototype.config.asynchronousLoad = false; Ext.data.Model.schema.setNamespace('spec'); MockAjaxManager.addMethods(); spy = jasmine.createSpy(); }); afterEach(function() { Ext.data.Store.prototype.config.asynchronousLoad = undefined; Ext.destroy(viewModel); Ext.destroy(session); session = scheduler = spy = viewModel = null; MockAjaxManager.removeMethods(); Ext.data.Model.schema.clear(true); }); describe("isReadOnly", function() { describe("always readOnly bindings", function() { it("should be true for template bindings", function() { createViewModel(); var b = viewModel.bind('Hello {foo}', Ext.emptyFn); expect(b.isReadOnly()).toBe(true); }); it("should be true for multi bindings", function() { createViewModel(); var b = viewModel.bind({ a: '{foo}', b: '{bar}' }, Ext.emptyFn); expect(b.isReadOnly()).toBe(true); }); }); describe("normal bindings", function() { it("should not be readOnly by default", function() { createViewModel(); var b = viewModel.bind('{foo}', Ext.emptyFn); expect(b.isReadOnly()).toBe(false); }); it("should not be readOnly when options are passed", function() { createViewModel(); var b = viewModel.bind('{foo}', Ext.emptyFn, null, { single: true }); expect(b.isReadOnly()).toBe(false); }); it("should be readOnly when twoWay is set to false", function() { createViewModel(); var b = viewModel.bind('{foo}', Ext.emptyFn, null, { twoWay: false }); expect(b.isReadOnly()).toBe(true); }); }); describe("formulas", function() { it("should be readOnly if there is no set", function() { createViewModel(false, { formulas: { foo: function() { return 1; } } }); var b = viewModel.bind('{foo}', Ext.emptyFn); expect(b.isReadOnly()).toBe(true); }); it("should be readOnly if there is a set but is marked as twoWay: false", function() { createViewModel(false, { formulas: { foo: { get: function() { return 1; }, set: function() { this.set('x', 1); } } } }); var b = viewModel.bind('{foo}', Ext.emptyFn, null, { twoWay: false }); expect(b.isReadOnly()).toBe(true); }); it("should not be readOnly if there is a set", function() { createViewModel(false, { formulas: { foo: { get: function() { return 1; }, set: function() { this.set('x', 1); } } } }); var b = viewModel.bind('{foo}', Ext.emptyFn); expect(b.isReadOnly()).toBe(false); }); }); }); describe("getting/setting values", function() { beforeEach(function() { createViewModel(); }); describe("set", function() { it("should set a root value if the param is an object", function() { viewModel.set({ foo: { bar: 1 }, baz: 2 }); expect(viewModel.getData().foo.bar).toBe(1); expect(viewModel.getData().baz).toBe(2); }); it("should set an object at a path", function() { viewModel.set('foo.bar', { baz: 1 }); expect(viewModel.getData().foo.bar.baz).toBe(1); }); it("should set a path + primitive", function() { viewModel.set('foo.bar', 100); expect(viewModel.getData().foo.bar).toBe(100); }); it("should be able to set object instances and not descend into them", function() { var Cls = Ext.define(null, { foo: 1 }); var o = new Cls(); viewModel.set('obj', o); expect(viewModel.getData().obj).toBe(o); }); it('should be able to set a value to undefined even after an unbound stub has been purged', function() { var child = new Ext.app.ViewModel({ parent: viewModel, data: { frob: 2 } }); // Will delete stubs that have no bindings. viewModel.doCollect(); expect(function() { child.set('frob'); }).not.toThrow(); }); }); describe("get", function() { it("should be able to retrieve a value at the root", function() { viewModel.set('foo', 1); expect(viewModel.get('foo')).toBe(1); }); it("should descend into a path", function() { viewModel.set({ foo: { bar: { baz: 100 } } }); expect(viewModel.get('foo.bar.baz')).toBe(100); }); it("should return null if the value has not presented", function() { expect(viewModel.get('something')).toBeNull(); }); }); }); describe("bind/set for non records/stores", function() { beforeEach(function() { createViewModel(); }); function createSuite(bindFirst) { function run(bindFn, setFn) { if (bindFirst) { bindFn(); setFn(); } else { setFn(); bindFn(); } } describe(bindFirst ? "bind before set" : "set before bind", function() { describe("setting simple value types", function() { it("should set a number", function() { run(function() { bindNotify('{age}', spy); }, function() { setNotify('age', 3); }); expectArgs(3, undefined); }); it("should set a string", function() { run(function() { bindNotify('{name}', spy); }, function() { setNotify('name', 'Kenneth'); }); expectArgs('Kenneth', undefined); }); it("should set a bool", function() { run(function() { bindNotify('{active}', spy); }, function() { setNotify('active', true); }); expectArgs(true, undefined); }); it("should set an array", function() { var arr = [18, 22, 13]; run(function() { bindNotify('{scores}', spy); }, function() { setNotify('scores', arr); }); expectArgs(arr, undefined); }); it("should set a date", function() { var d = new Date(1980, 0, 1); run(function() { bindNotify('{dob}', spy); }, function() { setNotify('dob', d); }); expectArgs(d, undefined); }); it("should set an object instance", function() { var map = new Ext.util.HashMap(); run(function() { bindNotify('{myMap}', spy); }, function() { setNotify('myMap', map); }); expectArgs(map, undefined); }); }); describe("using bind options", function() { it("should set a value using bindTo", function() { run(function() { bindNotify({ bindTo: '{age}' }, spy); }, function() { setNotify('age', 3); setNotify('age', 5); }); if (bindFirst) { expectArgsForCall(spy.calls[0], 3, undefined); expectArgsForCall(spy.calls[1], 5, 3); } else { expectArgs(5, undefined); } }); it("should set the value once when using single: true", function() { run(function() { bindNotify({ bindTo: '{age}', single: true }, spy); }, function() { setNotify('age', 3); setNotify('age', 5); }); expect(spy.callCount).toBe(1); if (bindFirst) { expectArgs(3, undefined); } else { expectArgs(5, undefined); } }); it("should bind deep", function() { run(function() { bindNotify({ bindTo: '{foo}', deep: true }, spy); }, function() { setNotify({ foo: { bar: 1 } }); setNotify('foo.bar', 2); }); if (bindFirst) { expect(spy.callCount).toBe(2); } else { expect(spy.mostRecentCall.args[0]).toEqual({ bar: 2 }); } }); }); describe("setting objects", function() { it("should set to the root if there's no name", function() { run(function() { bindNotify('{name}', spy); }, function() { setNotify('', { name: 'Bar' }); }); expectArgs('Bar', undefined); }); it("should be able to set simple nested properties", function() { run(function() { bindNotify('{user.name}', spy); }, function() { setNotify('user', { name: 'Foo' }); }); expectArgs('Foo', undefined); }); it("should set deeply nested properties", function() { run(function() { bindNotify('{a.b.c.d.e.f.g}', spy); }, function() { setNotify('a', { b: { c: { d: { e: { f: { g: 'val' } } } } } }); }); expectArgs('val', undefined); }); it("should be able to set mixes of values/objects", function() { var city = jasmine.createSpy(); run(function() { viewModel.bind('{user.name}', spy); viewModel.bind('{user.address.city}', city); notify(); }, function() { setNotify('user', { name: 'Foo', address: { city: 'Paris' } }); }); expectArgs('Foo', undefined); expectArgsForCall(city.mostRecentCall, 'Paris'); }); }); describe("callback settings", function() { it("should pass the old and new value", function() { run(function() { bindNotify('{name}', spy); }, function() { setNotify('name', 'Foo'); setNotify('name', 'Bar'); }); if (bindFirst) { expectArgsForCall(spy.calls[0], 'Foo', undefined); expectArgsForCall(spy.calls[1], 'Bar', 'Foo'); } else { expectArgs('Bar', undefined); } }); it("should default the scope to the session", function() { run(function() { bindNotify('{name}', spy); }, function() { setNotify('name', 'X'); }); expect(spy.mostRecentCall.object).toBe(viewModel); }); it("should use the passed scope", function() { var o = {}; run(function() { bindNotify('{name}', spy, o); }, function() { setNotify('name', 'X'); }); expect(spy.mostRecentCall.object).toBe(o); }); }); describe("timing of callbacks", function() { it("should not trigger the callback if the value doesn't change", function() { run(function() { bindNotify('{name}', spy); }, function() { setNotify('name', 'Foo'); }); spy.reset(); setNotify('name', 'Foo'); expect(spy).not.toHaveBeenCalled(); }); it("should not trigger any parent nodes if the leaf value doesn't change", function() { var inner = jasmine.createSpy(); run(function() { viewModel.bind('{foo}', spy); viewModel.bind('{foo.bar}', inner); }, function() { viewModel.set('foo.bar.baz.x', 'Foo'); }); notify(); reset(spy, inner); setNotify('foo.bar.baz.x', 'Foo'); expect(spy).not.toHaveBeenCalled(); expect(inner).not.toHaveBeenCalled(); }); it("should be able to bind twice to the same stub", function() { var other = jasmine.createSpy(); run(function() { bindNotify('{name}', spy); bindNotify('{name}', other); }, function() { setNotify('name', 'A'); }); expectArgsForCall(spy.mostRecentCall, 'A', undefined); expectArgsForCall(other.mostRecentCall, 'A', undefined); }); it("should trigger a new binding when there is a set pending", function() { var other = jasmine.createSpy(); run(function() { bindNotify('{name}', spy); }, function() { viewModel.set('name', 'A'); }); bindNotify('{name}', other); expect(other).toHaveBeenCalled(); }); it("should only fire a single callback inside the timer resolution", function() { run(function() { bindNotify('{name}', spy); }, function() { viewModel.set('name', 'A'); viewModel.set('name', 'B'); viewModel.set('name', 'C'); viewModel.set('name', 'D'); notify(); }); expect(spy.callCount).toBe(1); expectArgs('D', undefined); }); it("should only pass the last value since the last fired change", function() { run(function() { bindNotify('{name}', spy); }, function() { setNotify('name', 'A'); }); viewModel.set('name', 'B'); viewModel.set('name', 'C'); viewModel.set('name', 'D'); viewModel.set('name', 'E'); notify(); expectArgs('E', 'A'); }); // Tests specifically for bind/data first if (bindFirst) { it("should not trigger the binding initially if a value is not set", function() { bindNotify('{name}', spy); expect(spy).not.toHaveBeenCalled(); }); it("should suspend the initial binding if the value is set within the tick window", function() { viewModel.bind('{name}', spy); setNotify('name', 'Foo'); expectArgs('Foo', undefined); }); } else { it("should trigger the binding initially if a value exists", function() { viewModel.set('name', 'Foo'); bindNotify('{name}', spy); expect(spy).toHaveBeenCalled(); expectArgs('Foo', undefined); }); } }); describe("binding on nested values", function() { it("should trigger a new long chain binding", function() { run(function() { bindNotify('{user.address.city}', spy); }, function() { setNotify('user.address.city', 'Sydney'); }); expectArgs('Sydney', undefined); }); it("should trigger a deep parent binding when a child changes", function() { var city = jasmine.createSpy(), address = jasmine.createSpy(); run(function() { bindNotify('{user.address.city}', city); bindDeepNotify('{user.address}', address); }, function() { setNotify('user.address.city', 'Berlin'); }); expectArgsForCall(city.mostRecentCall, 'Berlin', undefined); expect(address.mostRecentCall.args[0]).toEqual({ city: 'Berlin' }); }); it("should trigger all deep parent bindings when a child changes", function() { var city = jasmine.createSpy(), address = jasmine.createSpy(), user = jasmine.createSpy(); run(function() { bindNotify('{user.address.city}', city); bindDeepNotify('{user.address}', address); bindDeepNotify('{user}', user); }, function() { setNotify('user.address.city', 'Jakarta'); }); expect(city).toHaveBeenCalled(); expect(address).toHaveBeenCalled(); expect(user).toHaveBeenCalled(); }); it("should trigger parent bindings even if a node in the hierarchy is skipped", function() { var city = jasmine.createSpy(), user = jasmine.createSpy(); run(function() { bindNotify('{user.address.city}', city); bindDeepNotify('{user}', user); }, function() { setNotify('user.address.city', 'London'); }); expect(city).toHaveBeenCalled(); expect(user).toHaveBeenCalled(); }); it("should only trigger the parent binding once if several direct children change", function() { run(function() { bindDeepNotify('{user.address}', spy); }, function() { viewModel.set('user.address.street', '1 Foo St'); viewModel.set('user.address.city', 'Moscow'); viewModel.set('user.address.zip', 12345); viewModel.set('user.address.country', 'Russia'); notify(); }); expect(spy.callCount).toBe(1); }); it("should only trigger the parent once even if several indirect children change", function() { run(function() { bindDeepNotify('{user}', spy); }, function() { viewModel.set('user.homeAddress.street', 'Foo'); viewModel.set('user.homeAddress.city', 'Florida'); viewModel.set('user.postalAddress.street', 'Bar'); viewModel.set('user.postalAddress.city', 'Baltimore'); notify(); }); expect(spy.callCount).toBe(1); }); describe("modifying hierarchies", function() { function getHierarchy(val) { return { foo: { bar: { baz: { xxx: val } } } }; } it("should trigger changes on the children when hierarchy is overwritten with a primitive", function() { var xxx = jasmine.createSpy(), baz = jasmine.createSpy(), bar = jasmine.createSpy(); run(function() { viewModel.bind('{foo.bar.baz.xxx}', xxx); viewModel.bind('{foo.bar.baz}', baz); viewModel.bind('{foo.bar}', bar); notify(); }, function() { setNotify('foo.bar.baz.xxx', 1); }); reset(xxx, baz, bar); setNotify('foo', 1); expect(xxx).toHaveBeenCalled(); expect(baz).toHaveBeenCalled(); expect(bar).toHaveBeenCalled(); }); it("should trigger changes on the children when hierarchy is overwritten with null", function() { run(function() { viewModel.bind('{foo.bar}', spy); notify(); }, function() { viewModel.set({ foo: { bar: 1 } }); notify(); }); spy.reset(); setNotify('foo', null); expectArgs(null, 1); }); it("should set the child value correctly when changing a hierarchy in a single tick", function() { run(function() { viewModel.bind('{foo.bar.baz.xxx}', spy); }, function() { viewModel.set(getHierarchy(123)); viewModel.set(getHierarchy(456)); viewModel.set(getHierarchy(789)); }); notify(); expect(spy.callCount).toBe(1); expectArgs(789); }); it("should set the child value correctly when changing a hierarchy over multiple ticks", function() { run(function() { viewModel.bind('{foo.bar.baz.xxx}', spy); }, function() { viewModel.set(getHierarchy(123)); }); notify(); expectArgs(123); viewModel.set(getHierarchy(456)); notify(); expectArgs(456); viewModel.set(getHierarchy(789)); notify(); expectArgs(789); }); it("should set the child value correctly when overwriting a hierarchy in a single tick", function() { run(function() { viewModel.bind('{foo.bar.baz.xxx}', spy); }, function() { viewModel.set(getHierarchy(123)); viewModel.set(getHierarchy(456)); viewModel.set({ foo: null }); }); notify(); if (bindFirst) { expect(spy.callCount).toBe(1); expectArgs(null, undefined); } else { expect(spy).not.toHaveBeenCalled(); } }); it("should set the child value correctly when overwriting a hierarchy over multiple ticks", function() { run(function() { viewModel.bind('{foo.bar.baz.xxx}', spy); }, function() { viewModel.set(getHierarchy(123)); }); notify(); expectArgs(123); viewModel.set(getHierarchy(456)); notify(); expectArgs(456); viewModel.set({ foo: null }); notify(); expectArgs(null); }); it("should be able to expand a primitive into a hierarchy", function() { var xxx = jasmine.createSpy(), baz = jasmine.createSpy(), bar = jasmine.createSpy(); run(function() { viewModel.bind('{foo.bar.baz.xxx}', xxx, null, {deep: true}); viewModel.bind('{foo.bar.baz}', baz, null, {deep: true}); viewModel.bind('{foo.bar}', bar, null, {deep: true}); }, function() { viewModel.set('foo', 1); }); notify(); reset(xxx, baz, bar); setNotify('foo.bar.baz.xxx', 1); expect(xxx).toHaveBeenCalled(); expect(baz).toHaveBeenCalled(); expect(bar).toHaveBeenCalled(); }); it("should be able to expand an existing object path", function() { run(function() { bindNotify('{foo.bar.baz.xxx}', spy); }, function() { viewModel.set({ foo: null }); }); notify(); setNotify('foo.bar.baz.xxx', 1); expect(spy.callCount).toBe(1); expectArgs(1, undefined); }); if (bindFirst) { it("should set the child value correctly when expanding a hierarchy in a single tick", function() { viewModel.bind('{foo.bar.baz.xxx}', spy); notify(); viewModel.set({ foo: null }); viewModel.set({ foo: { bar: null } }); viewModel.set({ foo: { bar: { baz: null } } }); viewModel.set({ foo: { bar: { baz: { xxx: 100 } } } }); notify(); expect(spy.callCount).toBe(1); expectArgs(100); }); it("should set the child value correctly when expanding a hierarchy over multiple ticks", function() { viewModel.bind('{foo.bar.baz.xxx}', spy); notify(); viewModel.set({ foo: null }); notify(); expect(spy).not.toHaveBeenCalled(); viewModel.set({ foo: { bar: null } }); notify(); expect(spy).not.toHaveBeenCalled(); viewModel.set({ foo: { bar: { baz: null } } }); notify(); expect(spy).not.toHaveBeenCalled(); viewModel.set({ foo: { bar: { baz: { xxx: 100 } } } }); notify(); expectArgs(100); viewModel.set({ foo: null }); notify(); // Now that we have a value loaded for xxx, it should publish as null expectArgs(null); }); } }); }); }); } createSuite(false); createSuite(true); describe("data types", function() { describe("dates", function() { it("should change when setting an initial date", function() { var d = new Date(2010, 0, 1); bindNotify('{val}', spy); setNotify('val', d); expectArgs(d, undefined); }); it("should change when setting a new date", function() { var d1 = new Date(2010, 0, 1), d2 = new Date(2000, 3, 15); setNotify('val', d1); bindNotify('{val}', spy); spy.reset(); setNotify('val', d2); expectArgs(d2, d1); }); it("should not change when setting the same date with a different reference", function() { var d1 = new Date(2010, 0, 1), d2 = new Date(2010, 0, 1); setNotify('val', d1); bindNotify('{val}', spy); spy.reset(); setNotify('val', d2); expect(spy).not.toHaveBeenCalled(); }); }); }); describe("firing order", function() { it("should fire children before parents", function() { var values = []; viewModel.bind('{address}', function (v) { values.push('address: ' + Ext.encode(v)); }).deep = true; viewModel.bind('{address.city}', function (v) { values.push('address.city: ' + v); }); notify(); expect(values).toEqual([]); viewModel.set('address.city', 'Melbourne'); notify(); expect(values[0]).toBe('address.city: Melbourne'); expect(values[1]).toBe('address: {"city":"Melbourne"}'); }); it("should fire a single binding at the depth of it's stub", function() { setNotify('foo.bar.baz.x', 1); var values = [], adder = function(arg1) { values.push(arg1); }; viewModel.bind('{foo.bar.baz.x}', adder); viewModel.bind('{foo.bar.y}', adder); viewModel.set('foo.bar.y', 3); viewModel.set('foo.bar.baz.x', 2); notify(); expect(values[0]).toBe(2); expect(values[1]).toBe(3); }); it("should fire complex hierarchies in depth order", function() { var data = { key1: { key11: { key111: { key1111: 'a', // d=4 key1112: 'b' // d=4 }, key112: 'c' // d=3 }, key12: { key121: 'd', // d=3 key122: 'e' // d=3 } }, key2: { key21: { key211: 'f' // d=3 }, key22: { key221: { key2211: { key22111: 'g' // d=5 } }, key222: { key2221: 'h' // d=4 } }, key23: { key231: 'i' // d=3 } }, key3: { key31: 'j', // d=2 key32: { key321: 'k' // d=3 }, key33: { key331: { key3311: 'l' // d=4 }, key332: 'm' // d=3 } }, key4: { key41: 'n' // d=2 }, key5: 'o', // d=1 key6: { key61: { key611: { key6111: { key61111: { key611111: { key6111111: 'p' // d=7 }, key611112: 'q' // d=6 } } }, key612: { key6121: { key61211: { key61211: 'r' // d=6 } } }, key613: { key6131: { key61311: { key613111: { key6131111: 's' // d=7 } } } } } } }; var map = {}; var items = []; var entryLog = []; var valueLog = []; function buildMap (value, parent, path) { var entry = { id: items.length + 1, path: path, parent: parent, value: value }; items.push(map[path] = entry); if (path) { viewModel.bind('{' + path + '}', function (v) { entryLog.push(entry); valueLog.push(v); // We can say for certain that none of our parent objects // should have called back at this time. for (var p = parent; p; p = p.parent) { expect(Ext.Array.contains(entryLog, p)).toBe(false); } }); } if (value && value.constructor === Object) { var subPath = path ? path + '.' : ''; Ext.Object.each(value, function (name, v) { buildMap(v, entry, subPath + name); }); } return entry; } var root = buildMap(data, null, ''), prefix, i; notify(); for (i = 0; i < valueLog.length; ++i) { prefix = entryLog[i].path + '='; expect(prefix + valueLog[i]).toEqual(prefix + 'null'); } entryLog.length = valueLog.length = 0; setNotify('', data); notify(); for (i = 0; i < valueLog.length; ++i) { // Each delivered value should preserve the references we passed in // for this case. expect(valueLog[i]).toBe(entryLog[i].value); } }); }); }); describe("parsing formulas", function () { var vm; function getFormula (name) { var stub = vm.getStub(name); return stub.formula; } function getExpressions (name) { var formula = getFormula(name), expressions = Ext.apply({}, formula.get.$expressions); delete expressions.$literal; return Ext.Object.getKeys(expressions); } beforeEach(function() { createViewModel(); }); afterEach(function () { vm.destroy(); vm = null; }); describe("simple formulas", function () { it("should recognize property access", function () { vm = new Ext.app.ViewModel({ formulas: { foo: function (get) { return get('x.y') + get('z'); } } }); var expressions = getExpressions('foo'); expect(expressions).toEqual(['x.y', 'z']); }); it("should ignore method calls", function () { vm = new Ext.app.ViewModel({ formulas: { foo: function (get) { return get('x.y').substring(1) + get('z').toLowerCase(); } } }); var expressions = getExpressions('foo'); expect(expressions).toEqual(['x.y', 'z']); }); it("should recognize data as method parameters", function () { vm = new Ext.app.ViewModel({ formulas: { foo: function (get) { return this.foo(get('x')+get('y.z')); } } }); var expressions = getExpressions('foo'); expect(expressions).toEqual(['x', 'y.z']); }); it("should ignore data used in suffix expression", function () { vm = new Ext.app.ViewModel({ formulas: { foo: function (get) { return this.get.foo(get('x') + get('y.z')); } } }); var expressions = getExpressions('foo'); expect(expressions).toEqual(['x', 'y.z']); }); }); describe("formula config objects", function () { it("should recognize property access", function () { vm = new Ext.app.ViewModel({ formulas: {