observablemixin.js 23 KB

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