UNPKG

can-map

Version:

Observable Objects

739 lines (573 loc) 18.6 kB
/* jshint asi:true */ /*jshint -W079 */ var Map = require('can-map'); var QUnit = require('steal-qunit'); var canCompute = require('can-compute'); var ObservationRecorder = require('can-observation-recorder'); var Construct = require('can-construct'); var observeReader = require('can-stache-key'); var canReflect = require('can-reflect'); var canSymbol = require('can-symbol'); var queues = require("can-queues"); var testHelpers = require("can-test-helpers"); QUnit.module('can-map'); QUnit.test("Basic Map", function(assert) { assert.expect(4); var state = new Map({ category: 5, productType: 4 }); state.bind("change", function (ev, attr, how, val, old) { assert.equal(attr, "category", "correct change name"); assert.equal(how, "set"); assert.equal(val, 6, "correct"); assert.equal(old, 5, "correct"); }); state.attr("category", 6); state.unbind("change"); }); QUnit.test("Nested Map", function(assert) { assert.expect(5); var me = new Map({ name: { first: "Justin", last: "Meyer" } }); assert.ok(me.attr("name") instanceof Map); me.bind("change", function (ev, attr, how, val, old) { assert.equal(attr, "name.first", "correct change name"); assert.equal(how, "set"); assert.equal(val, "Brian", "correct"); assert.equal(old, "Justin", "correct"); }); me.attr("name.first", "Brian"); me.unbind("change"); }); QUnit.test("remove attr", function(assert) { var state = new Map({ category: 5, productType: 4 }); state.removeAttr("category"); assert.deepEqual(Map.keys(state), ["productType"], "one property"); }); QUnit.test("remove attr on key with dot", function(assert) { var state = new Map({ "key.with.dots": 12, productType: 4 }); var state2 = new Map({ "key.with.dots": 4, key: { "with": { someValue: 20 } } }); state.removeAttr("key.with.dots"); state2.removeAttr("key.with.someValue"); assert.deepEqual( Map.keys(state), ["productType"], "one property"); assert.deepEqual( Map.keys(state2), ["key.with.dots", "key"], "two properties"); assert.deepEqual( Map.keys( state2.key["with"] ) , [], "zero properties"); }); QUnit.test("nested event handlers are not run by changing the parent property (#280)", function(assert) { var person = new Map({ name: { first: "Justin" } }) person.bind("name.first", function (ev, newName) { assert.ok(false, "name.first should never be called") //equal(newName, "hank", "name.first handler called back with correct new name") }); person.bind("name", function () { assert.ok(true, "name event triggered") }) person.attr("name", { first: "Hank" }); }); QUnit.test("cyclical objects (#521)", function(assert) { var foo = {}; foo.foo = foo; var fooed = new Map(foo); assert.ok(true, "did not cause infinate recursion"); assert.ok(fooed.attr('foo') === fooed, "map points to itself") var me = { name: "Justin" } var references = { husband: me, friend: me } var ref = new Map(references) assert.ok(ref.attr('husband') === ref.attr('friend'), "multiple properties point to the same thing") }) QUnit.test('_cid add to original object', function(assert) { var map = new Map(), obj = { 'name': 'thecountofzero' }; map.attr('myObj', obj); assert.ok(!obj._cid, '_cid not added to original object'); }); QUnit.test("Map serialize triggers reading (#626)", function(assert) { var old = ObservationRecorder.add; var attributesRead = []; var readingTriggeredForKeys = false; ObservationRecorder.add = function (object, attribute) { if (attribute === "__keys") { readingTriggeredForKeys = true; } else { attributesRead.push(attribute); } }; var testMap = new Map({ cats: "meow", dogs: "bark" }); testMap.serialize(); assert.ok(attributesRead.indexOf("cats") !== -1 && attributesRead.indexOf("dogs") !== -1, "map serialization triggered __reading on all attributes"); assert.ok(readingTriggeredForKeys, "map serialization triggered __reading for __keys"); ObservationRecorder.add = old; }) QUnit.test("Test top level attributes", function(assert) { assert.expect(7); var test = new Map({ 'my.enable': false, 'my.item': true, 'my.count': 0, 'my.newCount': 1, 'my': { 'value': true, 'nested': { 'value': 100 } } }); assert.equal(test.attr('my.value'), true, 'correct'); assert.equal(test.attr('my.nested.value'), 100, 'correct'); assert.ok(test.attr("my.nested") instanceof Map); assert.equal(test.attr('my.enable'), false, 'falsey (false) value accessed correctly'); assert.equal(test.attr('my.item'), true, 'truthey (true) value accessed correctly'); assert.equal(test.attr('my.count'), 0, 'falsey (0) value accessed correctly'); assert.equal(test.attr('my.newCount'), 1, 'falsey (1) value accessed correctly'); }); QUnit.test("serializing cycles", function(assert) { var map1 = new Map({name: "map1"}); var map2 = new Map({name: "map2"}); map1.attr("map2", map2); map2.attr("map1", map1); var res = map1.serialize(); assert.equal(res.name, "map1"); assert.equal(res.map2.name, "map2"); }); QUnit.test("Unbinding from a map with no bindings doesn't throw an error (#1015)", function(assert) { assert.expect(0); var test = new Map({}); try { test.unbind('change'); } catch(e) { assert.ok(false, 'No error should be thrown'); } }); QUnit.test("Fast dispatch event still has target and type (#1082)", function(assert) { assert.expect(4); var data = new Map({ name: 'CanJS' }); data.bind('change', function(ev){ assert.equal(ev.type, 'change'); assert.equal(ev.target, data); }); data.bind('name', function(ev){ assert.equal(ev.type, 'name'); assert.equal(ev.target, data); }); data.attr('name', 'David'); }); QUnit.test("map passed to Map constructor (#1166)", function(assert) { function y() {} var map = new Map({ x: 1, y: y }); var res = new Map(map); assert.deepEqual(res.attr(), { x: 1, y: y }, "has the same properties"); }); QUnit.test("constructor passed to scope is threated as a property (#1261)", function(assert) { var Constructor = Construct.extend({}); var MyMap = Map.extend({ Todo: Constructor }); var m = new MyMap(); assert.equal(m.attr("Todo"), Constructor); }); QUnit.test('_bindings count maintained after calling .off() on undefined property (#1490) ', function(assert) { var map = new Map({ test: 1 }); map.on('test', function(){}); var handlers = map[canSymbol.for("can.meta")].handlers; assert.equal(handlers.get([]).length, 1, 'The number of bindings is correct'); map.off('undefined_property'); assert.equal(handlers.get([]).length, 1, 'The number of bindings is still correct'); }); QUnit.test("Should be able to get and set attribute named 'watch' on Map in Firefox", function(assert) { var map = new Map({}); map.attr("watch"); assert.ok(true, "can have attribute named 'watch' on a Map instance"); }); QUnit.test("Should be able to get and set attribute named 'unwatch' on Map in Firefox", function(assert) { var map = new Map({}); map.attr("unwatch"); assert.ok(true, "can have attribute named 'unwatch' on a Map instance"); }); QUnit.test('should get an empty string property value correctly', function(assert) { var map = new Map({ foo: 'foo', '': 'empty string' }); assert.equal(map.attr(''), 'empty string'); }); QUnit.test("ObserveReader - can.Construct derived classes should be considered objects, not functions (#450)", function(assert) { var foostructor = Map.extend({ text: "bar" }, {}), obj = { next_level: { thing: foostructor, text: "In the inner context" } }, read; foostructor.self = foostructor; read = observeReader.read(obj, observeReader.reads("next_level.thing.self.text") ); assert.equal(read.value, "bar", "static properties on a can.Construct-based function"); read = observeReader.read(obj, observeReader.reads("next_level.thing.self"), { isArgument: true }); assert.ok(read.value === foostructor, "arguments shouldn't be executed"); }); // TODO re-enable tests after getting can-compute up to speed or replacing with simple observables // test("Basic Map.prototype.compute", function () { // var state = new Map({ // category: 5, // productType: 4 // }); // var catCompute = state.compute('category'); // var prodCompute = state.compute('productType'); // catCompute.bind("change", function (ev, val, old) { // equal(val, 6, "correct"); // equal(old, 5, "correct"); // }); // state.bind('productType', function(ev, val, old) { // equal(val, 5, "correct"); // equal(old, 4, "correct"); // }); // state.attr("category", 6); // prodCompute(5); // catCompute.unbind("change"); // state.unbind("productType"); // }); // test("Deep Map.prototype.compute", function () { // var state = new Map({ // product: { // category: 5, // productType: 4 // } // }); // var catCompute = state.compute('product.category'); // var prodCompute = state.compute('product.productType'); // catCompute.bind("change", function (ev, val, old) { // equal(val, 6, "correct"); // equal(old, 5, "correct"); // }); // state.attr('product').bind('productType', function(ev, val, old) { // equal(val, 5, "correct"); // equal(old, 4, "correct"); // }); // state.attr("product.category", 6); // prodCompute(5); // catCompute.unbind("change"); // state.unbind("productType"); // }); QUnit.test("works with can-reflect", function(assert) { assert.expect(7); var b = new Map({ "foo": "bar" }); var c = new (Map.extend({ "baz": canCompute(function(){ return b.attr("foo"); }) }))({ "foo": "bar", thud: "baz" }); assert.equal( canReflect.getKeyValue(b, "foo"), "bar", "unbound value"); function bazHandler(newValue){ assert.equal(newValue, "quux", "observed new value on baz"); // Turn off the "foo" handler but "thud" should still be bound. canReflect.offKeyValue(c, "baz", bazHandler); } function thudHandler(newValue){ assert.equal(newValue, "quux", "observed new value on thud"); // Turn off the "foo" handler but "thud" should still be bound. canReflect.offKeyValue(c, "thud", thudHandler); } assert.ok(!canReflect.isValueLike(c), "isValueLike is false"); assert.ok(canReflect.isMapLike(c), "isMapLike is true"); assert.ok(!canReflect.isListLike(c), "isListLike is false"); canReflect.onKeyValue(c, "baz", bazHandler); // Do a second binding to check that you can unbind correctly. canReflect.onKeyValue(c, "thud", thudHandler); b.attr("foo", "quux"); c.attr("thud", "quux"); assert.equal( canReflect.getKeyValue(c, "baz"), "quux", "bound value"); // sanity checks to ensure that handler doesn't get called again. b.attr("foo", "thud"); c.attr("baz", "jeek"); }); QUnit.test("onKeyValue and queues", function(assert) { var b = new Map({ "foo": "bar" }); var order = []; canReflect.onKeyValue(b, "foo", function(){ order.push("onKeyValue"); },"notify"); queues.batch.start(); queues.mutateQueue.enqueue(function(){ order.push("mutate"); }); b.attr("foo","baz"); queues.batch.stop(); assert.deepEqual(order,["onKeyValue", "mutate"]); }); QUnit.test("can-reflect setKeyValue", function(assert) { var a = new Map({ "a": "b" }); canReflect.setKeyValue(a, "a", "c"); assert.equal(a.attr("a"), "c", "setKeyValue"); }); QUnit.test("can-reflect getKeyDependencies", function(assert) { var a = new Map({ "a": "a" }); var b = new (Map.extend({ "a": canCompute(function(){ return a.attr("a"); }), "b": "b" }))(); assert.ok(canReflect.getKeyDependencies(b, "a"), "Dependencies on computed attr"); assert.ok(!canReflect.getKeyDependencies(b, "b"), "No dependencies on data attr"); b.on("a", function() {}); assert.ok(canReflect.getKeyDependencies(b, "a").valueDependencies.has(b._computedAttrs.a.compute), "dependencies returned"); assert.ok( canReflect.getValueDependencies(b._computedAttrs.a.compute).valueDependencies, "dependencies returned from compute" ); }); QUnit.test("registered symbols", function(assert) { var a = new Map({ "a": "a" }); assert.ok(a[canSymbol.for("can.isMapLike")], "can.isMapLike"); assert.equal(a[canSymbol.for("can.getKeyValue")]("a"), "a", "can.getKeyValue"); a[canSymbol.for("can.setKeyValue")]("a", "b"); assert.equal(a.attr("a"), "b", "can.setKeyValue"); function handler(val) { assert.equal(this, a); assert.equal(val, "c", "can.onKeyValue"); } a[canSymbol.for("can.onKeyValue")]("a", handler); a.attr("a", "c"); a[canSymbol.for("can.offKeyValue")]("a", handler); a.attr("a", "d"); // doesn't trigger handler }); require("can-reflect-tests/observables/map-like/type/type")("Map", function(){ return Map.extend({}); }); QUnit.test("can.isBound", function(assert) { var Person = Map.extend({ first: "any", last: "any" }); var p = new Person(); assert.ok(! p[canSymbol.for("can.isBound")](), "not bound"); }); QUnit.test("prototype properties", function(assert) { var MyMap = Map.extend({ letters: "ABC" }); var map = new MyMap(); assert.equal(map.attr("letters"), "ABC"); }); QUnit.test("can read numbers", function(assert) { var map = new Map({ 0: "zero" }); assert.equal(canReflect.getKeyValue(map, 0), "zero"); assert.equal(map.attr(0), "zero"); canReflect.onKeyValue(0, function handler(ev, newVal) { assert.equal(newVal, "one"); canReflect.offKeyValue(0, handler); }); canReflect.setKeyValue(map, 0, "one"); }); QUnit.test("attr should work when remove === 'true'", function(assert) { var map = new Map({ 0: "zero" }); map.attr({ 1: "one" }, "true"); assert.equal(canReflect.getKeyValue(map, 0), undefined); assert.equal(map.attr(0), undefined); assert.equal(canReflect.getKeyValue(map, 1), "one"); assert.equal(map.attr(1), "one"); }); QUnit.test("constructor should not bind on __keys (#106)", function(assert) { var map; var comp = canCompute(function() { map = new Map(); }); canReflect.onValue(comp, function() {}); map.attr('foo', 'bar'); assert.equal(map.attr('foo'), 'bar', 'map should not be reset'); }); QUnit.test(".attr(props) should overwrite if _legacyAttrBehavior is true (#112)", function(assert) { Map.prototype._legacyAttrBehavior = true; var myMap1Instance = new Map({prop1: new Map()}); var changes = 0; myMap1Instance.on("prop1", function() { changes++; }); var map2 = new Map({prop1: "xyz"}); myMap1Instance.attr({ "prop1": map2 }); delete Map.prototype._legacyAttrBehavior; assert.equal(changes,1, "caused a change event"); assert.equal(myMap1Instance.attr("prop1"), map2, "overwrite with maps"); }); QUnit.test(".attr() leaves typed instances alone if _legacyAttrBehavior is true (#111)", function(assert) { Map.prototype._legacyAttrBehavior = true; function MyClass(value){ this.value = value; } MyClass.prototype.log = function(){ return this.value; }; var myMap = new Map({ myClass: new MyClass(5) }); assert.equal( myMap.attr().myClass, myMap.attr("myClass") ); var myMap2 = new Map({ myClass: {foo: "bar"} }); assert.deepEqual(myMap2.attr(), { myClass: {foo: "bar"} }, "attr returns plain objects"); delete Map.prototype._legacyAttrBehavior; }); QUnit.test(".serialize() leaves typed instances alone if _legacyAttrBehavior is true", function(assert) { function MyClass(value) { this.value = value; } var myMap = new Map({ _legacyAttrBehavior: true, myClass: new MyClass('foo') }); var ser = myMap.serialize(); assert.equal(ser.myClass, myMap.attr("myClass")); }); QUnit.test("keys with undefined values should not be dropped (#118)", function(assert) { // handles new instances var obj1 = { "keepMe": undefined }; var map = new Map(obj1); // handles late props map.attr('foo', undefined); var keys = Map.keys(map); assert.deepEqual(keys, ["keepMe", "foo"]) }); QUnit.test("Can assign nested properties that are not CanMaps", function(assert) { var MyType = function() { this.one = 'one'; this.two = 'two'; this.three = 'three'; }; MyType.prototype[canSymbol.for("can.onKeyValue")] = function(){}; MyType.prototype[canSymbol.for("can.isMapLike")] = true; var map = new Map({ _legacyAttrBehavior: true, foo: 'bar', prop: new MyType() }); map.attr({ prop: {one: '1', two: '2'} }); // Did an assign assert.equal(map.attr("prop.one"), "1"); assert.equal(map.attr("prop.two"), "2"); assert.equal(map.attr("prop.three"), "three"); // An update map.attr({ prop: {one: 'one', two: 'two'} }, true); assert.equal(map.attr("prop.one"), "one"); assert.equal(map.attr("prop.two"), "two"); assert.equal(map.attr("prop.three"), undefined); }); testHelpers.dev.devOnlyTest("warning when setting during a get", function(assert){ var msg = /.* This can cause infinite loops and performance issues.*/; var teardownWarn = testHelpers.dev.willWarn(msg, function(text, match) { if(match) { assert.ok(true, "warning fired"); } }); var noop = function() {}; var Type = Map.extend("Type", { prop: "", prop2: "" }); var inst = new Type(); var Type2 = Map.extend("Type2", { baz: canCompute(function getterThatWrites() { inst.attr("prop", "foo"); return inst.attr("prop2"); }) }); var obs = new Type2(); canReflect.setName(Type2.prototype.baz, "a test observation"); obs.on("baz", noop); inst.attr("prop2", "bar"); assert.equal(teardownWarn(), 1, "warning correctly generated"); teardownWarn = testHelpers.dev.willWarn(msg, function(text, match) { if(match) { assert.ok(false, "warning incorrectly fired"); } }); obs.off("baz", noop); inst.attr("prop2", "baz"); teardownWarn(); }); testHelpers.dev.devOnlyTest("warning when setting during a get (batched)", function(assert){ var msg = /.* This can cause infinite loops and performance issues.*/; var teardownWarn = testHelpers.dev.willWarn(msg, function(text, match) { if(match) { assert.ok(true, "warning fired"); } }); var noop = function() {}; var Type = Map.extend("Type", { prop: "", prop2: "" }); var inst = new Type(); queues.batch.start(); var Type2 = Map.extend("Type2", { baz: canCompute(function getterThatWrites() { inst.attr("prop", "foo"); return inst.attr("prop2"); }) }); var obs = new Type2(); canReflect.setName(Type2.prototype.baz, "a test observation"); obs.on("baz", noop); inst.attr("prop2", "bar"); queues.batch.stop(); assert.equal(teardownWarn(), 1, "warning correctly generated"); teardownWarn = testHelpers.dev.willWarn(msg, function(text, match) { if(match) { assert.ok(false, "warning incorrectly fired"); } }); obs.off("baz", noop); queues.batch.start(); inst.attr("prop2", "baz"); queues.batch.stop(); teardownWarn(); });