8
0

observablemixin.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
  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 work when attributes don\'t exist in to() observable #1', () => {
  253. const vehicle = new Car();
  254. vehicle.bind( 'color' ).to( car, 'nonexistent in car' );
  255. assertBinding( vehicle,
  256. { color: undefined, year: undefined },
  257. [
  258. [ car, { 'nonexistent in car': 'foo', year: 1969 } ]
  259. ],
  260. { color: 'foo', year: undefined }
  261. );
  262. } );
  263. it( 'should work when attributes don\'t exist in to() observable #2', () => {
  264. const vehicle = new Car();
  265. vehicle.bind( 'nonexistent in car' ).to( car );
  266. assertBinding( vehicle,
  267. { 'nonexistent in car': undefined, year: undefined },
  268. [
  269. [ car, { 'nonexistent in car': 'foo', year: 1969 } ]
  270. ],
  271. { 'nonexistent in car': 'foo', year: undefined }
  272. );
  273. } );
  274. it( 'should work when attributes don\'t exist in to() observable #3', () => {
  275. const vehicle = new Car();
  276. vehicle.bind( 'year' ).to( car, 'color', car, 'nonexistent in car', ( a, b ) => a + b );
  277. assertBinding( vehicle,
  278. { color: undefined, year: car.color + undefined },
  279. [
  280. [ car, { color: 'blue', year: 1969 } ]
  281. ],
  282. { color: undefined, year: 'blueundefined' }
  283. );
  284. } );
  285. it( 'should set new observable attributes', () => {
  286. const car = new Car( { color: 'green', year: 2001, type: 'pickup' } );
  287. const vehicle = new Car( { 'not involved': true } );
  288. vehicle.bind( 'color', 'year', 'type' ).to( car );
  289. expect( vehicle ).to.have.property( 'color' );
  290. expect( vehicle ).to.have.property( 'year' );
  291. expect( vehicle ).to.have.property( 'type' );
  292. expect( vehicle ).to.have.property( 'not involved' );
  293. } );
  294. it( 'should work when no attribute specified #1', () => {
  295. const vehicle = new Car();
  296. vehicle.bind( 'color' ).to( car );
  297. assertBinding( vehicle,
  298. { color: car.color, year: undefined },
  299. [
  300. [ car, { color: 'blue', year: 1969 } ]
  301. ],
  302. { color: 'blue', year: undefined }
  303. );
  304. } );
  305. it( 'should work for a single attribute', () => {
  306. const vehicle = new Car();
  307. vehicle.bind( 'color' ).to( car, 'color' );
  308. assertBinding( vehicle,
  309. { color: car.color, year: undefined },
  310. [
  311. [ car, { color: 'blue', year: 1969 } ]
  312. ],
  313. { color: 'blue', year: undefined }
  314. );
  315. } );
  316. it( 'should work for multiple attributes', () => {
  317. const vehicle = new Car();
  318. vehicle.bind( 'color', 'year' ).to( car, 'color', 'year' );
  319. assertBinding( vehicle,
  320. { color: car.color, year: car.year },
  321. [
  322. [ car, { color: 'blue', year: 1969 } ]
  323. ],
  324. { color: 'blue', year: 1969 }
  325. );
  326. } );
  327. it( 'should work for attributes that don\'t exist in the observable', () => {
  328. const vehicle = new Car();
  329. vehicle.bind( 'nonexistent in vehicle' ).to( car, 'color' );
  330. assertBinding( vehicle,
  331. { 'nonexistent in vehicle': car.color, color: undefined },
  332. [
  333. [ car, { color: 'blue', year: 1969 } ]
  334. ],
  335. { 'nonexistent in vehicle': 'blue', color: undefined }
  336. );
  337. } );
  338. it( 'should work when using the same attribute name more than once', () => {
  339. const vehicle = new Car();
  340. vehicle.bind( 'color', 'year' ).to( car, 'year', 'year' );
  341. assertBinding( vehicle,
  342. { color: car.year, year: car.year },
  343. [
  344. [ car, { color: 'blue', year: 1969 } ]
  345. ],
  346. { color: 1969, year: 1969 }
  347. );
  348. } );
  349. it( 'should work when binding more that once', () => {
  350. const vehicle = new Car();
  351. vehicle.bind( 'color' ).to( car, 'color' );
  352. vehicle.bind( 'year' ).to( car, 'year' );
  353. assertBinding( vehicle,
  354. { color: car.color, year: car.year },
  355. [
  356. [ car, { color: 'blue', year: 1969 } ]
  357. ],
  358. { color: 'blue', year: 1969 }
  359. );
  360. } );
  361. it( 'should work with callback – set a new observable attribute', () => {
  362. const vehicle = new Car();
  363. const car1 = new Car( { type: 'pickup' } );
  364. const car2 = new Car( { type: 'truck' } );
  365. vehicle.bind( 'type' )
  366. .to( car1, car2, ( ...args ) => args.join( '' ) );
  367. expect( vehicle ).to.have.property( 'type' );
  368. } );
  369. it( 'should work with callback #1', () => {
  370. const vehicle = new Car();
  371. const car1 = new Car( { color: 'black' } );
  372. const car2 = new Car( { color: 'brown' } );
  373. vehicle.bind( 'color' )
  374. .to( car1, car2, ( ...args ) => args.join( '' ) );
  375. assertBinding( vehicle,
  376. { color: car1.color + car2.color, year: undefined },
  377. [
  378. [ car1, { color: 'black', year: 1930 } ],
  379. [ car2, { color: 'green', year: 1950 } ]
  380. ],
  381. { color: 'blackgreen', year: undefined }
  382. );
  383. } );
  384. it( 'should work with callback #2', () => {
  385. const vehicle = new Car();
  386. const car1 = new Car( { color: 'black' } );
  387. const car2 = new Car( { color: 'brown' } );
  388. vehicle.bind( 'color' )
  389. .to( car1, 'color', car2, 'color', ( ...args ) => args.join( '' ) );
  390. assertBinding( vehicle,
  391. { color: car1.color + car2.color, year: undefined },
  392. [
  393. [ car1, { color: 'black', year: 1930 } ],
  394. [ car2, { color: 'green', year: 1950 } ]
  395. ],
  396. { color: 'blackgreen', year: undefined }
  397. );
  398. } );
  399. it( 'should work with callback #3', () => {
  400. const vehicle = new Car();
  401. const car1 = new Car( { color: 'black' } );
  402. const car2 = new Car( { color: 'brown' } );
  403. const car3 = new Car( { color: 'yellow' } );
  404. vehicle.bind( 'color' )
  405. .to( car1, car2, car3, ( ...args ) => args.join( '' ) );
  406. assertBinding( vehicle,
  407. { color: car1.color + car2.color + car3.color, year: undefined },
  408. [
  409. [ car1, { color: 'black', year: 1930 } ],
  410. [ car2, { color: 'green', year: 1950 } ]
  411. ],
  412. { color: 'blackgreenyellow', year: undefined }
  413. );
  414. } );
  415. it( 'should work with callback #4', () => {
  416. const vehicle = new Car();
  417. const car1 = new Car( { color: 'black' } );
  418. const car2 = new Car( { lightness: 'bright' } );
  419. const car3 = new Car( { color: 'yellow' } );
  420. vehicle.bind( 'color' )
  421. .to( car1, car2, 'lightness', car3, ( ...args ) => args.join( '' ) );
  422. assertBinding( vehicle,
  423. { color: car1.color + car2.lightness + car3.color, year: undefined },
  424. [
  425. [ car1, { color: 'black', year: 1930 } ],
  426. [ car2, { color: 'green', year: 1950 } ]
  427. ],
  428. { color: 'blackbrightyellow', year: undefined }
  429. );
  430. } );
  431. it( 'should work with callback #5', () => {
  432. const vehicle = new Car();
  433. const car1 = new Car( { hue: 'reds' } );
  434. const car2 = new Car( { lightness: 'bright' } );
  435. vehicle.bind( 'color' )
  436. .to( car1, 'hue', car2, 'lightness', ( ...args ) => args.join( '' ) );
  437. assertBinding( vehicle,
  438. { color: car1.hue + car2.lightness, year: undefined },
  439. [
  440. [ car1, { hue: 'greens', year: 1930 } ],
  441. [ car2, { lightness: 'dark', year: 1950 } ]
  442. ],
  443. { color: 'greensdark', year: undefined }
  444. );
  445. } );
  446. it( 'should work with callback #6', () => {
  447. const vehicle = new Car();
  448. const car1 = new Car( { hue: 'reds' } );
  449. vehicle.bind( 'color' )
  450. .to( car1, 'hue', ( h ) => h.toUpperCase() );
  451. assertBinding( vehicle,
  452. { color: car1.hue.toUpperCase(), year: undefined },
  453. [
  454. [ car1, { hue: 'greens', year: 1930 } ]
  455. ],
  456. { color: 'GREENS', year: undefined }
  457. );
  458. } );
  459. it( 'should work with callback #7', () => {
  460. const vehicle = new Car();
  461. const car1 = new Car( { color: 'red', year: 1943 } );
  462. const car2 = new Car( { color: 'yellow', year: 1932 } );
  463. vehicle.bind( 'custom' )
  464. .to( car1, 'color', car2, 'year', ( ...args ) => args.join( '/' ) );
  465. assertBinding( vehicle,
  466. { color: undefined, year: undefined, 'custom': car1.color + '/' + car2.year },
  467. [
  468. [ car1, { color: 'blue', year: 2100 } ],
  469. [ car2, { color: 'violet', year: 1969 } ]
  470. ],
  471. { color: undefined, year: undefined, 'custom': 'blue/1969' }
  472. );
  473. } );
  474. it( 'should work with callback #8', () => {
  475. const vehicle = new Car();
  476. const car1 = new Car( { color: 'red', year: 1943 } );
  477. const car2 = new Car( { color: 'yellow', year: 1932 } );
  478. const car3 = new Car( { hue: 'reds' } );
  479. vehicle.bind( 'custom' )
  480. .to( car1, 'color', car2, 'year', car3, 'hue', ( ...args ) => args.join( '/' ) );
  481. assertBinding( vehicle,
  482. { color: undefined, year: undefined, hue: undefined, 'custom': car1.color + '/' + car2.year + '/' + car3.hue },
  483. [
  484. [ car1, { color: 'blue', year: 2100 } ],
  485. [ car2, { color: 'violet', year: 1969 } ]
  486. ],
  487. { color: undefined, year: undefined, hue: undefined, 'custom': 'blue/1969/reds' }
  488. );
  489. } );
  490. it( 'should work with callback – binding more that once #1', () => {
  491. const vehicle = new Car();
  492. const car1 = new Car( { hue: 'reds', produced: 1920 } );
  493. const car2 = new Car( { lightness: 'bright', sold: 1921 } );
  494. vehicle.bind( 'color' )
  495. .to( car1, 'hue', car2, 'lightness', ( ...args ) => args.join( '' ) );
  496. vehicle.bind( 'year' )
  497. .to( car1, 'produced', car2, 'sold', ( ...args ) => args.join( '/' ) );
  498. assertBinding( vehicle,
  499. { color: car1.hue + car2.lightness, year: car1.produced + '/' + car2.sold },
  500. [
  501. [ car1, { hue: 'greens', produced: 1930 } ],
  502. [ car2, { lightness: 'dark', sold: 2000 } ]
  503. ],
  504. { color: 'greensdark', year: '1930/2000' }
  505. );
  506. } );
  507. it( 'should work with callback – binding more that once #2', () => {
  508. const vehicle = new Car();
  509. const car1 = new Car( { hue: 'reds', produced: 1920 } );
  510. const car2 = new Car( { lightness: 'bright', sold: 1921 } );
  511. vehicle.bind( 'color' )
  512. .to( car1, 'hue', car2, 'lightness', ( ...args ) => args.join( '' ) );
  513. vehicle.bind( 'year' )
  514. .to( car1, 'produced', car2, 'sold', ( ...args ) => args.join( '/' ) );
  515. vehicle.bind( 'mix' )
  516. .to( car1, 'hue', car2, 'sold', ( ...args ) => args.join( '+' ) );
  517. assertBinding( vehicle,
  518. {
  519. color: car1.hue + car2.lightness,
  520. year: car1.produced + '/' + car2.sold,
  521. mix: car1.hue + '+' + car2.sold
  522. },
  523. [
  524. [ car1, { hue: 'greens', produced: 1930 } ],
  525. [ car2, { lightness: 'dark', sold: 2000 } ]
  526. ],
  527. {
  528. color: 'greensdark',
  529. year: '1930/2000',
  530. mix: 'greens+2000'
  531. }
  532. );
  533. } );
  534. it( 'should work with callback – binding more that once #3', () => {
  535. const vehicle = new Car();
  536. const car1 = new Car( { hue: 'reds', produced: 1920 } );
  537. const car2 = new Car( { lightness: 'bright', sold: 1921 } );
  538. vehicle.bind( 'color' )
  539. .to( car1, 'hue', car2, 'lightness', ( ...args ) => args.join( '' ) );
  540. vehicle.bind( 'custom1' ).to( car1, 'hue' );
  541. vehicle.bind( 'year' )
  542. .to( car1, 'produced', car2, 'sold', ( ...args ) => args.join( '/' ) );
  543. vehicle.bind( 'custom2', 'custom3' ).to( car1, 'produced', 'hue' );
  544. assertBinding( vehicle,
  545. {
  546. color: car1.hue + car2.lightness,
  547. year: car1.produced + '/' + car2.sold,
  548. custom1: car1.hue,
  549. custom2: car1.produced,
  550. custom3: car1.hue
  551. },
  552. [
  553. [ car1, { hue: 'greens', produced: 1930 } ],
  554. [ car2, { lightness: 'dark', sold: 2000 } ]
  555. ],
  556. {
  557. color: 'greensdark',
  558. year: '1930/2000',
  559. custom1: 'greens',
  560. custom2: 1930,
  561. custom3: 'greens'
  562. }
  563. );
  564. } );
  565. it( 'should fire a single change event per bound attribute', () => {
  566. const vehicle = new Car();
  567. const car = new Car( { color: 'red', year: 1943 } );
  568. const spy = sinon.spy();
  569. vehicle.on( 'change', spy );
  570. vehicle.bind( 'color', 'year' ).to( car );
  571. car.color = 'violet';
  572. car.custom = 'foo';
  573. car.year = 2001;
  574. expect( spy.args.map( args => args[ 1 ] ) )
  575. .to.have.members( [ 'color', 'year', 'color', 'year' ] );
  576. } );
  577. } );
  578. } );
  579. describe( 'unbind', () => {
  580. it( 'should not fail when unbinding a fresh observable', () => {
  581. const observable = new Observable();
  582. observable.unbind();
  583. } );
  584. it( 'should throw when non-string attribute is passed', () => {
  585. expect( () => {
  586. car.unbind( new Date() );
  587. } ).to.throw( CKEditorError, /observable-unbind-wrong-attrs/ );
  588. } );
  589. it( 'should remove all bindings', () => {
  590. const vehicle = new Car();
  591. vehicle.bind( 'color', 'year' ).to( car, 'color', 'year' );
  592. vehicle.unbind();
  593. assertBinding( vehicle,
  594. { color: 'red', year: 2015 },
  595. [
  596. [ car, { color: 'blue', year: 1969 } ]
  597. ],
  598. { color: 'red', year: 2015 }
  599. );
  600. } );
  601. it( 'should remove bindings of certain attributes', () => {
  602. const vehicle = new Car();
  603. const car = new Car( { color: 'red', year: 2000, torque: 160 } );
  604. vehicle.bind( 'color', 'year', 'torque' ).to( car );
  605. vehicle.unbind( 'year', 'torque' );
  606. assertBinding( vehicle,
  607. { color: 'red', year: 2000, torque: 160 },
  608. [
  609. [ car, { color: 'blue', year: 1969, torque: 220 } ]
  610. ],
  611. { color: 'blue', year: 2000, torque: 160 }
  612. );
  613. } );
  614. it( 'should remove bindings of certain attributes, callback', () => {
  615. const vehicle = new Car();
  616. const car1 = new Car( { color: 'red' } );
  617. const car2 = new Car( { color: 'blue' } );
  618. vehicle.bind( 'color' ).to( car1, car2, ( c1, c2 ) => c1 + c2 );
  619. vehicle.unbind( 'color' );
  620. assertBinding( vehicle,
  621. { color: 'redblue' },
  622. [
  623. [ car1, { color: 'green' } ],
  624. [ car2, { color: 'violet' } ]
  625. ],
  626. { color: 'redblue' }
  627. );
  628. } );
  629. it( 'should be able to unbind two attributes from a single source observable attribute', () => {
  630. const vehicle = new Car();
  631. vehicle.bind( 'color' ).to( car, 'color' );
  632. vehicle.bind( 'interiorColor' ).to( car, 'color' );
  633. vehicle.unbind( 'color' );
  634. vehicle.unbind( 'interiorColor' );
  635. assertBinding( vehicle,
  636. { color: 'red', interiorColor: 'red' },
  637. [
  638. [ car, { color: 'blue' } ]
  639. ],
  640. { color: 'red', interiorColor: 'red' }
  641. );
  642. } );
  643. } );
  644. } );