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