8
0

observablemixin.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  6. import utilsTestUtils from '../tests/_utils/utils';
  7. import ObservableMixin from '../src/observablemixin';
  8. import EmitterMixin from '../src/emittermixin';
  9. import EventInfo from '../src/eventinfo';
  10. import CKEditorError from '../src/ckeditorerror';
  11. import mix from '../src/mix';
  12. const assertBinding = utilsTestUtils.assertBinding;
  13. describe( 'ObservableMixin', () => {
  14. testUtils.createSinonSandbox();
  15. it( 'exists', () => {
  16. expect( ObservableMixin ).to.be.an( 'object' );
  17. } );
  18. it( 'mixes in EmitterMixin', () => {
  19. expect( ObservableMixin ).to.have.property( 'on', EmitterMixin.on );
  20. } );
  21. it( 'implements set, bind, and unbind methods', () => {
  22. expect( ObservableMixin ).to.contain.keys( 'set', 'bind', 'unbind' );
  23. } );
  24. } );
  25. describe( 'Observable', () => {
  26. testUtils.createSinonSandbox();
  27. class Observable {
  28. constructor( properties ) {
  29. if ( properties ) {
  30. this.set( properties );
  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 properties 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. const spy = sinon.spy();
  76. const spyColor = sinon.spy();
  77. const spyYear = sinon.spy();
  78. const 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(
  104. spyWheels, sinon.match.instanceOf( EventInfo ),
  105. 'wheels', 4, sinon.match.typeOf( 'undefined' )
  106. );
  107. } );
  108. it( 'should not fire the "change" event for the same property value', () => {
  109. const spy = sinon.spy();
  110. const spyColor = sinon.spy();
  111. car.on( 'change', spy );
  112. car.on( 'change:color', spyColor );
  113. // Set the "color" property in all possible ways.
  114. car.color = 'red';
  115. car.set( 'color', 'red' );
  116. car.set( { color: 'red' } );
  117. sinon.assert.notCalled( spy );
  118. sinon.assert.notCalled( spyColor );
  119. } );
  120. it( 'should fire the "set" event', () => {
  121. const spy = sinon.spy();
  122. const spyColor = sinon.spy();
  123. const spyYear = sinon.spy();
  124. const spyWheels = sinon.spy();
  125. car.on( 'set', spy );
  126. car.on( 'set:color', spyColor );
  127. car.on( 'set:year', spyYear );
  128. car.on( 'set:wheels', spyWheels );
  129. // Set property in all possible ways.
  130. car.color = 'blue';
  131. car.set( { year: 2003 } );
  132. car.set( 'wheels', 4 );
  133. // Check number of calls.
  134. sinon.assert.calledThrice( spy );
  135. sinon.assert.calledOnce( spyColor );
  136. sinon.assert.calledOnce( spyYear );
  137. sinon.assert.calledOnce( spyWheels );
  138. // Check context.
  139. sinon.assert.alwaysCalledOn( spy, car );
  140. sinon.assert.calledOn( spyColor, car );
  141. sinon.assert.calledOn( spyYear, car );
  142. sinon.assert.calledOn( spyWheels, car );
  143. // Check params.
  144. sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'color', 'blue', 'red' );
  145. sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'year', 2003, 2015 );
  146. sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'wheels', 4, sinon.match.typeOf( 'undefined' ) );
  147. sinon.assert.calledWithExactly( spyColor, sinon.match.instanceOf( EventInfo ), 'color', 'blue', 'red' );
  148. sinon.assert.calledWithExactly( spyYear, sinon.match.instanceOf( EventInfo ), 'year', 2003, 2015 );
  149. sinon.assert.calledWithExactly(
  150. spyWheels, sinon.match.instanceOf( EventInfo ),
  151. 'wheels', 4, sinon.match.typeOf( 'undefined' )
  152. );
  153. } );
  154. it( 'should use "set" return value as an observable new value', () => {
  155. car.color = 'blue';
  156. const spy = sinon.spy();
  157. car.on( 'set:color', evt => {
  158. evt.stop();
  159. evt.return = 'red';
  160. }, { priority: 'high' } );
  161. car.on( 'change:color', spy );
  162. car.color = 'pink';
  163. sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'color', 'red', 'blue' );
  164. } );
  165. it( 'should fire the "set" event for the same property value', () => {
  166. const spy = sinon.spy();
  167. const spyColor = sinon.spy();
  168. car.on( 'set', spy );
  169. car.on( 'set:color', spyColor );
  170. // Set the "color" property in all possible ways.
  171. car.color = 'red';
  172. car.set( 'color', 'red' );
  173. car.set( { color: 'red' } );
  174. sinon.assert.calledThrice( spy );
  175. sinon.assert.calledThrice( spyColor );
  176. } );
  177. it( 'should throw when overriding already existing property', () => {
  178. car.normalProperty = 1;
  179. expect( () => {
  180. car.set( 'normalProperty', 2 );
  181. } ).to.throw( CKEditorError, /^observable-set-cannot-override/ );
  182. expect( car ).to.have.property( 'normalProperty', 1 );
  183. } );
  184. it( 'should throw when overriding already existing property (in the prototype)', () => {
  185. class Car extends Observable {
  186. method() {}
  187. }
  188. car = new Car();
  189. expect( () => {
  190. car.set( 'method', 2 );
  191. } ).to.throw( CKEditorError, /^observable-set-cannot-override/ );
  192. expect( car.method ).to.be.a( 'function' );
  193. } );
  194. it( 'should allow setting properties with undefined value', () => {
  195. const spy = sinon.spy();
  196. car.on( 'change', spy );
  197. car.set( 'seats', undefined );
  198. sinon.assert.calledOnce( spy );
  199. expect( car ).to.contain.keys( 'seats' );
  200. expect( car.seats ).to.be.undefined;
  201. car.set( 'seats', 5 );
  202. sinon.assert.calledTwice( spy );
  203. expect( car ).to.have.property( 'seats', 5 );
  204. } );
  205. } );
  206. describe( 'bind()', () => {
  207. it( 'should chain for a single property', () => {
  208. expect( car.bind( 'color' ) ).to.contain.keys( 'to' );
  209. } );
  210. it( 'should chain for multiple properties', () => {
  211. expect( car.bind( 'color', 'year' ) ).to.contain.keys( 'to' );
  212. } );
  213. it( 'should chain for nonexistent properties', () => {
  214. expect( car.bind( 'nonexistent' ) ).to.contain.keys( 'to' );
  215. } );
  216. it( 'should throw when properties are not strings', () => {
  217. expect( () => {
  218. car.bind();
  219. } ).to.throw( CKEditorError, /observable-bind-wrong-properties/ );
  220. expect( () => {
  221. car.bind( new Date() );
  222. } ).to.throw( CKEditorError, /observable-bind-wrong-properties/ );
  223. expect( () => {
  224. car.bind( 'color', new Date() );
  225. } ).to.throw( CKEditorError, /observable-bind-wrong-properties/ );
  226. } );
  227. it( 'should throw when the same property is used than once', () => {
  228. expect( () => {
  229. car.bind( 'color', 'color' );
  230. } ).to.throw( CKEditorError, /observable-bind-duplicate-properties/ );
  231. } );
  232. it( 'should throw when binding the same property more than once', () => {
  233. expect( () => {
  234. car.bind( 'color' );
  235. car.bind( 'color' );
  236. } ).to.throw( CKEditorError, /observable-bind-rebind/ );
  237. } );
  238. describe( 'to()', () => {
  239. it( 'should not chain', () => {
  240. expect(
  241. car.bind( 'color' ).to( new Observable( { color: 'red' } ) )
  242. ).to.be.undefined;
  243. } );
  244. it( 'should throw when arguments are of invalid type - empty', () => {
  245. expect( () => {
  246. car = new Car();
  247. car.bind( 'color' ).to();
  248. } ).to.throw( CKEditorError, /observable-bind-to-parse-error/ );
  249. } );
  250. it( 'should throw when binding multiple properties to multiple observables', () => {
  251. let vehicle = new Car();
  252. const car1 = new Car( { color: 'red', year: 1943 } );
  253. const car2 = new Car( { color: 'yellow', year: 1932 } );
  254. expect( () => {
  255. vehicle.bind( 'color', 'year' ).to( car1, 'color', car2, 'year' );
  256. } ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
  257. expect( () => {
  258. vehicle = new Car();
  259. vehicle.bind( 'color', 'year' ).to( car1, car2 );
  260. } ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
  261. expect( () => {
  262. vehicle = new Car();
  263. vehicle.bind( 'color', 'year' ).to( car1, car2, 'year' );
  264. } ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
  265. expect( () => {
  266. vehicle = new Car();
  267. vehicle.bind( 'color', 'year' ).to( car1, 'color', car2 );
  268. } ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
  269. expect( () => {
  270. vehicle = new Car();
  271. vehicle.bind( 'color', 'year', 'custom' ).to( car, car );
  272. } ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
  273. } );
  274. it( 'should throw when binding multiple properties but passed a callback', () => {
  275. let vehicle = new Car();
  276. expect( () => {
  277. vehicle.bind( 'color', 'year' ).to( car, () => {} );
  278. } ).to.throw( CKEditorError, /observable-bind-to-extra-callback/ );
  279. expect( () => {
  280. vehicle = new Car();
  281. vehicle.bind( 'color', 'year' ).to( car, car, () => {} );
  282. } ).to.throw( CKEditorError, /observable-bind-to-extra-callback/ );
  283. } );
  284. it( 'should throw when binding a single property but multiple callbacks', () => {
  285. let vehicle = new Car();
  286. expect( () => {
  287. vehicle.bind( 'color' ).to( car, () => {}, () => {} );
  288. } ).to.throw( CKEditorError, /observable-bind-to-parse-error/ );
  289. expect( () => {
  290. vehicle = new Car();
  291. vehicle.bind( 'color' ).to( car, car, () => {}, () => {} );
  292. } ).to.throw( CKEditorError, /observable-bind-to-parse-error/ );
  293. } );
  294. it( 'should throw when a number of properties does not match', () => {
  295. let vehicle = new Car();
  296. expect( () => {
  297. vehicle.bind( 'color' ).to( car, 'color', 'year' );
  298. } ).to.throw( CKEditorError, /observable-bind-to-properties-length/ );
  299. expect( () => {
  300. vehicle = new Car();
  301. vehicle.bind( 'color', 'year' ).to( car, 'color' );
  302. } ).to.throw( CKEditorError, /observable-bind-to-properties-length/ );
  303. expect( () => {
  304. vehicle = new Car();
  305. vehicle.bind( 'color' ).to( car, 'color', 'year', () => {} );
  306. } ).to.throw( CKEditorError, /observable-bind-to-properties-length/ );
  307. expect( () => {
  308. vehicle = new Car();
  309. vehicle.bind( 'color' ).to( car, 'color', car, 'color', 'year', () => {} );
  310. } ).to.throw( CKEditorError, /observable-bind-to-properties-length/ );
  311. } );
  312. it( 'should work when properties don\'t exist in to() observable #1', () => {
  313. const vehicle = new Car();
  314. vehicle.bind( 'color' ).to( car, 'nonexistent in car' );
  315. assertBinding( vehicle,
  316. { color: undefined, year: undefined },
  317. [
  318. [ car, { 'nonexistent in car': 'foo', year: 1969 } ]
  319. ],
  320. { color: 'foo', year: undefined }
  321. );
  322. } );
  323. it( 'should work when properties don\'t exist in to() observable #2', () => {
  324. const vehicle = new Car();
  325. vehicle.bind( 'nonexistent in car' ).to( car );
  326. assertBinding( vehicle,
  327. { 'nonexistent in car': undefined, year: undefined },
  328. [
  329. [ car, { 'nonexistent in car': 'foo', year: 1969 } ]
  330. ],
  331. { 'nonexistent in car': 'foo', year: undefined }
  332. );
  333. } );
  334. it( 'should work when properties don\'t exist in to() observable #3', () => {
  335. const vehicle = new Car();
  336. vehicle.bind( 'year' ).to( car, 'color', car, 'nonexistent in car', ( a, b ) => a + b );
  337. assertBinding( vehicle,
  338. { color: undefined, year: car.color + undefined },
  339. [
  340. [ car, { color: 'blue', year: 1969 } ]
  341. ],
  342. { color: undefined, year: 'blueundefined' }
  343. );
  344. } );
  345. it( 'should set new observable properties', () => {
  346. const car = new Car( { color: 'green', year: 2001, type: 'pickup' } );
  347. const vehicle = new Car( { 'not involved': true } );
  348. vehicle.bind( 'color', 'year', 'type' ).to( car );
  349. expect( vehicle ).to.have.property( 'color' );
  350. expect( vehicle ).to.have.property( 'year' );
  351. expect( vehicle ).to.have.property( 'type' );
  352. expect( vehicle ).to.have.property( 'not involved' );
  353. } );
  354. it( 'should work when no property specified #1', () => {
  355. const vehicle = new Car();
  356. vehicle.bind( 'color' ).to( car );
  357. assertBinding( vehicle,
  358. { color: car.color, year: undefined },
  359. [
  360. [ car, { color: 'blue', year: 1969 } ]
  361. ],
  362. { color: 'blue', year: undefined }
  363. );
  364. } );
  365. it( 'should work for a single property', () => {
  366. const vehicle = new Car();
  367. vehicle.bind( 'color' ).to( car, 'color' );
  368. assertBinding( vehicle,
  369. { color: car.color, year: undefined },
  370. [
  371. [ car, { color: 'blue', year: 1969 } ]
  372. ],
  373. { color: 'blue', year: undefined }
  374. );
  375. } );
  376. it( 'should work for multiple properties', () => {
  377. const vehicle = new Car();
  378. vehicle.bind( 'color', 'year' ).to( car, 'color', 'year' );
  379. assertBinding( vehicle,
  380. { color: car.color, year: car.year },
  381. [
  382. [ car, { color: 'blue', year: 1969 } ]
  383. ],
  384. { color: 'blue', year: 1969 }
  385. );
  386. } );
  387. it( 'should work for properties that don\'t exist in the observable', () => {
  388. const vehicle = new Car();
  389. vehicle.bind( 'nonexistent in vehicle' ).to( car, 'color' );
  390. assertBinding( vehicle,
  391. { 'nonexistent in vehicle': car.color, color: undefined },
  392. [
  393. [ car, { color: 'blue', year: 1969 } ]
  394. ],
  395. { 'nonexistent in vehicle': 'blue', color: undefined }
  396. );
  397. } );
  398. it( 'should work when using the same property name more than once', () => {
  399. const vehicle = new Car();
  400. vehicle.bind( 'color', 'year' ).to( car, 'year', 'year' );
  401. assertBinding( vehicle,
  402. { color: car.year, year: car.year },
  403. [
  404. [ car, { color: 'blue', year: 1969 } ]
  405. ],
  406. { color: 1969, year: 1969 }
  407. );
  408. } );
  409. it( 'should work when binding more that once', () => {
  410. const vehicle = new Car();
  411. vehicle.bind( 'color' ).to( car, 'color' );
  412. vehicle.bind( 'year' ).to( car, 'year' );
  413. assertBinding( vehicle,
  414. { color: car.color, year: car.year },
  415. [
  416. [ car, { color: 'blue', year: 1969 } ]
  417. ],
  418. { color: 'blue', year: 1969 }
  419. );
  420. } );
  421. it( 'should work with callback – set a new observable property', () => {
  422. const vehicle = new Car();
  423. const car1 = new Car( { type: 'pickup' } );
  424. const car2 = new Car( { type: 'truck' } );
  425. vehicle.bind( 'type' )
  426. .to( car1, car2, ( ...args ) => args.join( '' ) );
  427. expect( vehicle ).to.have.property( 'type' );
  428. } );
  429. it( 'should work with callback #1', () => {
  430. const vehicle = new Car();
  431. const car1 = new Car( { color: 'black' } );
  432. const car2 = new Car( { color: 'brown' } );
  433. vehicle.bind( 'color' )
  434. .to( car1, car2, ( ...args ) => args.join( '' ) );
  435. assertBinding( vehicle,
  436. { color: car1.color + car2.color, year: undefined },
  437. [
  438. [ car1, { color: 'black', year: 1930 } ],
  439. [ car2, { color: 'green', year: 1950 } ]
  440. ],
  441. { color: 'blackgreen', year: undefined }
  442. );
  443. } );
  444. it( 'should work with callback #2', () => {
  445. const vehicle = new Car();
  446. const car1 = new Car( { color: 'black' } );
  447. const car2 = new Car( { color: 'brown' } );
  448. vehicle.bind( 'color' )
  449. .to( car1, 'color', car2, 'color', ( ...args ) => args.join( '' ) );
  450. assertBinding( vehicle,
  451. { color: car1.color + car2.color, year: undefined },
  452. [
  453. [ car1, { color: 'black', year: 1930 } ],
  454. [ car2, { color: 'green', year: 1950 } ]
  455. ],
  456. { color: 'blackgreen', year: undefined }
  457. );
  458. } );
  459. it( 'should work with callback #3', () => {
  460. const vehicle = new Car();
  461. const car1 = new Car( { color: 'black' } );
  462. const car2 = new Car( { color: 'brown' } );
  463. const car3 = new Car( { color: 'yellow' } );
  464. vehicle.bind( 'color' )
  465. .to( car1, car2, car3, ( ...args ) => args.join( '' ) );
  466. assertBinding( vehicle,
  467. { color: car1.color + car2.color + car3.color, year: undefined },
  468. [
  469. [ car1, { color: 'black', year: 1930 } ],
  470. [ car2, { color: 'green', year: 1950 } ]
  471. ],
  472. { color: 'blackgreenyellow', year: undefined }
  473. );
  474. } );
  475. it( 'should work with callback #4', () => {
  476. const vehicle = new Car();
  477. const car1 = new Car( { color: 'black' } );
  478. const car2 = new Car( { lightness: 'bright' } );
  479. const car3 = new Car( { color: 'yellow' } );
  480. vehicle.bind( 'color' )
  481. .to( car1, car2, 'lightness', car3, ( ...args ) => args.join( '' ) );
  482. assertBinding( vehicle,
  483. { color: car1.color + car2.lightness + car3.color, year: undefined },
  484. [
  485. [ car1, { color: 'black', year: 1930 } ],
  486. [ car2, { color: 'green', year: 1950 } ]
  487. ],
  488. { color: 'blackbrightyellow', year: undefined }
  489. );
  490. } );
  491. it( 'should work with callback #5', () => {
  492. const vehicle = new Car();
  493. const car1 = new Car( { hue: 'reds' } );
  494. const car2 = new Car( { lightness: 'bright' } );
  495. vehicle.bind( 'color' )
  496. .to( car1, 'hue', car2, 'lightness', ( ...args ) => args.join( '' ) );
  497. assertBinding( vehicle,
  498. { color: car1.hue + car2.lightness, year: undefined },
  499. [
  500. [ car1, { hue: 'greens', year: 1930 } ],
  501. [ car2, { lightness: 'dark', year: 1950 } ]
  502. ],
  503. { color: 'greensdark', year: undefined }
  504. );
  505. } );
  506. it( 'should work with callback #6', () => {
  507. const vehicle = new Car();
  508. const car1 = new Car( { hue: 'reds' } );
  509. vehicle.bind( 'color' )
  510. .to( car1, 'hue', h => h.toUpperCase() );
  511. assertBinding( vehicle,
  512. { color: car1.hue.toUpperCase(), year: undefined },
  513. [
  514. [ car1, { hue: 'greens', year: 1930 } ]
  515. ],
  516. { color: 'GREENS', year: undefined }
  517. );
  518. } );
  519. it( 'should work with callback #7', () => {
  520. const vehicle = new Car();
  521. const car1 = new Car( { color: 'red', year: 1943 } );
  522. const car2 = new Car( { color: 'yellow', year: 1932 } );
  523. vehicle.bind( 'custom' )
  524. .to( car1, 'color', car2, 'year', ( ...args ) => args.join( '/' ) );
  525. assertBinding( vehicle,
  526. { color: undefined, year: undefined, 'custom': car1.color + '/' + car2.year },
  527. [
  528. [ car1, { color: 'blue', year: 2100 } ],
  529. [ car2, { color: 'violet', year: 1969 } ]
  530. ],
  531. { color: undefined, year: undefined, 'custom': 'blue/1969' }
  532. );
  533. } );
  534. it( 'should work with callback #8', () => {
  535. const vehicle = new Car();
  536. const car1 = new Car( { color: 'red', year: 1943 } );
  537. const car2 = new Car( { color: 'yellow', year: 1932 } );
  538. const car3 = new Car( { hue: 'reds' } );
  539. vehicle.bind( 'custom' )
  540. .to( car1, 'color', car2, 'year', car3, 'hue', ( ...args ) => args.join( '/' ) );
  541. assertBinding( vehicle,
  542. { color: undefined, year: undefined, hue: undefined, 'custom': car1.color + '/' + car2.year + '/' + car3.hue },
  543. [
  544. [ car1, { color: 'blue', year: 2100 } ],
  545. [ car2, { color: 'violet', year: 1969 } ]
  546. ],
  547. { color: undefined, year: undefined, hue: undefined, 'custom': 'blue/1969/reds' }
  548. );
  549. } );
  550. it( 'should work with callback – binding more that once #1', () => {
  551. const vehicle = new Car();
  552. const car1 = new Car( { hue: 'reds', produced: 1920 } );
  553. const car2 = new Car( { lightness: 'bright', sold: 1921 } );
  554. vehicle.bind( 'color' )
  555. .to( car1, 'hue', car2, 'lightness', ( ...args ) => args.join( '' ) );
  556. vehicle.bind( 'year' )
  557. .to( car1, 'produced', car2, 'sold', ( ...args ) => args.join( '/' ) );
  558. assertBinding( vehicle,
  559. { color: car1.hue + car2.lightness, year: car1.produced + '/' + car2.sold },
  560. [
  561. [ car1, { hue: 'greens', produced: 1930 } ],
  562. [ car2, { lightness: 'dark', sold: 2000 } ]
  563. ],
  564. { color: 'greensdark', year: '1930/2000' }
  565. );
  566. } );
  567. it( 'should work with callback – binding more that once #2', () => {
  568. const vehicle = new Car();
  569. const car1 = new Car( { hue: 'reds', produced: 1920 } );
  570. const car2 = new Car( { lightness: 'bright', sold: 1921 } );
  571. vehicle.bind( 'color' )
  572. .to( car1, 'hue', car2, 'lightness', ( ...args ) => args.join( '' ) );
  573. vehicle.bind( 'year' )
  574. .to( car1, 'produced', car2, 'sold', ( ...args ) => args.join( '/' ) );
  575. vehicle.bind( 'mix' )
  576. .to( car1, 'hue', car2, 'sold', ( ...args ) => args.join( '+' ) );
  577. assertBinding( vehicle,
  578. {
  579. color: car1.hue + car2.lightness,
  580. year: car1.produced + '/' + car2.sold,
  581. mix: car1.hue + '+' + car2.sold
  582. },
  583. [
  584. [ car1, { hue: 'greens', produced: 1930 } ],
  585. [ car2, { lightness: 'dark', sold: 2000 } ]
  586. ],
  587. {
  588. color: 'greensdark',
  589. year: '1930/2000',
  590. mix: 'greens+2000'
  591. }
  592. );
  593. } );
  594. it( 'should work with callback – binding more that once #3', () => {
  595. const vehicle = new Car();
  596. const car1 = new Car( { hue: 'reds', produced: 1920 } );
  597. const car2 = new Car( { lightness: 'bright', sold: 1921 } );
  598. vehicle.bind( 'color' )
  599. .to( car1, 'hue', car2, 'lightness', ( ...args ) => args.join( '' ) );
  600. vehicle.bind( 'custom1' ).to( car1, 'hue' );
  601. vehicle.bind( 'year' )
  602. .to( car1, 'produced', car2, 'sold', ( ...args ) => args.join( '/' ) );
  603. vehicle.bind( 'custom2', 'custom3' ).to( car1, 'produced', 'hue' );
  604. assertBinding( vehicle,
  605. {
  606. color: car1.hue + car2.lightness,
  607. year: car1.produced + '/' + car2.sold,
  608. custom1: car1.hue,
  609. custom2: car1.produced,
  610. custom3: car1.hue
  611. },
  612. [
  613. [ car1, { hue: 'greens', produced: 1930 } ],
  614. [ car2, { lightness: 'dark', sold: 2000 } ]
  615. ],
  616. {
  617. color: 'greensdark',
  618. year: '1930/2000',
  619. custom1: 'greens',
  620. custom2: 1930,
  621. custom3: 'greens'
  622. }
  623. );
  624. } );
  625. it( 'should fire a single change event per bound property', () => {
  626. const vehicle = new Car();
  627. const car = new Car( { color: 'red', year: 1943 } );
  628. const spy = sinon.spy();
  629. vehicle.on( 'change', spy );
  630. vehicle.bind( 'color', 'year' ).to( car );
  631. car.color = 'violet';
  632. car.custom = 'foo';
  633. car.year = 2001;
  634. expect( spy.args.map( args => args[ 1 ] ) )
  635. .to.have.members( [ 'color', 'year', 'color', 'year' ] );
  636. } );
  637. } );
  638. describe( 'toMany()', () => {
  639. let Wheel;
  640. beforeEach( () => {
  641. Wheel = class extends Observable {
  642. };
  643. } );
  644. it( 'should not chain', () => {
  645. expect(
  646. car.bind( 'color' ).toMany( [ new Observable( { color: 'red' } ) ], 'color', () => {} )
  647. ).to.be.undefined;
  648. } );
  649. it( 'should throw when binding multiple properties', () => {
  650. let vehicle = new Car();
  651. expect( () => {
  652. vehicle.bind( 'color', 'year' ).toMany( [ car ], 'foo', () => {} );
  653. } ).to.throw( CKEditorError, /observable-bind-to-many-not-one-binding/ );
  654. expect( () => {
  655. vehicle = new Car();
  656. vehicle.bind( 'color', 'year' ).to( car, car, () => {} );
  657. } ).to.throw( CKEditorError, /observable-bind-to-extra-callback/ );
  658. } );
  659. it( 'binds observable property to collection property using callback', () => {
  660. const wheels = [
  661. new Wheel( { isTyrePressureOK: true } ),
  662. new Wheel( { isTyrePressureOK: true } ),
  663. new Wheel( { isTyrePressureOK: true } ),
  664. new Wheel( { isTyrePressureOK: true } )
  665. ];
  666. car.bind( 'showTyrePressureWarning' ).toMany( wheels, 'isTyrePressureOK', ( ...areEnabled ) => {
  667. // Every tyre must have OK pressure.
  668. return !areEnabled.every( isTyrePressureOK => isTyrePressureOK );
  669. } );
  670. expect( car.showTyrePressureWarning ).to.be.false;
  671. wheels[ 0 ].isTyrePressureOK = false;
  672. expect( car.showTyrePressureWarning ).to.be.true;
  673. wheels[ 0 ].isTyrePressureOK = true;
  674. expect( car.showTyrePressureWarning ).to.be.false;
  675. wheels[ 1 ].isTyrePressureOK = false;
  676. expect( car.showTyrePressureWarning ).to.be.true;
  677. } );
  678. } );
  679. } );
  680. describe( 'unbind()', () => {
  681. it( 'should not fail when unbinding a fresh observable', () => {
  682. const observable = new Observable();
  683. observable.unbind();
  684. } );
  685. it( 'should not fail when unbinding property that is not bound', () => {
  686. const observable = new Observable();
  687. observable.bind( 'foo' ).to( car, 'color' );
  688. expect( () => observable.unbind( 'bar' ) ).to.not.throw();
  689. } );
  690. it( 'should throw when non-string property is passed', () => {
  691. expect( () => {
  692. car.unbind( new Date() );
  693. } ).to.throw( CKEditorError, /observable-unbind-wrong-properties/ );
  694. } );
  695. it( 'should remove all bindings', () => {
  696. const vehicle = new Car();
  697. vehicle.bind( 'color', 'year' ).to( car, 'color', 'year' );
  698. vehicle.unbind();
  699. assertBinding( vehicle,
  700. { color: 'red', year: 2015 },
  701. [
  702. [ car, { color: 'blue', year: 1969 } ]
  703. ],
  704. { color: 'red', year: 2015 }
  705. );
  706. } );
  707. it( 'should remove bindings of certain properties', () => {
  708. const vehicle = new Car();
  709. const car = new Car( { color: 'red', year: 2000, torque: 160 } );
  710. vehicle.bind( 'color', 'year', 'torque' ).to( car );
  711. vehicle.unbind( 'year', 'torque' );
  712. assertBinding( vehicle,
  713. { color: 'red', year: 2000, torque: 160 },
  714. [
  715. [ car, { color: 'blue', year: 1969, torque: 220 } ]
  716. ],
  717. { color: 'blue', year: 2000, torque: 160 }
  718. );
  719. } );
  720. it( 'should remove bindings of certain properties, callback', () => {
  721. const vehicle = new Car();
  722. const car1 = new Car( { color: 'red' } );
  723. const car2 = new Car( { color: 'blue' } );
  724. vehicle.bind( 'color' ).to( car1, car2, ( c1, c2 ) => c1 + c2 );
  725. vehicle.unbind( 'color' );
  726. assertBinding( vehicle,
  727. { color: 'redblue' },
  728. [
  729. [ car1, { color: 'green' } ],
  730. [ car2, { color: 'violet' } ]
  731. ],
  732. { color: 'redblue' }
  733. );
  734. } );
  735. it( 'should be able to unbind two properties from a single source observable property', () => {
  736. const vehicle = new Car();
  737. vehicle.bind( 'color' ).to( car, 'color' );
  738. vehicle.bind( 'interiorColor' ).to( car, 'color' );
  739. vehicle.unbind( 'color' );
  740. vehicle.unbind( 'interiorColor' );
  741. assertBinding( vehicle,
  742. { color: 'red', interiorColor: 'red' },
  743. [
  744. [ car, { color: 'blue' } ]
  745. ],
  746. { color: 'red', interiorColor: 'red' }
  747. );
  748. } );
  749. } );
  750. describe( 'decorate()', () => {
  751. it( 'makes the method fire an event', () => {
  752. const spy = sinon.spy();
  753. class Foo extends Observable {
  754. method() {}
  755. }
  756. const foo = new Foo();
  757. foo.decorate( 'method' );
  758. foo.on( 'method', spy );
  759. foo.method( 1, 2 );
  760. expect( spy.calledOnce ).to.be.true;
  761. expect( spy.args[ 0 ][ 1 ] ).to.deep.equal( [ 1, 2 ] );
  762. } );
  763. it( 'executes the original method in a listener with the default priority', () => {
  764. const calls = [];
  765. class Foo extends Observable {
  766. method() {
  767. calls.push( 'original' );
  768. }
  769. }
  770. const foo = new Foo();
  771. foo.decorate( 'method' );
  772. foo.on( 'method', () => calls.push( 'high' ), { priority: 'high' } );
  773. foo.on( 'method', () => calls.push( 'low' ), { priority: 'low' } );
  774. foo.method();
  775. expect( calls ).to.deep.equal( [ 'high', 'original', 'low' ] );
  776. } );
  777. it( 'supports overriding return values', () => {
  778. class Foo extends Observable {
  779. method() {
  780. return 1;
  781. }
  782. }
  783. const foo = new Foo();
  784. foo.decorate( 'method' );
  785. foo.on( 'method', evt => {
  786. expect( evt.return ).to.equal( 1 );
  787. evt.return = 2;
  788. } );
  789. expect( foo.method() ).to.equal( 2 );
  790. } );
  791. it( 'supports overriding arguments', () => {
  792. class Foo extends Observable {
  793. method( a ) {
  794. expect( a ).to.equal( 2 );
  795. }
  796. }
  797. const foo = new Foo();
  798. foo.decorate( 'method' );
  799. foo.on( 'method', ( evt, args ) => {
  800. args[ 0 ] = 2;
  801. }, { priority: 'high' } );
  802. foo.method( 1 );
  803. } );
  804. it( 'supports stopping the event (which prevents execution of the orignal method', () => {
  805. class Foo extends Observable {
  806. method() {
  807. throw new Error( 'this should not be executed' );
  808. }
  809. }
  810. const foo = new Foo();
  811. foo.decorate( 'method' );
  812. foo.on( 'method', evt => {
  813. evt.stop();
  814. }, { priority: 'high' } );
  815. foo.method();
  816. } );
  817. it( 'throws when trying to decorate non existing method', () => {
  818. class Foo extends Observable {}
  819. const foo = new Foo();
  820. expect( () => {
  821. foo.decorate( 'method' );
  822. } ).to.throw( CKEditorError, /^observablemixin-cannot-decorate-undefined:/ );
  823. } );
  824. } );
  825. } );