CommandStackSpec.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855
  1. /* global sinon */
  2. import {
  3. bootstrapDiagram,
  4. inject
  5. } from 'test/TestHelper';
  6. import cmdModule from 'lib/command';
  7. // example commands
  8. function TracableCommand(id) {
  9. this.execute = function(ctx) {
  10. ctx.element.trace.push(id);
  11. };
  12. this.revert = function(ctx) {
  13. expect(ctx.element.trace.pop()).to.equal(id);
  14. };
  15. }
  16. function SimpleCommand() {
  17. TracableCommand.call(this, 'simple-command');
  18. }
  19. function ComplexCommand(commandStack) {
  20. TracableCommand.call(this, 'complex-command');
  21. this.preExecute = function(ctx) {
  22. commandStack.execute('pre-command', { element: ctx.element });
  23. };
  24. this.postExecute = function(ctx) {
  25. commandStack.execute('post-command', { element: ctx.element });
  26. };
  27. }
  28. function PreCommand() {
  29. TracableCommand.call(this, 'pre-command');
  30. }
  31. function PostCommand() {
  32. TracableCommand.call(this, 'post-command');
  33. }
  34. describe('command/CommandStack', function() {
  35. beforeEach(bootstrapDiagram({ modules: [ cmdModule ] }));
  36. describe('#register', function() {
  37. var handler = {
  38. execute: function(ctx) {
  39. ctx.heho = 'HE';
  40. },
  41. revert: function(ctx) {
  42. ctx.heho = 'HO';
  43. }
  44. };
  45. it('should register handler instance', inject(function(commandStack) {
  46. // when
  47. commandStack.register('heho', handler);
  48. // then
  49. expect(commandStack._getHandler('heho')).to.equal(handler);
  50. }));
  51. });
  52. describe('#registerHandler', function() {
  53. var Handler = function(eventBus) {
  54. this.execute = function(ctx) {
  55. expect(eventBus).to.be.an('object');
  56. ctx.heho = 'HE';
  57. };
  58. this.revert = function(ctx) {
  59. ctx.heho = 'HO';
  60. expect(eventBus).to.be.an('object');
  61. };
  62. };
  63. it('should register handler class', inject(function(commandStack) {
  64. // when
  65. commandStack.registerHandler('heho', Handler);
  66. // then
  67. expect(commandStack._getHandler('heho') instanceof Handler).to.eql(true);
  68. }));
  69. it('should inject handler instance', inject(function(commandStack) {
  70. // given
  71. commandStack.registerHandler('heho', Handler);
  72. var context = {};
  73. // when
  74. commandStack.execute('heho', context);
  75. // then
  76. expect(context.heho).to.equal('HE');
  77. }));
  78. });
  79. describe('#execute', function() {
  80. it('should throw error on no command', inject(function(commandStack) {
  81. expect(function() {
  82. commandStack.execute();
  83. }).to.throw();
  84. }));
  85. it('should throw error on no handler', inject(function(commandStack) {
  86. expect(function() {
  87. commandStack.execute('non-existing-command');
  88. }).to.throw();
  89. }));
  90. it('should execute command', inject(function(commandStack) {
  91. // given
  92. commandStack.registerHandler('simple-command', SimpleCommand);
  93. var context = { element: { trace: [] } };
  94. // when
  95. commandStack.execute('simple-command', context);
  96. // then
  97. expect(context.element.trace).to.eql([ 'simple-command' ]);
  98. expect(commandStack._stack.length).to.equal(1);
  99. expect(commandStack._stackIdx).to.equal(0);
  100. }));
  101. });
  102. describe('#canExecute', function() {
  103. it('should reject unhandled', inject(function(commandStack) {
  104. // when
  105. var canExecute = commandStack.canExecute('non-existing-command');
  106. // then
  107. expect(canExecute).to.be.false;
  108. }));
  109. it('should still allow unregistered commands on interceptor response', inject(function(eventBus, commandStack) {
  110. eventBus.on('commandStack.foo.canExecute', function() {
  111. return true;
  112. });
  113. // when
  114. var canExecute = commandStack.canExecute('foo');
  115. // then
  116. expect(canExecute).to.be.true;
  117. }));
  118. describe('should forward to handler', function() {
  119. function testCanExecute(commandStack, accept) {
  120. // given
  121. commandStack.register('command', {
  122. canExecute: function(context) {
  123. return (context.canExecute = accept);
  124. }
  125. });
  126. var context = { };
  127. // when
  128. var canExecute = commandStack.canExecute('command', context);
  129. // then
  130. expect(canExecute).to.equal(accept);
  131. expect(context.canExecute).to.equal(accept);
  132. }
  133. it('accepting', inject(function(commandStack) {
  134. testCanExecute(commandStack, true);
  135. }));
  136. it('rejecting', inject(function(commandStack) {
  137. testCanExecute(commandStack, false);
  138. }));
  139. });
  140. describe('should integrate with eventBus', function() {
  141. it('rejecting in listener', inject(function(eventBus, commandStack) {
  142. // given
  143. eventBus.on('commandStack.command.canExecute', function(event) {
  144. return (event.context.listenerCanExecute = false);
  145. });
  146. commandStack.register('command', {
  147. canExecute: function(context) {
  148. return (context.commandCanExecute = true);
  149. }
  150. });
  151. var context = { };
  152. // when
  153. var canExecute = commandStack.canExecute('command', context);
  154. // then
  155. expect(canExecute).to.be.false;
  156. expect(context.listenerCanExecute).to.be.false;
  157. expect(context.commandCanExecute).to.be.undefined;
  158. }));
  159. it('allowing in listener', inject(function(eventBus, commandStack) {
  160. // given
  161. eventBus.on('commandStack.command.canExecute', function(event) {
  162. return (event.context.listenerCanExecute = true);
  163. });
  164. commandStack.register('command', {
  165. canExecute: function(context) {
  166. return (context.commandCanExecute = false);
  167. }
  168. });
  169. var context = { };
  170. // when
  171. var canExecute = commandStack.canExecute('command', context);
  172. // then
  173. expect(canExecute).to.be.true;
  174. expect(context.listenerCanExecute).to.be.true;
  175. expect(context.commandCanExecute).to.be.undefined;
  176. }));
  177. it('rejecting in command', inject(function(eventBus, commandStack) {
  178. // given
  179. eventBus.on('commandStack.command.canExecute', function(event) {
  180. // do nothing, just chill
  181. });
  182. commandStack.register('command', {
  183. canExecute: function(context) {
  184. return (context.commandCanExecute = false);
  185. }
  186. });
  187. var context = { };
  188. // when
  189. var canExecute = commandStack.canExecute('command', context);
  190. // then
  191. expect(canExecute).to.be.false;
  192. expect(context.commandCanExecute).to.be.false;
  193. }));
  194. });
  195. });
  196. describe('#undo', function() {
  197. it('should not fail if nothing to undo', inject(function(commandStack) {
  198. expect(function() {
  199. commandStack.undo();
  200. }).not.to.throw();
  201. }));
  202. it('should undo command', inject(function(commandStack) {
  203. // given
  204. commandStack.registerHandler('simple-command', SimpleCommand);
  205. var context = { element: { trace: [] } };
  206. commandStack.execute('simple-command', context);
  207. // when
  208. commandStack.undo();
  209. // then
  210. expect(context.element.trace).to.eql([]);
  211. expect(commandStack._stack.length).to.equal(1);
  212. expect(commandStack._stackIdx).to.equal(-1);
  213. }));
  214. });
  215. describe('#redo', function() {
  216. it('should not fail if nothing to redo', inject(function(commandStack) {
  217. expect(function() {
  218. commandStack.redo();
  219. }).not.to.throw();
  220. }));
  221. it('should redo command', inject(function(commandStack) {
  222. // given
  223. commandStack.registerHandler('simple-command', SimpleCommand);
  224. var context = { element: { trace: [] } };
  225. commandStack.execute('simple-command', context);
  226. commandStack.undo();
  227. // when
  228. commandStack.redo();
  229. // then
  230. expect(context.element.trace).to.eql([ 'simple-command' ]);
  231. expect(commandStack._stack.length).to.equal(1);
  232. expect(commandStack._stackIdx).to.equal(0);
  233. }));
  234. });
  235. describe('command context', function() {
  236. it('should pass command context to handler', inject(function(commandStack) {
  237. // given
  238. var context = {};
  239. commandStack.register('command', {
  240. execute: function(ctx) {
  241. expect(ctx).to.equal(context);
  242. },
  243. revert: function(ctx) {
  244. expect(ctx).to.equal(context);
  245. }
  246. });
  247. // then
  248. // expect not to fail
  249. commandStack.execute('command', context);
  250. commandStack.undo();
  251. commandStack.redo('command', context);
  252. }));
  253. });
  254. describe('#preExecute / #postExecute support', function() {
  255. var element, context;
  256. beforeEach(inject(function(commandStack) {
  257. element = { trace: [] };
  258. context = {
  259. element: element
  260. };
  261. commandStack.registerHandler('complex-command', ComplexCommand);
  262. commandStack.registerHandler('pre-command', PreCommand);
  263. commandStack.registerHandler('post-command', PostCommand);
  264. }));
  265. it('should invoke #preExecute and #postExecute in order', inject(function(commandStack) {
  266. // when
  267. commandStack.execute('complex-command', context);
  268. // then
  269. expect(element.trace).to.eql([
  270. 'pre-command',
  271. 'complex-command',
  272. 'post-command'
  273. ]);
  274. }));
  275. it('should group {pre,actual,post} commands on commandStack', inject(function(commandStack) {
  276. // when
  277. commandStack.execute('complex-command', context);
  278. var stack = commandStack._stack,
  279. stackIdx = commandStack._stackIdx;
  280. // then
  281. expect(stack.length).to.equal(3);
  282. expect(stackIdx).to.equal(2);
  283. // expect same id(s)
  284. expect(stack[0].id).to.equal(stack[1].id);
  285. expect(stack[2].id).to.equal(stack[1].id);
  286. }));
  287. it('should undo {pre,actual,post} commands', inject(function(commandStack) {
  288. // when
  289. commandStack.execute('complex-command', context);
  290. commandStack.undo();
  291. var stack = commandStack._stack,
  292. stackIdx = commandStack._stackIdx;
  293. // then
  294. expect(stack.length).to.equal(3);
  295. expect(stackIdx).to.equal(-1);
  296. expect(element.trace).eql([]);
  297. }));
  298. it('should redo pre/post commands', inject(function(commandStack) {
  299. // when
  300. commandStack.execute('complex-command', context);
  301. commandStack.undo();
  302. commandStack.redo();
  303. var stack = commandStack._stack,
  304. stackIdx = commandStack._stackIdx;
  305. // then
  306. expect(stack.length).to.equal(3);
  307. expect(stackIdx).to.equal(2);
  308. expect(element.trace).eql([
  309. 'pre-command',
  310. 'complex-command',
  311. 'post-command'
  312. ]);
  313. }));
  314. describe('event integration', function() {
  315. it('should emit #preExecute and #postExecute events', inject(function(commandStack, eventBus) {
  316. // given
  317. var events = [];
  318. function logEvent(e) {
  319. events.push(e.type + ' ' + e.command);
  320. }
  321. eventBus.on([
  322. 'commandStack.preExecute',
  323. 'commandStack.preExecuted',
  324. 'commandStack.execute',
  325. 'commandStack.postExecute',
  326. 'commandStack.postExecuted'
  327. ], logEvent);
  328. // when
  329. commandStack.execute('complex-command', context);
  330. // then
  331. expect(events).eql([
  332. 'commandStack.preExecute complex-command',
  333. 'commandStack.preExecute pre-command',
  334. 'commandStack.preExecuted pre-command',
  335. 'commandStack.execute pre-command',
  336. 'commandStack.postExecute pre-command',
  337. 'commandStack.postExecuted pre-command',
  338. 'commandStack.preExecuted complex-command',
  339. 'commandStack.execute complex-command',
  340. 'commandStack.postExecute complex-command',
  341. 'commandStack.preExecute post-command',
  342. 'commandStack.preExecuted post-command',
  343. 'commandStack.execute post-command',
  344. 'commandStack.postExecute post-command',
  345. 'commandStack.postExecuted post-command',
  346. 'commandStack.postExecuted complex-command'
  347. ]);
  348. }));
  349. it('should emit execute* events', inject(function(commandStack, eventBus) {
  350. // given
  351. var events = [];
  352. function logEvent(e) {
  353. events.push((e && e.command) || 'changed');
  354. }
  355. eventBus.on([ 'commandStack.execute', 'commandStack.changed' ], logEvent);
  356. // when
  357. commandStack.execute('complex-command', context);
  358. // then
  359. expect(events).to.eql([
  360. 'pre-command',
  361. 'complex-command',
  362. 'post-command',
  363. 'changed'
  364. ]);
  365. }));
  366. it('should emit revert* events', inject(function(commandStack, eventBus) {
  367. // given
  368. var events = [];
  369. function logEvent(e) {
  370. events.push((e && e.command) || 'changed');
  371. }
  372. commandStack.execute('complex-command', context);
  373. eventBus.on([ 'commandStack.revert', 'commandStack.changed' ], logEvent);
  374. // when
  375. commandStack.undo();
  376. // then
  377. expect(events).to.eql([
  378. 'post-command',
  379. 'complex-command',
  380. 'pre-command',
  381. 'changed'
  382. ]);
  383. }));
  384. });
  385. });
  386. describe('missing handler #execute / #revert', function() {
  387. function JustPrePostCommand() {
  388. var id = 'just-pre-post-command';
  389. this.preExecute = function(ctx) {
  390. ctx.element.trace.push(id + '-pre-execute');
  391. };
  392. this.postExecute = function(ctx) {
  393. ctx.element.trace.push(id + '-post-execute');
  394. };
  395. }
  396. it('should execute anyway', inject(function(commandStack) {
  397. // given
  398. var element = { trace: [] };
  399. commandStack.registerHandler('just-pre-post-command', JustPrePostCommand);
  400. // when
  401. commandStack.execute('just-pre-post-command', { element: element });
  402. // then
  403. expect(element.trace).to.eql([
  404. 'just-pre-post-command-pre-execute',
  405. 'just-pre-post-command-post-execute'
  406. ]);
  407. }));
  408. it('should undo anyway', inject(function(commandStack) {
  409. // given
  410. var element = { trace: [] };
  411. commandStack.registerHandler('just-pre-post-command', JustPrePostCommand);
  412. commandStack.execute('just-pre-post-command', { element: element });
  413. // then
  414. expect(function() {
  415. commandStack.undo();
  416. }).not.to.throw;
  417. }));
  418. });
  419. describe('dirty handling', function() {
  420. var OuterHandler = function(commandStack) {
  421. this.execute = function(context) {
  422. return context.s1;
  423. };
  424. this.revert = function(context) {
  425. return context.s1;
  426. };
  427. this.postExecute = function(context) {
  428. commandStack.execute('inner-command', context);
  429. };
  430. };
  431. var InnerHandler = function() {
  432. this.execute = function(context) {
  433. return [ context.s1, context.s2 ];
  434. };
  435. this.revert = function(context) {
  436. return [ context.s1, context.s2 ];
  437. };
  438. };
  439. it('should update dirty shapes after change', inject(function(commandStack, eventBus) {
  440. // given
  441. commandStack.registerHandler('outer-command', OuterHandler);
  442. commandStack.registerHandler('inner-command', InnerHandler);
  443. var s1 = { id: 1 }, s2 = { id: 2 }, context = { s1: s1, s2: s2 };
  444. var events = [];
  445. function logEvent(e) {
  446. events.push(e.elements);
  447. }
  448. eventBus.on('elements.changed', logEvent);
  449. // when
  450. commandStack.execute('outer-command', context);
  451. // then
  452. expect(events).to.eql([ [ s1, s2 ] ]);
  453. }));
  454. });
  455. describe('stack information', function() {
  456. var Handler = function(eventBus) {
  457. this.execute = function(ctx) {
  458. expect(eventBus).to.be.an('object');
  459. ctx.heho = 'HE';
  460. };
  461. this.revert = function(ctx) {
  462. ctx.heho = 'HO';
  463. expect(eventBus).to.be.an('object');
  464. };
  465. };
  466. describe('stack information #canUndo', function() {
  467. it('should return true', inject(function(commandStack) {
  468. // given
  469. commandStack.registerHandler('heho', Handler);
  470. var context = {};
  471. // when
  472. commandStack.execute('heho', context);
  473. // then
  474. expect(commandStack.canUndo()).to.be.true;
  475. }));
  476. it('should return false', inject(function(commandStack) {
  477. // then
  478. expect(commandStack.canUndo()).to.be.false;
  479. }));
  480. });
  481. describe('stack information #canRedo', function() {
  482. it('should return true', inject(function(commandStack) {
  483. // given
  484. commandStack.registerHandler('heho', Handler);
  485. var context = {};
  486. // when
  487. commandStack.execute('heho', context);
  488. commandStack.undo();
  489. // then
  490. expect(commandStack.canRedo()).to.be.true;
  491. }));
  492. it('should return false', inject(function(commandStack) {
  493. // given
  494. commandStack.registerHandler('heho', Handler);
  495. var context = {};
  496. // when
  497. commandStack.execute('heho', context);
  498. // then
  499. expect(commandStack.canRedo()).to.be.false;
  500. }));
  501. });
  502. });
  503. describe('diagram life-cycle integration', function() {
  504. function verifyReset(eventName) {
  505. return function(eventBus, commandStack) {
  506. // given
  507. commandStack._stack.push('FOO');
  508. commandStack._stackIdx = 10;
  509. var changedSpy = sinon.spy(function() {});
  510. eventBus.on('commandStack.reset', changedSpy);
  511. // when
  512. eventBus.fire(eventName);
  513. // then
  514. expect(commandStack._stack).to.be.empty;
  515. expect(commandStack._stackIdx).to.eql(-1);
  516. expect(changedSpy).not.to.have.been.called;
  517. };
  518. }
  519. it('should clear on diagram.destroy', inject(verifyReset('diagram.destroy')));
  520. it('should clear on diagram.clear', inject(verifyReset('diagram.clear')));
  521. });
  522. describe('atomic commands', function() {
  523. var ERROR_MESSAGE = 'illegal invocation in <execute> or <revert> phase (action: simple-command)';
  524. describe('should protect against illegal invocation', function() {
  525. it('during <execute>', inject(function(eventBus, commandStack) {
  526. // given
  527. var invokeNested = sinon.spy(function invokeNested(event) {
  528. // then
  529. expect(function() {
  530. commandStack.execute('simple-command', {});
  531. }).to.throw(ERROR_MESSAGE);
  532. });
  533. commandStack.registerHandler('simple-command', SimpleCommand);
  534. eventBus.on('commandStack.execute', invokeNested);
  535. // when
  536. commandStack.execute('simple-command', { element: { trace: [] } });
  537. // then
  538. expect(invokeNested).to.have.been.called;
  539. }));
  540. it('during <revert>', inject(function(eventBus, commandStack) {
  541. // given
  542. var invokeNested = sinon.spy(function invokeNested(event) {
  543. // then
  544. expect(function() {
  545. commandStack.execute('simple-command', {});
  546. }).to.throw(ERROR_MESSAGE);
  547. });
  548. commandStack.registerHandler('simple-command', SimpleCommand);
  549. eventBus.on('commandStack.revert', invokeNested);
  550. commandStack.execute('simple-command', { element: { trace: [] } });
  551. // when
  552. commandStack.undo();
  553. // then
  554. expect(invokeNested).to.have.been.called;
  555. }));
  556. });
  557. });
  558. });