observablemixin.js 29 KB

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