PageRenderTime 8ms CodeModel.GetById 5ms app.highlight 65ms RepoModel.GetById 2ms app.codeStats 0ms

/test/epoxy-test.js

https://github.com/anthonybrown/backbone.epoxy
JavaScript | 1011 lines | 722 code | 239 blank | 50 comment | 5 complexity | 2ee407b5485a852af8e94a6a7cb7e7b7 MD5 | raw file
   1// Epoxy.Model
   2// -----------
   3describe("Backbone.Epoxy.Model", function() {
   4	
   5	var model;
   6	
   7	
   8	// Primay model for test suite:
   9	var TestModel = Backbone.Epoxy.Model.extend({
  10		defaults: {
  11			firstName: "Charlie",
  12			lastName: "Brown",
  13			payment: 100
  14		},
  15		
  16		observableDefaults: {
  17			isSelected: false,
  18			testArray: []
  19		},
  20		
  21		computeds: {
  22			// Tests setting a computed property with the direct single-function getter shorthand:
  23			fullName: function() {
  24				return this.get( "firstName" ) +" "+ this.get( "lastName" );
  25			},
  26			
  27			// Tests two facets:
  28			// 1) computed dependencies definition order (defined before/after a dependency).
  29			// 2) computed dependencies building ontop of one another.
  30			paymentLabel: function() {
  31				return this.get( "fullName" ) +" paid "+ this.get( "paymentCurrency" );
  32			},
  33			
  34			// Tests defining a read/write computed property with getters and setters:
  35			paymentCurrency: {
  36				get: function() {
  37					return "$"+ this.get( "payment" );
  38				},
  39				set: function( value ) {
  40					return value ? {payment: parseInt(value.replace("$", ""), 10)} : value;
  41				}
  42			},
  43			
  44			// Tests defining a computed property with unreachable values...
  45			// first/last names are accessed conditionally, therefore cannot be automatically detected.
  46			// field dependencies may be declared manually to address this (ugly though);
  47			// a better solution would be to collect both "first" and "last" as local vars,
  48			// then release the locally established values conditionally.
  49			unreachable: {
  50				deps: ["firstName", "lastName", "isSelected"],
  51				get: function() {
  52					return this.get("isSelected") ? this.get("lastName") : this.get("firstName");
  53				}
  54			}
  55		},
  56
  57		initialize: function() {
  58
  59		}
  60	});
  61	
  62	
  63	// Secondary model, established for some relationship testing:
  64	var ForeignModel = Backbone.Epoxy.Model.extend({
  65		defaults: {
  66			avgPayment: 200
  67		}
  68	});
  69	
  70	
  71	// Setup
  72	beforeEach(function() {
  73		model = new TestModel();
  74	});
  75	
  76	// Teardown
  77	afterEach(function() {
  78		model.clearObservables();
  79		model = null;
  80	});
  81	
  82	
  83	it("should use '.observableDefaults' to define basic virtual properties.", function() {
  84		expect( model.get("isSelected") ).toBe( false );
  85	});
  86	
  87	
  88	it("should use .get() and .set() to modify virtual properties.", function() {
  89		model.set( "isSelected", true );
  90		expect( model.get("isSelected") ).toBe( true );
  91	});
  92	
  93	
  94	it("should allow direct access to observable objects through the '.obs' namespace.", function() {
  95		expect( !!model.obs.isSelected ).toBe( true );
  96	});
  97	
  98	
  99	// Deprecating this feature within the published API...
 100	it("should allow direct access to observable property values using their own getters and setters.", function() {
 101		var sel = model.obs[ "isSelected" ];
 102		expect( sel.get() ).toBe( false );
 103		sel.set( true );
 104		expect( sel.get() ).toBe( true );
 105	});
 106	
 107	
 108	it("should allow direct management of array attributes using the '.modifyArray' method.", function() {
 109		expect( model.get( "testArray" ).length ).toBe( 0 );
 110		model.modifyArray("testArray", "push", "beachball");
 111		expect( model.get( "testArray" ).length ).toBe( 1 );
 112	});
 113	
 114	
 115	it("should defer all action when using '.modifyArray' on a non-array object.", function() {
 116		model.modifyArray("isSelected", "push", "beachball");
 117		expect( model.get( "isSelected" ) ).toBe( false );
 118	});
 119	
 120	
 121	it("should assume computed properties defined as functions to be getters.", function() {
 122		var obsGetter = model.obs.fullName._get;
 123		var protoGetter = TestModel.prototype.computeds.fullName;
 124		expect( obsGetter === protoGetter ).toBe( true );
 125	});
 126	
 127	
 128	it("should use '.computeds' to automatically construct computed properties.", function() {
 129		var hasFullName = model.hasObservable("fullName");
 130		var hasDonation = model.hasObservable("paymentCurrency");
 131		expect( hasFullName && hasDonation ).toBe( true );
 132	});
 133	
 134	
 135	it("should allow computed properties to be constructed out of dependency order (dependents may preceed their dependencies).", function() {
 136		expect( model.get("paymentLabel") ).toBe( "Charlie Brown paid $100" );
 137	});
 138	
 139	
 140	it("should allow computed properties to be defined with manual dependency declarations.", function() {
 141		// Test initial reachable value:
 142		expect( model.get("unreachable") ).toBe( "Charlie" );
 143		
 144		// Change conditional value to point at the originally unreachable value:
 145		model.set("isSelected", true);
 146		expect( model.get("unreachable") ).toBe( "Brown" );
 147		
 148		// Change unreachable value
 149		model.set("lastName", "Black");
 150		expect( model.get("unreachable") ).toBe( "Black" );
 151	});
 152	
 153	
 154	it("should use .addComputed() to define computed properties.", function() {
 155		model.addComputed("nameReverse", function() {
 156			return this.get("lastName") +", "+ this.get("firstName");
 157		});
 158		expect( model.get("nameReverse") ).toBe( "Brown, Charlie" );
 159	});
 160	
 161	
 162	it("should use .addComputed() to define properties with passed dependencies.", function() {
 163		
 164		model.addComputed("unreachable", function() {
 165			return this.get("payment") > 50 ? this.get("firstName") : this.get("lastName");
 166		}, "payment", "firstName", "lastName");
 167		
 168		// Test initial reachable value:
 169		expect( model.get("unreachable") ).toBe( "Charlie" );
 170		
 171		// Change conditional value to point at the originally unreachable value:
 172		model.set("payment", 0);
 173		expect( model.get("unreachable") ).toBe( "Brown" );
 174		
 175		// Change unreachable value
 176		model.set("lastName", "Black");
 177		expect( model.get("unreachable") ).toBe( "Black" );
 178	});
 179	
 180	
 181	it("should use .addComputed() to define new properties from a params object.", function() {
 182		
 183		model.addComputed("addedProp", {
 184			deps: ["payment", "firstName", "lastName"],
 185			get: function() {
 186				return this.get("payment") > 50 ? this.get("firstName") : this.get("lastName");
 187			},
 188			set: function( value ) {
 189				return {payment: value};
 190			}
 191		});
 192		
 193		// Test initial reachable value:
 194		expect( model.get("addedProp") ).toBe( "Charlie" );
 195		
 196		// Change conditional value to point at the originally unreachable value:
 197		model.set("payment", 0);
 198		expect( model.get("addedProp") ).toBe( "Brown" );
 199		
 200		// Change unreachable value
 201		model.set("lastName", "Black");
 202		expect( model.get("addedProp") ).toBe( "Black" );
 203		
 204		// Set computed value
 205		model.set("addedProp", 123);
 206		expect( model.get("payment") ).toBe( 123 );
 207	});
 208	
 209
 210	it("should use .get() to access both model attributes and computed properties.", function() {
 211		var firstName = (model.get("firstName") === "Charlie");
 212		var fullName = (model.get("fullName") === "Charlie Brown");
 213		expect( firstName && fullName ).toBe( true );
 214	});
 215	
 216	
 217	it("should automatically map and bind computed property dependencies.", function() {
 218		var fullPre = (model.get( "fullName" ) === "Charlie Brown");
 219		model.set( "lastName", "Black" );
 220		var fullPost = (model.get( "fullName" ) === "Charlie Black");
 221		expect( fullPre && fullPost ).toBe( true );
 222	});
 223	
 224	
 225	it("should automatically map and bind computed property dependencies on foreign Epoxy models.", function() {
 226		var averages = new ForeignModel();
 227		
 228		model.addComputed("percentAvgPayment", function() {
 229			return this.get("payment") / averages.get("avgPayment");
 230		});
 231		
 232		expect( model.get("percentAvgPayment") ).toBe( 0.5 );
 233		averages.set("avgPayment", 400);
 234		expect( model.get("percentAvgPayment") ).toBe( 0.25 );
 235		averages.clearObservables();
 236	});
 237	
 238	
 239	it("should support manual definition of foreign dependencies.", function() {
 240		var foreign = new ForeignModel();
 241		
 242		model.addComputed("unreachable", function() {
 243			return this.get("firstName") ? this.get("payment") : foreign.get("avgPayment");
 244		}, "firstName", "payment", ["avgPayment", foreign]);
 245		
 246		// Test initial reachable value:
 247		expect( model.get("unreachable") ).toBe( 100 );
 248		
 249		// Change conditional value to point at the originally unreachable value:
 250		model.set("firstName", "");
 251		expect( model.get("unreachable") ).toBe( 200 );
 252		
 253		// Change unreachable value
 254		foreign.set("avgPayment", 400);
 255		expect( model.get("unreachable") ).toBe( 400 );
 256		foreign.clearObservables();
 257	});
 258
 259	
 260	it("should manage extended graphs of computed dependencies.", function() {
 261		expect( model.get("paymentLabel") ).toBe( "Charlie Brown paid $100" );
 262		model.set("payment", 150);
 263		expect( model.get("paymentLabel") ).toBe( "Charlie Brown paid $150" );
 264	});
 265	
 266	
 267	it("should use .set() to modify normal model attributes.", function() {
 268		model.set("payment", 150);
 269		expect( model.get("payment") ).toBe( 150 );
 270		expect( model.get("paymentCurrency") ).toBe( "$150" );
 271	});
 272	
 273	
 274	it("should use .set() for virtual computed properties to pass values along to the model.", function() {
 275		expect( model.get("payment") ).toBe( 100 );
 276		model.set("paymentCurrency", "$200");
 277		expect( model.get("payment") ).toBe( 200 );
 278		expect( model.get("paymentCurrency") ).toBe( "$200" );
 279	});
 280	
 281	
 282	it("should throw .set() error when modifying read-only computed properties.", function() {
 283		function testForError() {
 284			model.set("fullName", "Charlie Black");
 285		}
 286		expect( testForError ).toThrow();
 287	});
 288	
 289	
 290	it("should use .set() to allow computed properties to cross-set one another.", function() {
 291		model.addComputed("crossSetter", {
 292			get: function() {
 293				return this.get("isSelected");
 294			},
 295			set: function( value ) {
 296				return {isSelected: true};
 297			}
 298		});
 299		
 300		expect( model.get("crossSetter") ).toBe( false );
 301		model.set("crossSetter", true );
 302		expect( model.get("isSelected") ).toBe( true );
 303	});
 304	
 305	
 306	it("should throw .set() error in response to circular setter references.", function() {
 307		
 308		model.addComputed("loopSetter1", {
 309			get: function() {
 310				return "Nothing";
 311			},
 312			set: function( value ) {
 313				return {loopSetter2: false};
 314			}
 315		});
 316		
 317		model.addComputed("loopSetter2", {
 318			get: function() {
 319				return "Nothing";
 320			},
 321			set: function( value ) {
 322				return {loopSetter1: false};
 323			}
 324		});
 325		
 326		function circularRef() {
 327			model.set("loopSetter1", true );
 328		}
 329
 330		expect( circularRef ).toThrow();
 331	});
 332});
 333
 334// Epoxy.View
 335// ----------
 336describe("Backbone.Epoxy.View", function() {
 337	
 338	// Collection test components:
 339	
 340	var CollectionView = Backbone.View.extend({
 341		el: "<li><span class='name-dsp'></span> <button class='name-remove'>x</button></li>",
 342		initialize: function() {
 343			this.$( ".name-dsp" ).text( this.model.get("name") );
 344		}
 345	});
 346	
 347	var TestCollection = Backbone.Collection.extend({
 348		model: Backbone.Model,
 349		view: CollectionView
 350	});
 351	
 352	
 353	// Test model:
 354	
 355	window.bindingModel = new (Backbone.Epoxy.Model.extend({
 356		defaults: {
 357			firstName: "Luke",
 358			lastName: "Skywalker",
 359			preference: "b",
 360			active: true
 361		},
 362		
 363		observableDefaults: {
 364			checkList: ["b"],
 365			optionsList: [
 366				{value: "0", label: "Luke Skywalker"},
 367				{value: "1", label: "Han Solo"},
 368				{value: "2", label: "Obi-Wan Kenobi"}
 369			],
 370			optDefault: "default",
 371			optEmpty: "empty",
 372			valOptions: "1",
 373			valDefault: "1",
 374			valEmpty: "1",
 375			valBoth: "1",
 376			valMulti: "1",
 377			valCollect: ""
 378		},
 379		
 380		computeds: {
 381			nameDisplay: function() {
 382				return "<strong>"+this.get("lastName")+"</strong>, "+this.get("firstName");
 383			},
 384			firstNameError: function() {
 385				return !this.get( "firstName" );
 386			},
 387			lastNameError: function() {
 388				return !this.get( "lastName" );
 389			},
 390			errorDisplay: function() {
 391				var first = this.get( "firstName" );
 392				var last = this.get( "lastName" );
 393				return (!first || !last) ? "block" : "none";
 394			}
 395		}
 396	}));
 397	
 398	
 399	// Basic bindings test view:
 400	
 401	var domView = new (Backbone.Epoxy.View.extend({
 402		el: "#dom-view",
 403		model: bindingModel,
 404		bindings: "data-bind",
 405		
 406		bindingHandlers: {
 407			printArray: function( $element, value ) {
 408				$element.text( value.slice().sort().join(", ") );
 409			},
 410			sayYesNo: {
 411				get: function( $element ) {
 412					return {active: $element.val().indexOf("Y") === 0 };
 413				},
 414				set: function( $element, value ) {
 415					$element.val( value ? "Y" : "N" );
 416				}
 417			}
 418		}
 419	}));
 420	
 421	
 422	// Modifiers / Collections testing view:
 423	
 424	var modView = new (Backbone.Epoxy.View.extend({
 425		el: "#mod-view",
 426		model: bindingModel,
 427		collection: new TestCollection(),
 428		bindings: "data-bind",
 429
 430		events: {
 431			"click .name-add": "onAddName",
 432			"click .name-remove": "onRemoveName"
 433		},
 434		
 435		onAddName: function() {
 436			var input = this.$( ".name-input" );
 437			
 438			if ( input.val() ) {
 439				this.collection.add({
 440					name: input.val()
 441				});
 442				input.val("");
 443			}
 444		},
 445		
 446		onRemoveName: function( evt ) {
 447			var i = $( evt.target ).closest( "li" ).index();
 448			this.collection.remove( this.collection.at(i) );
 449		}
 450	}));
 451	
 452	
 453	// Bindings map declaration:
 454	
 455	var tmplView = new (Backbone.Epoxy.View.extend({
 456		el: $("#tmpl-view-tmpl").html(),
 457		model: bindingModel,
 458		
 459		bindings: {
 460			".user-first": "text:firstName",
 461			".user-last": "text:lastName"
 462		},
 463
 464		initialize: function() {
 465			$("#tmpl-view-tmpl").after( this.$el );
 466		}
 467	}));
 468	
 469	// Setup
 470	beforeEach(function() {
 471		
 472	});
 473	
 474	// Teardown
 475	afterEach(function() {
 476		var defaults = _.clone( bindingModel.observableDefaults );
 477		defaults.checkList = _.clone( defaults.checkList );
 478		defaults.optionsList = _.clone( defaults.optionsList );
 479		
 480		bindingModel.set( bindingModel.defaults );
 481		bindingModel.set( defaults );
 482		modView.collection.reset();
 483	});
 484	
 485	
 486	it("should bind view elements to model via binding selector map.", function() {
 487		var $el = $("#tmpl-view .user-first");
 488		expect( $el.text() ).toBe( "Luke" );
 489	});
 490	
 491	
 492	it("should bind view elements to model via element attribute query.", function() {
 493		var $el = $("#dom-view .test-text-first");
 494		expect( $el.text() ).toBe( "Luke" );
 495	});
 496	
 497	
 498	it("should include top-level view container in bindings searches.", function() {
 499		
 500		var view1 = new (Backbone.Epoxy.View.extend({
 501			el: "<span data-bind='text:firstName'></span>",
 502			model: bindingModel,
 503			bindings: "data-bind"
 504		}));
 505		
 506		var view2 = new (Backbone.Epoxy.View.extend({
 507			el: "<span class='first-name'></span>",
 508			model: bindingModel,
 509			bindings: {
 510				".first-name": "text:firstName"
 511			}
 512		}));
 513		
 514		expect( view1.$el.text() ).toBe( "Luke" );
 515		expect( view2.$el.text() ).toBe( "Luke" );
 516	});
 517	
 518	it("should throw error in response to undefined property bindings.", function() {
 519		
 520		var ErrorView = Backbone.Epoxy.View.extend({
 521			el: "<div><span data-bind='text:undefinedProp'></span></div>",
 522			model: bindingModel,
 523			bindings: "data-bind"
 524		});
 525		
 526		function testForError(){
 527			var error = new ErrorView();
 528		}
 529		
 530		expect( testForError ).toThrow();
 531	});
 532	
 533	
 534	it("should allow custom bindings to set data into the view.", function() {
 535		var $els = $(".test-custom-binding");
 536		expect( $els.text() ).toBe( "b" );
 537		bindingModel.set("checkList", ["c","a"]);
 538		expect( $els.text() ).toBe( "a, c" );
 539	});
 540	
 541	
 542	it("should allow custom bindings to get data from the view.", function() {
 543		var $el = $(".test-yes-no");
 544		expect( $el.val() ).toBe( "Y" );
 545		
 546		// Change through model, look for view change:
 547		bindingModel.set("active", false);
 548		expect( $el.val() ).toBe( "N" );
 549		
 550		// Change through view, look for model change:
 551		$el.val( "Y" ).trigger( "change" );
 552		expect( bindingModel.get("active") ).toBe( true );
 553	});
 554	
 555	
 556	it("should allow multiple data sources and their namespaced attributes to be defined through 'bindingSources'.", function() {
 557		var m1 = new Backbone.Model({name: "Luke"});
 558		var m2 = new Backbone.Collection();
 559		var m3 = new Backbone.Model({name: "Han"});
 560		var m4 = new Backbone.Collection();
 561		var v1, v2, v3, v4, v5, v6;
 562		
 563		var sourceView = new (Backbone.Epoxy.View.extend({
 564			el: "<div data-bind='b1:$model, b2:$collection, b3:$mod2, b4:$col2, b5:name, b6:mod2_name'></div>",
 565			model: m1,
 566			collection: m2,
 567			bindingSources: {
 568				mod2: m3,
 569				col2: m4
 570			},
 571			bindingHandlers: {
 572				b1: function( $el, value ) {
 573					v1 = value;
 574				},
 575				b2: function( $el, value ) {
 576					v2 = value;
 577				},
 578				b3: function( $el, value ) {
 579					v3 = value;
 580				},
 581				b4: function( $el, value ) {
 582					v4 = value;
 583				},
 584				b5: function( $el, value ) {
 585					v5 = value;
 586				},
 587				b6: function( $el, value ) {
 588					v6 = value;
 589				}
 590			}
 591		}));
 592		
 593		expect( v1 ).toBe( m1 );
 594		expect( v2 ).toBe( m2 );
 595		expect( v3 ).toBe( m3 );
 596		expect( v4 ).toBe( m4 );
 597		expect( v5 ).toBe( "Luke" );
 598		expect( v6 ).toBe( "Han" );
 599	});
 600	
 601	
 602	it("binding 'attr:' should establish a one-way binding with an element's attribute definitions.", function() {
 603		var $el = $(".test-attr-multi");
 604		expect( $el.attr("href") ).toBe( "b" );
 605		expect( $el.attr("title") ).toBe( "b" );
 606		bindingModel.set("preference", "c");
 607		expect( $el.attr("href") ).toBe( "c" );
 608		expect( $el.attr("title") ).toBe( "c" );
 609	});
 610	
 611	
 612	it("binding 'attr:' should allow string property definitions.", function() {
 613		var $el = $(".test-attr");
 614		expect( $el.attr("data-active") ).toBe( "true" );
 615		bindingModel.set("active", false);
 616		expect( $el.attr("data-active") ).toBe( "false" );
 617	});
 618	
 619	
 620	it("binding 'checked:' should establish a two-way binding with a radio group.", function() {
 621		var $a = $(".preference[value='a']");
 622		var $b = $(".preference[value='b']");
 623		expect( $a.prop("checked") ).toBe( false );
 624		expect( $b.prop("checked") ).toBe( true );
 625		$a.prop("checked", true).trigger("change");
 626		expect( bindingModel.get("preference") ).toBe( "a" );
 627	});
 628	
 629	
 630	it("binding 'checked:' should establish a two-way binding between a checkbox and boolean value.", function() {
 631		var $el = $(".test-checked-boolean");
 632		expect( $el.prop("checked") ).toBe( true );
 633		$el.prop("checked", false).trigger("change");
 634		expect( bindingModel.get("active") ).toBe( false );
 635	});
 636	
 637	
 638	it("binding 'checked:' should set a checkbox series based on a model array.", function() {
 639		var $els = $(".check-list");
 640		
 641		// Default: populate based on intial setting:
 642		expect( !!$els.filter("[value='b']" ).prop("checked") ).toBe( true );
 643		expect( !!$els.filter("[value='c']" ).prop("checked") ).toBe( false );
 644		
 645		// Add new selection to the checkbox group:
 646		bindingModel.set("checkList", ["b", "c"]);
 647		expect( !!$els.filter("[value='b']" ).prop("checked") ).toBe( true );
 648		expect( !!$els.filter("[value='c']" ).prop("checked") ).toBe( true );
 649	});
 650	
 651	
 652	it("binding 'checked:' should respond to model changes performed by '.modifyArray'.", function() {
 653		var $els = $(".check-list");
 654		
 655		// Add new selection to the checkbox group:
 656		expect( !!$els.filter("[value='b']" ).prop("checked") ).toBe( true );
 657		expect( !!$els.filter("[value='c']" ).prop("checked") ).toBe( false );
 658		bindingModel.modifyArray("checkList", "push", "c");
 659		expect( !!$els.filter("[value='b']" ).prop("checked") ).toBe( true );
 660		expect( !!$els.filter("[value='c']" ).prop("checked") ).toBe( true );
 661	});
 662	
 663	
 664	it("binding 'checked:' should get a checkbox series formatted as a model array.", function() {
 665		var $els = $(".check-list");
 666		bindingModel.set("checkList", ["b"]);
 667		
 668		// Default: populate based on intial setting:
 669		expect( !!$els.filter("[value='b']" ).prop("checked") ).toBe( true );
 670		$els.filter("[value='a']").prop("checked", true).trigger("change");
 671		expect( bindingModel.get("checkList").join(",") ).toBe( "b,a" );
 672	});
 673	
 674	
 675	it("binding 'classes:' should establish a one-way binding with an element's class definitions.", function() {
 676		var $el = $(".test-classes").eq(0);
 677		expect( $el.hasClass("error") ).toBe( false );
 678		expect( $el.hasClass("active") ).toBe( true );
 679		bindingModel.set({
 680			firstName: "",
 681			active: false
 682		});
 683		expect( $el.hasClass("error") ).toBe( true );
 684		expect( $el.hasClass("active") ).toBe( false );
 685	});
 686	
 687	
 688	it("binding 'collection:' should update display in response Backbone.Collection 'reset' events.", function() {
 689		var $el = $(".test-collection");
 690		
 691		modView.collection.reset([
 692			{name: "Luke Skywalker"}
 693		]);
 694		expect( $el.children().length ).toBe( 1 );
 695		
 696		modView.collection.reset([
 697			{name: "Hans Solo"},
 698			{name: "Chewy"}
 699		]);
 700		expect( $el.children().length ).toBe( 2 );
 701	});
 702
 703	
 704	it("binding 'collection:' should update display in response Backbone.Collection 'add' events.", function() {
 705		var $el = $(".test-collection");
 706		
 707		modView.collection.add({name: "Luke Skywalker"});
 708		expect( $el.children().length ).toBe( 1 );
 709		
 710		modView.collection.add([
 711			{name: "Hans Solo"},
 712			{name: "Chewy"}
 713		]);
 714		expect( $el.children().length ).toBe( 3 );
 715	});
 716	
 717	
 718	it("binding 'collection:' should update display in response Backbone.Collection 'remove' events.", function() {
 719		var $el = $(".test-collection");
 720		
 721		modView.collection.add({name: "Luke Skywalker"});
 722		expect( $el.children().length ).toBe( 1 );
 723		
 724		modView.collection.remove( modView.collection.at(0) );
 725		expect( $el.children().length ).toBe( 0 );
 726	});
 727	
 728	
 729	it("binding 'collection:' should update display in response Backbone.Collection 'sort' events.", function() {
 730		var $el = $(".test-collection");
 731		
 732		modView.collection.reset([
 733			{name: "B"},
 734			{name: "A"}
 735		]);
 736		expect( $el.find(":first-child .name-dsp").text() ).toBe( "B" );
 737		
 738		modView.collection.comparator = function( model ) { return model.get("name"); };
 739		modView.collection.sort();
 740		modView.collection.comparator = null;
 741		
 742		expect( $el.find(":first-child .name-dsp").text() ).toBe( "A" );
 743	});
 744	
 745	
 746	it("binding 'css:' should establish a one-way binding with an element's css styles.", function() {
 747		var $el = $(".test-css");
 748		expect( $el.css("display") ).toBe( "none" );
 749		bindingModel.set( "lastName", "" );
 750		expect( $el.css("display") ).toBe( "block" );
 751	});
 752	
 753	
 754	it("binding 'disabled:' should establish a one-way binding with an element's disabled state.", function() {
 755		var $el = $(".test-disabled");
 756		expect( $el.prop("disabled") ).toBeTruthy();
 757		bindingModel.set( "active", false );
 758		expect( $el.prop("disabled") ).toBeFalsy();
 759	});
 760	
 761	
 762	it("binding 'enabled:' should establish a one-way binding with an element's inverted disabled state.", function() {
 763		var $el = $(".test-enabled");
 764		expect( $el.prop("disabled") ).toBeFalsy();
 765		bindingModel.set( "active", false );
 766		expect( $el.prop("disabled") ).toBeTruthy();
 767	});
 768	
 769	
 770	it("binding 'events:' should configure additional DOM event triggers.", function() {
 771		var $el = $(".test-input-first");
 772		expect( $el.val() ).toBe( "Luke" );
 773		$el.val( "Anakin" ).trigger("keyup");
 774		expect( bindingModel.get("firstName") ).toBe( "Anakin" );
 775	});
 776	
 777	
 778	it("binding 'html:' should establish a one-way binding with an element's html contents.", function() {
 779		var $el = $(".test-html");
 780		// Compare markup as case insensitive to accomodate variances in browser DOM styling:
 781		expect( $el.html() ).toMatch( /<strong>Skywalker<\/strong>, Luke/i );
 782		bindingModel.set("firstName", "Anakin");
 783		expect( $el.html() ).toMatch( /<strong>Skywalker<\/strong>, Anakin/i );
 784	});
 785	
 786	
 787	it("binding 'options:' should bind an array of strings to a select element's options.", function() {
 788		var $el = $(".test-select");
 789		bindingModel.set("optionsList", ["Luke", "Leia"]);
 790		expect( $el.children().length ).toBe( 2 );
 791		expect( $el.find(":first-child").attr("value") ).toBe( "Luke" );
 792		expect( $el.find(":first-child").text() ).toBe( "Luke" );
 793	});
 794	
 795	
 796	it("binding 'options:' should bind an array of label/value pairs to a select element's options.", function() {
 797		var $el = $(".test-select");
 798		bindingModel.set("optionsList", [
 799			{label:"Luke", value:"a"},
 800			{label:"Leia", value:"b"}
 801		]);
 802		
 803		expect( $el.children().length ).toBe( 2 );
 804		expect( $el.find(":first-child").attr("value") ).toBe( "a" );
 805		expect( $el.find(":first-child").text() ).toBe( "Luke" );
 806	});
 807	
 808	
 809	it("binding 'options:' should bind a collection of model label/value attributes to a select element's options.", function() {
 810		var $el = $(".test-select-collect");
 811		modView.collection.reset([
 812			{label:"Luke Skywalker", value:"Luke"},
 813			{label:"Han Solo", value:"Han"}
 814		]);
 815		
 816		expect( $el.children().length ).toBe( 2 );
 817		expect( bindingModel.get("valCollect") ).toBe( "Luke" );
 818	});
 819	
 820	
 821	it("binding 'options:' should update selection when additional items are added/removed.", function() {
 822		var $el = $(".test-select");
 823		bindingModel.modifyArray("optionsList", "push", {label:"Leia", value:"3"});
 824		
 825		expect( $el.children().length ).toBe( 4 );
 826		expect( $el.find(":last-child").attr("value") ).toBe( "3" );
 827		expect( $el.find(":last-child").text() ).toBe( "Leia" );
 828	});
 829	
 830	
 831	it("binding 'options:' should preserve previous selection state after binding.", function() {
 832		var $el = $(".test-select");
 833		bindingModel.modifyArray("optionsList", "push", {label:"Leia", value:"3"});
 834		expect( $el.children().length ).toBe( 4 );
 835		expect( $el.val() ).toBe( "1" );
 836	});
 837	
 838	
 839	it("binding 'options:' should update the bound model value when the previous selection is no longer available.", function() {
 840		var $el = $(".test-select-default");
 841		expect( bindingModel.get("valDefault") ).toBe( "1" );
 842		bindingModel.set("optionsList", []);
 843		expect( bindingModel.get("valDefault") ).toBe( "default" );
 844	});
 845	
 846	
 847	it("binding 'options:' should update a bound multiselect value when the previous selection is no longer available.", function() {
 848		var $el = $(".test-select-multi");
 849		
 850		// Set two options as selected, and confirm they appear within the view:
 851		bindingModel.set("valMulti", ["1", "2"]);
 852		expect( $el.val().join(",") ).toBe( "1,2" );
 853		
 854		// Remove one option from the list, then confirm the model captures the revised selection:
 855		bindingModel.modifyArray("optionsList", "splice", 1, 1);
 856		expect( bindingModel.get("valMulti").join(",") ).toBe( "2" );
 857	});
 858	
 859	
 860	it("binding 'optionsDefault:' should include a default first option in a select menu.", function() {
 861		var $el = $(".test-select-default");
 862		expect( $el.children().length ).toBe( 4 );
 863		expect( $el.find(":first-child").text() ).toBe( "default" );
 864	});
 865	
 866	
 867	it("binding 'optionsDefault:' should bind the default option value to a model.", function() {
 868		var $el = $(".test-select-default");
 869		bindingModel.set("optDefault", {label:"choose...", value:""});
 870		expect( $el.find(":first-child").text() ).toBe( "choose..." );
 871	});
 872	
 873	
 874	it("binding 'optionsEmpty:' should provide a placeholder option value for an empty select.", function() {
 875		var $el = $(".test-select-empty");
 876		expect( $el.children().length ).toBe( 3 );
 877		bindingModel.set("optionsList", []);
 878		expect( $el.children().length ).toBe( 1 );
 879		expect( $el.find(":first-child").text() ).toBe( "empty" );
 880	});
 881	
 882	
 883	it("binding 'optionsEmpty:' should bind the empty placeholder option value to a model.", function() {
 884		var $el = $(".test-select-empty");
 885		bindingModel.set("optionsList", []);
 886		bindingModel.set("optEmpty", {label:"---", value:""});
 887		expect( $el.find(":first-child").text() ).toBe( "---" );
 888	});
 889	
 890	
 891	it("binding 'optionsEmpty:' should disable an empty select menu.", function() {
 892		var $el = $(".test-select-empty");
 893		bindingModel.set("optionsList", []);
 894		expect( $el.prop("disabled") ).toBe( true );
 895	});
 896	
 897	
 898	it("binding 'optionsDefault:' should supersede 'optionsEmpty:' by providing a default item.", function() {
 899		var $el = $(".test-select-both");
 900		
 901		// Empty the list, expect first option to still be the default:
 902		bindingModel.set("optionsList", []);
 903		expect( $el.find(":first-child").text() ).toBe( "default" );
 904		
 905		// Empty the default, now expect the first option to be the empty placeholder.
 906		bindingModel.set("optDefault", "");
 907		expect( $el.find(":first-child").text() ).toBe( "empty" );
 908	});
 909	
 910	
 911	it("binding 'text:' should establish a one-way binding with an element's text contents.", function() {
 912		var $el = $(".test-text-first");
 913		expect( $el.text() ).toBe( "Luke" );
 914		bindingModel.set("firstName", "Anakin");
 915		expect( $el.text() ).toBe( "Anakin" );
 916	});
 917	
 918	
 919	it("binding 'toggle:' should establish a one-way binding with an element's visibility.", function() {
 920		var $el = $(".test-toggle");
 921		expect( $el.is(":visible") ).toBe( true );
 922		bindingModel.set("active", false);
 923		expect( $el.is(":visible") ).toBe( false );
 924	});
 925	
 926	
 927	it("binding 'value:' should set a value from the model into the view.", function() {
 928		var $el = $(".test-input-first");
 929		expect( $el.val() ).toBe( "Luke" );
 930	});
 931	
 932	
 933	it("binding 'value:' should set an array value from the model to a multiselect list.", function() {
 934		var $el = $(".test-select-multi");
 935		expect( $el.val().length ).toBe( 1 );
 936		bindingModel.set("valMulti", ["1", "2"]);
 937		expect( $el.val().length ).toBe( 2 );
 938		expect( $el.val().join(",") ).toBe( "1,2" );
 939	});
 940	
 941	
 942	it("binding 'value:' should set a value from the view into the model.", function() {
 943		var $el = $(".test-input-first");
 944		$el.val( "Anakin" ).trigger("change");
 945		expect( bindingModel.get("firstName") ).toBe( "Anakin" );
 946	});
 947	
 948	
 949	it("operating with not() should negate a binding value.", function() {
 950		var $el = $(".test-mod-not");
 951		expect( $el.is(":visible") ).toBe( false );
 952		bindingModel.set("active", false);
 953		expect( $el.is(":visible") ).toBe( true );
 954	});
 955	
 956	
 957	it("operating with all() should bind true when all bound values are truthy.", function() {
 958		var $el = $(".test-mod-all");
 959		expect( $el.hasClass("hilite") ).toBe( true );
 960		bindingModel.set("firstName", "");
 961		expect( $el.hasClass("hilite") ).toBe( false );
 962	});
 963	
 964	
 965	it("operating with none() should bind true when all bound values are falsy.", function() {
 966		var $el = $(".test-mod-none");
 967		expect( $el.hasClass("hilite") ).toBe( false );
 968		bindingModel.set({
 969			firstName: "",
 970			lastName: ""
 971		});
 972		expect( $el.hasClass("hilite") ).toBe( true );
 973	});
 974	
 975	
 976	it("operating with any() should bind true when any bound value is truthy.", function() {
 977		var $el = $(".test-mod-any");
 978		expect( $el.hasClass("hilite") ).toBe( true );
 979		bindingModel.set("firstName", "");
 980		expect( $el.hasClass("hilite") ).toBe( true );
 981		bindingModel.set("lastName", "");
 982		expect( $el.hasClass("hilite") ).toBe( false );
 983	});
 984	
 985
 986	it("operating with format() should bind true when any bound value is truthy.", function() {
 987		var $el = $(".test-mod-format");
 988		expect( $el.text() ).toBe( "Name: Luke Skywalker" );
 989		bindingModel.set({
 990			firstName: "Han",
 991			lastName: "Solo"
 992		});
 993		expect( $el.text() ).toBe( "Name: Han Solo" );
 994	});
 995	
 996	
 997	it("operating with select() should perform a ternary return from three values.", function() {
 998		var $el = $(".test-mod-select");
 999		expect( $el.text() ).toBe( "Luke" );
1000		bindingModel.set("active", false);
1001		expect( $el.text() ).toBe( "Skywalker" );
1002	});
1003	
1004	
1005	it("operating with length() should assess the length of an array/collection.", function() {
1006		var $el = $(".test-mod-length");
1007		expect( $el.hasClass("hilite") ).toBe( true );
1008		bindingModel.set("checkList", []);
1009		expect( $el.hasClass("hilite") ).toBe( false );
1010	});
1011});