observablemixin.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import testUtils from '/tests/ckeditor5/_utils/utils.js';
  7. import utilsTestUtils from '/tests/utils/_utils/utils.js';
  8. import ObservableMixin from '/ckeditor5/utils/observablemixin.js';
  9. import EmitterMixin from '/ckeditor5/utils/emittermixin.js';
  10. import EventInfo from '/ckeditor5/utils/eventinfo.js';
  11. import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
  12. import mix from '/ckeditor5/utils/mix.js';
  13. const assertBinding = utilsTestUtils.assertBinding;
  14. testUtils.createSinonSandbox();
  15. describe( 'ObservableMixin', () => {
  16. it( 'exists', () => {
  17. expect( ObservableMixin ).to.be.an( 'object' );
  18. } );
  19. it( 'mixes in EmitterMixin', () => {
  20. expect( ObservableMixin ).to.have.property( 'on', EmitterMixin.on );
  21. } );
  22. it( 'implements set, bind, and unbind methods', () => {
  23. expect( ObservableMixin ).to.contain.keys( 'set', 'bind', 'unbind' );
  24. } );
  25. } );
  26. describe( 'Observable', () => {
  27. class Observable {
  28. constructor( attrs ) {
  29. if ( attrs ) {
  30. this.set( attrs );
  31. }
  32. }
  33. }
  34. mix( Observable, ObservableMixin );
  35. let Car, car;
  36. beforeEach( () => {
  37. Car = class extends Observable {};
  38. car = new Car( {
  39. color: 'red',
  40. year: 2015
  41. } );
  42. } );
  43. it( 'should set attributes on creation', () => {
  44. expect( car ).to.have.property( 'color', 'red' );
  45. expect( car ).to.have.property( 'year', 2015 );
  46. } );
  47. it( 'should get correctly after set', () => {
  48. car.color = 'blue';
  49. expect( car.color ).to.equal( 'blue' );
  50. } );
  51. describe( 'set', () => {
  52. it( 'should work when passing an object', () => {
  53. car.set( {
  54. color: 'blue', // Override
  55. wheels: 4,
  56. seats: 5
  57. } );
  58. expect( car ).to.deep.equal( {
  59. color: 'blue',
  60. year: 2015,
  61. wheels: 4,
  62. seats: 5
  63. } );
  64. } );
  65. it( 'should work when passing a key/value pair', () => {
  66. car.set( 'color', 'blue' );
  67. car.set( 'wheels', 4 );
  68. expect( car ).to.deep.equal( {
  69. color: 'blue',
  70. year: 2015,
  71. wheels: 4
  72. } );
  73. } );
  74. it( 'should fire the "change" event', () => {
  75. let spy = sinon.spy();
  76. let spyColor = sinon.spy();
  77. let spyYear = sinon.spy();
  78. let spyWheels = sinon.spy();
  79. car.on( 'change', spy );
  80. car.on( 'change:color', spyColor );
  81. car.on( 'change:year', spyYear );
  82. car.on( 'change:wheels', spyWheels );
  83. // Set property in all possible ways.
  84. car.color = 'blue';
  85. car.set( { year: 2003 } );
  86. car.set( 'wheels', 4 );
  87. // Check number of calls.
  88. sinon.assert.calledThrice( spy );
  89. sinon.assert.calledOnce( spyColor );
  90. sinon.assert.calledOnce( spyYear );
  91. sinon.assert.calledOnce( spyWheels );
  92. // Check context.
  93. sinon.assert.alwaysCalledOn( spy, car );
  94. sinon.assert.calledOn( spyColor, car );
  95. sinon.assert.calledOn( spyYear, car );
  96. sinon.assert.calledOn( spyWheels, car );
  97. // Check params.
  98. sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'color', 'blue', 'red' );
  99. sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'year', 2003, 2015 );
  100. sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'wheels', 4, sinon.match.typeOf( 'undefined' ) );
  101. sinon.assert.calledWithExactly( spyColor, sinon.match.instanceOf( EventInfo ), 'color', 'blue', 'red' );
  102. sinon.assert.calledWithExactly( spyYear, sinon.match.instanceOf( EventInfo ), 'year', 2003, 2015 );
  103. sinon.assert.calledWithExactly( spyWheels, sinon.match.instanceOf( EventInfo ), 'wheels', 4, sinon.match.typeOf( 'undefined' ) );
  104. } );
  105. it( 'should not fire the "change" event for the same attribute value', () => {
  106. let spy = sinon.spy();
  107. let spyColor = sinon.spy();
  108. car.on( 'change', spy );
  109. car.on( 'change:color', spyColor );
  110. // Set the "color" property in all possible ways.
  111. car.color = 'red';
  112. car.set( 'color', 'red' );
  113. car.set( { color: 'red' } );
  114. sinon.assert.notCalled( spy );
  115. sinon.assert.notCalled( spyColor );
  116. } );
  117. it( 'should throw when overriding already existing property', () => {
  118. car.normalProperty = 1;
  119. expect( () => {
  120. car.set( 'normalProperty', 2 );
  121. } ).to.throw( CKEditorError, /^observable-set-cannot-override/ );
  122. expect( car ).to.have.property( 'normalProperty', 1 );
  123. } );
  124. it( 'should throw when overriding already existing property (in the prototype)', () => {
  125. class Car extends Observable {
  126. method() {}
  127. }
  128. car = new Car();
  129. expect( () => {
  130. car.set( 'method', 2 );
  131. } ).to.throw( CKEditorError, /^observable-set-cannot-override/ );
  132. expect( car.method ).to.be.a( 'function' );
  133. } );
  134. it( 'should allow setting attributes with undefined value', () => {
  135. let spy = sinon.spy();
  136. car.on( 'change', spy );
  137. car.set( 'seats', undefined );
  138. sinon.assert.calledOnce( spy );
  139. expect( car ).to.contain.keys( 'seats' );
  140. expect( car.seats ).to.be.undefined;
  141. car.set( 'seats', 5 );
  142. sinon.assert.calledTwice( spy );
  143. expect( car ).to.have.property( 'seats', 5 );
  144. } );
  145. } );
  146. describe( 'bind', () => {
  147. it( 'should chain for a single attribute', () => {
  148. expect( car.bind( 'color' ) ).to.contain.keys( 'to' );
  149. } );
  150. it( 'should chain for multiple attributes', () => {
  151. expect( car.bind( 'color', 'year' ) ).to.contain.keys( 'to' );
  152. } );
  153. it( 'should chain for nonexistent attributes', () => {
  154. expect( car.bind( 'nonexistent' ) ).to.contain.keys( 'to' );
  155. } );
  156. it( 'should throw when attributes are not strings', () => {
  157. expect( () => {
  158. car.bind();
  159. } ).to.throw( CKEditorError, /observable-bind-wrong-attrs/ );
  160. expect( () => {
  161. car.bind( new Date() );
  162. } ).to.throw( CKEditorError, /observable-bind-wrong-attrs/ );
  163. expect( () => {
  164. car.bind( 'color', new Date() );
  165. } ).to.throw( CKEditorError, /observable-bind-wrong-attrs/ );
  166. } );
  167. it( 'should throw when the same attribute is used than once', () => {
  168. expect( () => {
  169. car.bind( 'color', 'color' );
  170. } ).to.throw( CKEditorError, /observable-bind-duplicate-attrs/ );
  171. } );
  172. it( 'should throw when binding the same attribute more than once', () => {
  173. expect( () => {
  174. car.bind( 'color' );
  175. car.bind( 'color' );
  176. } ).to.throw( CKEditorError, /observable-bind-rebind/ );
  177. } );
  178. describe( 'to', () => {
  179. it( 'should not chain', () => {
  180. expect(
  181. car.bind( 'color' ).to( new Observable( { color: 'red' } ) )
  182. ).to.be.undefined;
  183. } );
  184. it( 'should throw when arguments are of invalid type - empty', () => {
  185. expect( () => {
  186. car = new Car();
  187. car.bind( 'color' ).to();
  188. } ).to.throw( CKEditorError, /observable-bind-to-parse-error/ );
  189. } );
  190. it( 'should throw when binding multiple attributes to multiple observables', () => {
  191. let vehicle = new Car();
  192. const car1 = new Car( { color: 'red', year: 1943 } );
  193. const car2 = new Car( { color: 'yellow', year: 1932 } );
  194. expect( () => {
  195. vehicle.bind( 'color', 'year' ).to( car1, 'color', car2, 'year' );
  196. } ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
  197. expect( () => {
  198. vehicle = new Car();
  199. vehicle.bind( 'color', 'year' ).to( car1, car2 );
  200. } ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
  201. expect( () => {
  202. vehicle = new Car();
  203. vehicle.bind( 'color', 'year' ).to( car1, car2, 'year' );
  204. } ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
  205. expect( () => {
  206. vehicle = new Car();
  207. vehicle.bind( 'color', 'year' ).to( car1, 'color', car2 );
  208. } ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
  209. expect( () => {
  210. vehicle = new Car();
  211. vehicle.bind( 'color', 'year', 'custom' ).to( car, car );
  212. } ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
  213. } );
  214. it( 'should throw when binding multiple attributes but passed a callback', () => {
  215. let vehicle = new Car();
  216. expect( () => {
  217. vehicle.bind( 'color', 'year' ).to( car, () => {} );
  218. } ).to.throw( CKEditorError, /observable-bind-to-extra-callback/ );
  219. expect( () => {
  220. vehicle = new Car();
  221. vehicle.bind( 'color', 'year' ).to( car, car, () => {} );
  222. } ).to.throw( CKEditorError, /observable-bind-to-extra-callback/ );
  223. } );
  224. it( 'should throw when binding a single attribute but multiple callbacks', () => {
  225. let vehicle = new Car();
  226. expect( () => {
  227. vehicle.bind( 'color' ).to( car, () => {}, () => {} );
  228. } ).to.throw( CKEditorError, /observable-bind-to-parse-error/ );
  229. expect( () => {
  230. vehicle = new Car();
  231. vehicle.bind( 'color' ).to( car, car, () => {}, () => {} );
  232. } ).to.throw( CKEditorError, /observable-bind-to-parse-error/ );
  233. } );
  234. it( 'should throw when a number of attributes does not match', () => {
  235. let vehicle = new Car();
  236. expect( () => {
  237. vehicle.bind( 'color' ).to( car, 'color', 'year' );
  238. } ).to.throw( CKEditorError, /observable-bind-to-attrs-length/ );
  239. expect( () => {
  240. vehicle = new Car();
  241. vehicle.bind( 'color', 'year' ).to( car, 'color' );
  242. } ).to.throw( CKEditorError, /observable-bind-to-attrs-length/ );
  243. expect( () => {
  244. vehicle = new Car();
  245. vehicle.bind( 'color' ).to( car, 'color', 'year', () => {} );
  246. } ).to.throw( CKEditorError, /observable-bind-to-attrs-length/ );
  247. expect( () => {
  248. vehicle = new Car();
  249. vehicle.bind( 'color' ).to( car, 'color', car, 'color', 'year', () => {} );
  250. } ).to.throw( CKEditorError, /observable-bind-to-attrs-length/ );
  251. } );
  252. it( 'should throw when attributes don\'t exist in to() observable', () => {
  253. const vehicle = new Car();
  254. expect( () => {
  255. vehicle.bind( 'color' ).to( car, 'nonexistent in car' );
  256. } ).to.throw( CKEditorError, /observable-bind-to-missing-attr/ );
  257. expect( () => {
  258. vehicle.bind( 'nonexistent in car' ).to( car );
  259. } ).to.throw( CKEditorError, /observable-bind-to-missing-attr/ );
  260. expect( () => {
  261. vehicle.bind( 'year' ).to( car, 'color', car, 'nonexistent in car', () => {} );
  262. } ).to.throw( CKEditorError, /observable-bind-to-missing-attr/ );
  263. } );
  264. it( 'should set new observable attributes', () => {
  265. const car = new Car( { color: 'green', year: 2001, type: 'pickup' } );
  266. const vehicle = new Car( { 'not involved': true } );
  267. vehicle.bind( 'color', 'year', 'type' ).to( car );
  268. expect( vehicle ).to.have.property( 'color' );
  269. expect( vehicle ).to.have.property( 'year' );
  270. expect( vehicle ).to.have.property( 'type' );
  271. expect( vehicle ).to.have.property( 'not involved' );
  272. } );
  273. it( 'should work when no attribute specified #1', () => {
  274. const vehicle = new Car();
  275. vehicle.bind( 'color' ).to( car );
  276. assertBinding( vehicle,
  277. { color: car.color, year: undefined },
  278. [
  279. [ car, { color: 'blue', year: 1969 } ]
  280. ],
  281. { color: 'blue', year: undefined }
  282. );
  283. } );
  284. it( 'should work for a single attribute', () => {
  285. const vehicle = new Car();
  286. vehicle.bind( 'color' ).to( car, 'color' );
  287. assertBinding( vehicle,
  288. { color: car.color, year: undefined },
  289. [
  290. [ car, { color: 'blue', year: 1969 } ]
  291. ],
  292. { color: 'blue', year: undefined }
  293. );
  294. } );
  295. it( 'should work for multiple attributes', () => {
  296. const vehicle = new Car();
  297. vehicle.bind( 'color', 'year' ).to( car, 'color', 'year' );
  298. assertBinding( vehicle,
  299. { color: car.color, year: car.year },
  300. [
  301. [ car, { color: 'blue', year: 1969 } ]
  302. ],
  303. { color: 'blue', year: 1969 }
  304. );
  305. } );
  306. it( 'should work for attributes that don\'t exist in the observable', () => {
  307. const vehicle = new Car();
  308. vehicle.bind( 'nonexistent in vehicle' ).to( car, 'color' );
  309. assertBinding( vehicle,
  310. { 'nonexistent in vehicle': car.color, color: undefined },
  311. [
  312. [ car, { color: 'blue', year: 1969 } ]
  313. ],
  314. { 'nonexistent in vehicle': 'blue', color: undefined }
  315. );
  316. } );
  317. it( 'should work when using the same attribute name more than once', () => {
  318. const vehicle = new Car();
  319. vehicle.bind( 'color', 'year' ).to( car, 'year', 'year' );
  320. assertBinding( vehicle,
  321. { color: car.year, year: car.year },
  322. [
  323. [ car, { color: 'blue', year: 1969 } ]
  324. ],
  325. { color: 1969, year: 1969 }
  326. );
  327. } );
  328. it( 'should work when binding more that once', () => {
  329. const vehicle = new Car();
  330. vehicle.bind( 'color' ).to( car, 'color' );
  331. vehicle.bind( 'year' ).to( car, 'year' );
  332. assertBinding( vehicle,
  333. { color: car.color, year: car.year },
  334. [
  335. [ car, { color: 'blue', year: 1969 } ]
  336. ],
  337. { color: 'blue', year: 1969 }
  338. );
  339. } );
  340. it( 'should work with callback – set a new observable attribute', () => {
  341. const vehicle = new Car();
  342. const car1 = new Car( { type: 'pickup' } );
  343. const car2 = new Car( { type: 'truck' } );
  344. vehicle.bind( 'type' )
  345. .to( car1, car2, ( ...args ) => args.join( '' ) );
  346. expect( vehicle ).to.have.property( 'type' );
  347. } );
  348. it( 'should work with callback #1', () => {
  349. const vehicle = new Car();
  350. const car1 = new Car( { color: 'black' } );
  351. const car2 = new Car( { color: 'brown' } );
  352. vehicle.bind( 'color' )
  353. .to( car1, car2, ( ...args ) => args.join( '' ) );
  354. assertBinding( vehicle,
  355. { color: car1.color + car2.color, year: undefined },
  356. [
  357. [ car1, { color: 'black', year: 1930 } ],
  358. [ car2, { color: 'green', year: 1950 } ]
  359. ],
  360. { color: 'blackgreen', year: undefined }
  361. );
  362. } );
  363. it( 'should work with callback #2', () => {
  364. const vehicle = new Car();
  365. const car1 = new Car( { color: 'black' } );
  366. const car2 = new Car( { color: 'brown' } );
  367. vehicle.bind( 'color' )
  368. .to( car1, 'color', car2, 'color', ( ...args ) => args.join( '' ) );
  369. assertBinding( vehicle,
  370. { color: car1.color + car2.color, year: undefined },
  371. [
  372. [ car1, { color: 'black', year: 1930 } ],
  373. [ car2, { color: 'green', year: 1950 } ]
  374. ],
  375. { color: 'blackgreen', year: undefined }
  376. );
  377. } );
  378. it( 'should work with callback #3', () => {
  379. const vehicle = new Car();
  380. const car1 = new Car( { color: 'black' } );
  381. const car2 = new Car( { color: 'brown' } );
  382. const car3 = new Car( { color: 'yellow' } );
  383. vehicle.bind( 'color' )
  384. .to( car1, car2, car3, ( ...args ) => args.join( '' ) );
  385. assertBinding( vehicle,
  386. { color: car1.color + car2.color + car3.color, year: undefined },
  387. [
  388. [ car1, { color: 'black', year: 1930 } ],
  389. [ car2, { color: 'green', year: 1950 } ]
  390. ],
  391. { color: 'blackgreenyellow', year: undefined }
  392. );
  393. } );
  394. it( 'should work with callback #4', () => {
  395. const vehicle = new Car();
  396. const car1 = new Car( { color: 'black' } );
  397. const car2 = new Car( { lightness: 'bright' } );
  398. const car3 = new Car( { color: 'yellow' } );
  399. vehicle.bind( 'color' )
  400. .to( car1, car2, 'lightness', car3, ( ...args ) => args.join( '' ) );
  401. assertBinding( vehicle,
  402. { color: car1.color + car2.lightness + car3.color, year: undefined },
  403. [
  404. [ car1, { color: 'black', year: 1930 } ],
  405. [ car2, { color: 'green', year: 1950 } ]
  406. ],
  407. { color: 'blackbrightyellow', year: undefined }
  408. );
  409. } );
  410. it( 'should work with callback #5', () => {
  411. const vehicle = new Car();
  412. const car1 = new Car( { hue: 'reds' } );
  413. const car2 = new Car( { lightness: 'bright' } );
  414. vehicle.bind( 'color' )
  415. .to( car1, 'hue', car2, 'lightness', ( ...args ) => args.join( '' ) );
  416. assertBinding( vehicle,
  417. { color: car1.hue + car2.lightness, year: undefined },
  418. [
  419. [ car1, { hue: 'greens', year: 1930 } ],
  420. [ car2, { lightness: 'dark', year: 1950 } ]
  421. ],
  422. { color: 'greensdark', year: undefined }
  423. );
  424. } );
  425. it( 'should work with callback #6', () => {
  426. const vehicle = new Car();
  427. const car1 = new Car( { hue: 'reds' } );
  428. vehicle.bind( 'color' )
  429. .to( car1, 'hue', ( h ) => h.toUpperCase() );
  430. assertBinding( vehicle,
  431. { color: car1.hue.toUpperCase(), year: undefined },
  432. [
  433. [ car1, { hue: 'greens', year: 1930 } ]
  434. ],
  435. { color: 'GREENS', year: undefined }
  436. );
  437. } );
  438. it( 'should work with callback #7', () => {
  439. const vehicle = new Car();
  440. const car1 = new Car( { color: 'red', year: 1943 } );
  441. const car2 = new Car( { color: 'yellow', year: 1932 } );
  442. vehicle.bind( 'custom' )
  443. .to( car1, 'color', car2, 'year', ( ...args ) => args.join( '/' ) );
  444. assertBinding( vehicle,
  445. { color: undefined, year: undefined, 'custom': car1.color + '/' + car2.year },
  446. [
  447. [ car1, { color: 'blue', year: 2100 } ],
  448. [ car2, { color: 'violet', year: 1969 } ]
  449. ],
  450. { color: undefined, year: undefined, 'custom': 'blue/1969' }
  451. );
  452. } );
  453. it( 'should work with callback #8', () => {
  454. const vehicle = new Car();
  455. const car1 = new Car( { color: 'red', year: 1943 } );
  456. const car2 = new Car( { color: 'yellow', year: 1932 } );
  457. const car3 = new Car( { hue: 'reds' } );
  458. vehicle.bind( 'custom' )
  459. .to( car1, 'color', car2, 'year', car3, 'hue', ( ...args ) => args.join( '/' ) );
  460. assertBinding( vehicle,
  461. { color: undefined, year: undefined, hue: undefined, 'custom': car1.color + '/' + car2.year + '/' + car3.hue },
  462. [
  463. [ car1, { color: 'blue', year: 2100 } ],
  464. [ car2, { color: 'violet', year: 1969 } ]
  465. ],
  466. { color: undefined, year: undefined, hue: undefined, 'custom': 'blue/1969/reds' }
  467. );
  468. } );
  469. it( 'should work with callback – binding more that once #1', () => {
  470. const vehicle = new Car();
  471. const car1 = new Car( { hue: 'reds', produced: 1920 } );
  472. const car2 = new Car( { lightness: 'bright', sold: 1921 } );
  473. vehicle.bind( 'color' )
  474. .to( car1, 'hue', car2, 'lightness', ( ...args ) => args.join( '' ) );
  475. vehicle.bind( 'year' )
  476. .to( car1, 'produced', car2, 'sold', ( ...args ) => args.join( '/' ) );
  477. assertBinding( vehicle,
  478. { color: car1.hue + car2.lightness, year: car1.produced + '/' + car2.sold },
  479. [
  480. [ car1, { hue: 'greens', produced: 1930 } ],
  481. [ car2, { lightness: 'dark', sold: 2000 } ]
  482. ],
  483. { color: 'greensdark', year: '1930/2000' }
  484. );
  485. } );
  486. it( 'should work with callback – binding more that once #2', () => {
  487. const vehicle = new Car();
  488. const car1 = new Car( { hue: 'reds', produced: 1920 } );
  489. const car2 = new Car( { lightness: 'bright', sold: 1921 } );
  490. vehicle.bind( 'color' )
  491. .to( car1, 'hue', car2, 'lightness', ( ...args ) => args.join( '' ) );
  492. vehicle.bind( 'year' )
  493. .to( car1, 'produced', car2, 'sold', ( ...args ) => args.join( '/' ) );
  494. vehicle.bind( 'mix' )
  495. .to( car1, 'hue', car2, 'sold', ( ...args ) => args.join( '+' ) );
  496. assertBinding( vehicle,
  497. {
  498. color: car1.hue + car2.lightness,
  499. year: car1.produced + '/' + car2.sold,
  500. mix: car1.hue + '+' + car2.sold
  501. },
  502. [
  503. [ car1, { hue: 'greens', produced: 1930 } ],
  504. [ car2, { lightness: 'dark', sold: 2000 } ]
  505. ],
  506. {
  507. color: 'greensdark',
  508. year: '1930/2000',
  509. mix: 'greens+2000'
  510. }
  511. );
  512. } );
  513. it( 'should work with callback – binding more that once #3', () => {
  514. const vehicle = new Car();
  515. const car1 = new Car( { hue: 'reds', produced: 1920 } );
  516. const car2 = new Car( { lightness: 'bright', sold: 1921 } );
  517. vehicle.bind( 'color' )
  518. .to( car1, 'hue', car2, 'lightness', ( ...args ) => args.join( '' ) );
  519. vehicle.bind( 'custom1' ).to( car1, 'hue' );
  520. vehicle.bind( 'year' )
  521. .to( car1, 'produced', car2, 'sold', ( ...args ) => args.join( '/' ) );
  522. vehicle.bind( 'custom2', 'custom3' ).to( car1, 'produced', 'hue' );
  523. assertBinding( vehicle,
  524. {
  525. color: car1.hue + car2.lightness,
  526. year: car1.produced + '/' + car2.sold,
  527. custom1: car1.hue,
  528. custom2: car1.produced,
  529. custom3: car1.hue
  530. },
  531. [
  532. [ car1, { hue: 'greens', produced: 1930 } ],
  533. [ car2, { lightness: 'dark', sold: 2000 } ]
  534. ],
  535. {
  536. color: 'greensdark',
  537. year: '1930/2000',
  538. custom1: 'greens',
  539. custom2: 1930,
  540. custom3: 'greens'
  541. }
  542. );
  543. } );
  544. it( 'should fire a single change event per bound attribute', () => {
  545. const vehicle = new Car();
  546. const car = new Car( { color: 'red', year: 1943 } );
  547. const spy = sinon.spy();
  548. vehicle.on( 'change', spy );
  549. vehicle.bind( 'color', 'year' ).to( car );
  550. car.color = 'violet';
  551. car.custom = 'foo';
  552. car.year = 2001;
  553. expect( spy.args.map( args => args[ 1 ] ) )
  554. .to.have.members( [ 'color', 'year', 'color', 'year' ] );
  555. } );
  556. } );
  557. } );
  558. describe( 'unbind', () => {
  559. it( 'should not fail when unbinding a fresh observable', () => {
  560. const observable = new Observable();
  561. observable.unbind();
  562. } );
  563. it( 'should throw when non-string attribute is passed', () => {
  564. expect( () => {
  565. car.unbind( new Date() );
  566. } ).to.throw( CKEditorError, /observable-unbind-wrong-attrs/ );
  567. } );
  568. it( 'should remove all bindings', () => {
  569. const vehicle = new Car();
  570. vehicle.bind( 'color', 'year' ).to( car, 'color', 'year' );
  571. vehicle.unbind();
  572. assertBinding( vehicle,
  573. { color: 'red', year: 2015 },
  574. [
  575. [ car, { color: 'blue', year: 1969 } ]
  576. ],
  577. { color: 'red', year: 2015 }
  578. );
  579. } );
  580. it( 'should remove bindings of certain attributes', () => {
  581. const vehicle = new Car();
  582. const car = new Car( { color: 'red', year: 2000, torque: 160 } );
  583. vehicle.bind( 'color', 'year', 'torque' ).to( car );
  584. vehicle.unbind( 'year', 'torque' );
  585. assertBinding( vehicle,
  586. { color: 'red', year: 2000, torque: 160 },
  587. [
  588. [ car, { color: 'blue', year: 1969, torque: 220 } ]
  589. ],
  590. { color: 'blue', year: 2000, torque: 160 }
  591. );
  592. } );
  593. it( 'should remove bindings of certain attributes, callback', () => {
  594. const vehicle = new Car();
  595. const car1 = new Car( { color: 'red' } );
  596. const car2 = new Car( { color: 'blue' } );
  597. vehicle.bind( 'color' ).to( car1, car2, ( c1, c2 ) => c1 + c2 );
  598. vehicle.unbind( 'color' );
  599. assertBinding( vehicle,
  600. { color: 'redblue' },
  601. [
  602. [ car1, { color: 'green' } ],
  603. [ car2, { color: 'violet' } ]
  604. ],
  605. { color: 'redblue' }
  606. );
  607. } );
  608. it( 'should be able to unbind two attributes from a single source observable attribute', () => {
  609. const vehicle = new Car();
  610. vehicle.bind( 'color' ).to( car, 'color' );
  611. vehicle.bind( 'interiorColor' ).to( car, 'color' );
  612. vehicle.unbind( 'color' );
  613. vehicle.unbind( 'interiorColor' );
  614. assertBinding( vehicle,
  615. { color: 'red', interiorColor: 'red' },
  616. [
  617. [ car, { color: 'blue' } ]
  618. ],
  619. { color: 'red', interiorColor: 'red' }
  620. );
  621. } );
  622. } );
  623. } );