observablemixin.js 23 KB

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