PageRenderTime 48ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/spec/javascripts/views/story_view_spec.js

https://bitbucket.org/sqctest01/fulcrum
JavaScript | 476 lines | 359 code | 113 blank | 4 comment | 0 complexity | 52487ea36a6d6b4a40d65310d884cab6 MD5 | raw file
  1. describe('Fulcrum.StoryView', function() {
  2. beforeEach(function() {
  3. window.projectView = {
  4. availableTags: []
  5. };
  6. window.md = { makeHtml: sinon.stub() };
  7. var Note = Backbone.Model.extend({name: 'note'});
  8. var NotesCollection = Backbone.Collection.extend({model: Note});
  9. var Story = Backbone.Model.extend({
  10. name: 'story', defaults: {story_type: 'feature'},
  11. estimable: function() { return true; },
  12. estimated: function() { return false; },
  13. point_values: function() { return [0,1,2]; },
  14. hasErrors: function() { return false; },
  15. errorsOn: function() { return false; },
  16. url: '/path/to/story',
  17. collection: { project: { users: { forSelect: function() {return [];} } } },
  18. start: function() {},
  19. setAcceptedAt: sinon.spy()
  20. //moveAfter: function() {},
  21. //moveBefore: function() {}
  22. });
  23. this.story = new Story({id: 999, title: 'Story'});
  24. this.new_story = new Story({title: 'New Story'});
  25. this.story.notes = this.new_story.notes = new NotesCollection();
  26. this.view = new Fulcrum.StoryView({
  27. model: this.story
  28. });
  29. this.new_story_view = new Fulcrum.StoryView({
  30. model: this.new_story
  31. });
  32. this.server = sinon.fakeServer.create();
  33. });
  34. afterEach(function() {
  35. this.server.restore();
  36. });
  37. describe('class name', function() {
  38. it('should have the story class', function() {
  39. expect($(this.view.el)).toHaveClass('story');
  40. });
  41. it('should have the story type class', function() {
  42. expect($(this.view.el)).toHaveClass('feature');
  43. });
  44. it('should have the unestimated class', function() {
  45. expect($(this.view.el)).toHaveClass('unestimated');
  46. // Should not have the unestimated class if it's been estimated
  47. sinon.stub(this.view.model, "estimated").returns(true);
  48. this.view.model.set({estimate: 1});
  49. expect($(this.view.el)).not.toHaveClass('unestimated');
  50. });
  51. it("should have the story state class", function() {
  52. expect($(this.view.el)).toHaveClass('unestimated');
  53. this.view.model.set({state: 'accepted'});
  54. expect($(this.view.el)).toHaveClass('accepted');
  55. });
  56. });
  57. describe("id", function() {
  58. it("should have an id", function() {
  59. expect(this.view.id).toEqual(this.view.model.id);
  60. expect($(this.view.el)).toHaveId(this.view.model.id);
  61. });
  62. });
  63. describe("cancel edit", function() {
  64. it("should remove itself when edit cancelled if its new", function() {
  65. var view = new Fulcrum.StoryView({model: this.new_story});
  66. var spy = sinon.spy(this.new_story, "clear");
  67. view.cancelEdit();
  68. expect(spy).toHaveBeenCalled();
  69. });
  70. it("should reload after cancel if there were existing errors", function() {
  71. this.story.set({errors:true});
  72. expect(this.story.get('errors')).toEqual(true);
  73. sinon.stub(this.story, "hasErrors").returns(true);
  74. var spy = sinon.spy(this.story, "fetch");
  75. this.view.cancelEdit();
  76. expect(spy).toHaveBeenCalled();
  77. expect(this.story.get('errors')).toBeUndefined();
  78. });
  79. });
  80. describe("save edit", function() {
  81. it("should call save", function() {
  82. this.server.respondWith(
  83. "PUT", "/path/to/story", [
  84. 200, {"Content-Type": "application/json"},
  85. '{"story":{"title":"Story title"}}'
  86. ]
  87. );
  88. this.story.set({editing: true});
  89. this.view.saveEdit();
  90. expect(this.story.get('editing')).toBeTruthy();
  91. expect(this.server.requests.length).toEqual(1);
  92. // editing should be set to false when save is successful
  93. this.server.respond();
  94. expect(this.story.get('editing')).toBeFalsy();
  95. });
  96. it("should set editing when errors occur", function() {
  97. this.server.respondWith(
  98. "PUT", "/path/to/story", [
  99. 422, {"Content-Type": "application/json"},
  100. '{"story":{"errors":{"title":["cannot be blank"]}}}'
  101. ]
  102. );
  103. this.view.saveEdit();
  104. expect(this.server.responses.length).toEqual(1);
  105. expect(this.server.responses[0].method).toEqual("PUT");
  106. expect(this.server.responses[0].url).toEqual("/path/to/story");
  107. this.server.respond();
  108. expect(this.story.get('editing')).toBeTruthy();
  109. expect(this.story.get('errors').title[0]).toEqual("cannot be blank");
  110. });
  111. it("should disable all form controls on submit", function() {
  112. this.server.respondWith(
  113. "PUT", "/path/to/story", [
  114. 200, {"Content-Type": "application/json"},
  115. '{"story":{"title":"Story title"}}'
  116. ]
  117. );
  118. var disable_spy = sinon.spy(this.view, 'disableForm');
  119. var enable_spy = sinon.spy(this.view, 'enableForm');
  120. this.story.set({editing: true});
  121. this.view.saveEdit();
  122. expect(disable_spy).toHaveBeenCalled();
  123. expect(enable_spy).not.toHaveBeenCalled();
  124. expect($(this.view.el).find('a.collapse').hasClass('icons-throbber')).toBeTruthy();
  125. this.server.respond();
  126. expect(enable_spy).toHaveBeenCalled();
  127. });
  128. it('should disable state transition buttons on click', function() {
  129. this.server.respondWith(
  130. "PUT", "/path/to/story", [
  131. 200, {"Content-Type": "application/json"},
  132. '{"story":{"state":"started"}}'
  133. ]
  134. );
  135. var ev = { target: { value : 'start' } };
  136. this.view.transition(ev);
  137. expect(this.view.saveInProgress).toBeTruthy();
  138. this.server.respond();
  139. expect(this.view.saveInProgress).toBeFalsy();
  140. });
  141. it('should disable estimate buttons on click', function() {
  142. this.server.respondWith(
  143. "PUT", "/path/to/story", [
  144. 200, {"Content-Type": "application/json"},
  145. '{"story":{"estimate":"1"}}'
  146. ]
  147. );
  148. var ev = { target: { value : '1' } };
  149. this.view.estimate(ev);
  150. expect(this.view.saveInProgress).toBeTruthy();
  151. this.server.respond();
  152. expect(this.view.saveInProgress).toBeFalsy();
  153. });
  154. it("should call setAcceptedAt on the story", function() {
  155. this.view.saveEdit();
  156. expect(this.story.setAcceptedAt).toHaveBeenCalledOnce();
  157. });
  158. });
  159. describe("expand collapse controls", function() {
  160. it("should not show the collapse control if its a new story", function() {
  161. this.new_story.set({editing: true});
  162. expect($(this.new_story_view.el)).not.toContain('a.collapse');
  163. });
  164. });
  165. describe("sorting", function() {
  166. beforeEach(function() {
  167. this.story.collection.length = 1;
  168. this.story.collection.columns = function() {return [];};
  169. this.story.collection.project.columnsBefore = sinon.stub();
  170. this.story.collection.project.columnsAfter = sinon.stub();
  171. });
  172. it("sets state to unstarted if dropped on the backlog column", function() {
  173. this.story.set({'state':'unscheduled'});
  174. var html = $('<td id="backlog"><div id="1"></div></td>');
  175. var ev = {target: html.find('#1')};
  176. this.view.sortUpdate(ev);
  177. expect(this.story.get('state')).toEqual("unstarted");
  178. });
  179. it("sets state to unstarted if dropped on the in_progress column", function() {
  180. this.story.set({'state':'unscheduled'});
  181. var html = $('<td id="in_progress"><div id="1"></div></td>');
  182. var ev = {target: html.find('#1')};
  183. this.view.sortUpdate(ev);
  184. expect(this.story.get('state')).toEqual("unstarted");
  185. });
  186. it("doesn't change state if not unscheduled and dropped on the in_progress column", function() {
  187. this.story.set({'state':'finished'});
  188. var html = $('<td id="in_progress"><div id="1"></div></td>');
  189. var ev = {target: html.find('#1')};
  190. this.view.sortUpdate(ev);
  191. expect(this.story.get('state')).toEqual("finished");
  192. });
  193. it("sets state to unscheduled if dropped on the chilly_bin column", function() {
  194. this.story.set({'state':'unstarted'});
  195. var html = $('<td id="chilly_bin"><div id="1"></div></td>');
  196. var ev = {target: html.find('#1')};
  197. this.view.sortUpdate(ev);
  198. expect(this.story.get('state')).toEqual("unscheduled");
  199. });
  200. it("should move after the previous story in the column", function() {
  201. var html = $('<div id="1" class="story"></div><div id="2" class="story"></div>');
  202. var ev = {target: html[1]};
  203. this.story.moveAfter = sinon.spy();
  204. this.view.sortUpdate(ev);
  205. expect(this.story.moveAfter).toHaveBeenCalledWith("1");
  206. });
  207. it("should move before the next story in the column", function() {
  208. var html = $('<div id="1" class="story"></div><div id="2" class="story"></div>');
  209. var ev = {target: html[0]};
  210. this.story.moveBefore = sinon.spy();
  211. this.view.sortUpdate(ev);
  212. expect(this.story.moveBefore).toHaveBeenCalledWith("2");
  213. });
  214. it("should move before the next story in the column", function() {
  215. var html = $('<div id="foo"></div><div id="1" class="story"></div><div id="2" class="story"></div>');
  216. var ev = {target: html[1]};
  217. this.story.moveBefore = sinon.spy();
  218. this.view.sortUpdate(ev);
  219. expect(this.story.moveBefore).toHaveBeenCalledWith("2");
  220. });
  221. it("should move into an empty chilly bin", function() {
  222. var html = $('<td id="backlog"><div id="1"></div></td><td id="chilly_bin"><div id="2"></div></td>');
  223. var ev = {target: html.find('#2')};
  224. this.story.moveAfter = sinon.spy();
  225. this.view.sortUpdate(ev);
  226. expect(this.story.get('state')).toEqual('unscheduled');
  227. });
  228. });
  229. describe("hover box placement", function() {
  230. it("should return right if element is in the left half of the page", function() {
  231. var positionStub = sinon.stub(jQuery.fn, 'position');
  232. var widthStub = sinon.stub(jQuery.fn, 'width');
  233. positionStub.returns({'left': 25, 'top': 25});
  234. widthStub.returns(100);
  235. expect(this.view.hoverBoxPlacement()).toEqual('right');
  236. positionStub.restore();
  237. widthStub.restore();
  238. });
  239. it("should return left if element is in the right half of the page", function() {
  240. var positionStub = sinon.stub(jQuery.fn, 'position');
  241. var widthStub = sinon.stub(jQuery.fn, 'width');
  242. positionStub.returns({'left': 75, 'top': 75});
  243. widthStub.returns(100);
  244. expect(this.view.hoverBoxPlacement()).toEqual('left');
  245. positionStub.restore();
  246. widthStub.restore();
  247. });
  248. });
  249. describe("labels", function() {
  250. it("should initialize tagit on edit", function() {
  251. var spy = sinon.spy(jQuery.fn, 'tagit');
  252. this.new_story.set({editing: true});
  253. expect(spy).toHaveBeenCalled();
  254. spy.restore();
  255. });
  256. });
  257. describe("notes", function() {
  258. it("binds change:notes to renderNotesCollection()", function() {
  259. var spy = sinon.spy(this.story, 'bind');
  260. var view = new Fulcrum.StoryView({model: this.story});
  261. expect(spy).toHaveBeenCalledWith('change:notes', view.renderNotesCollection);
  262. });
  263. it("binds change:notes to addEmptyNote()", function() {
  264. var spy = sinon.spy(this.story, 'bind');
  265. var view = new Fulcrum.StoryView({model: this.story});
  266. expect(spy).toHaveBeenCalledWith('change:notes', view.addEmptyNote);
  267. });
  268. it("adds a blank note to the end of the notes collection", function() {
  269. this.view.model.notes.reset();
  270. expect(this.view.model.notes.length).toEqual(0);
  271. this.view.addEmptyNote();
  272. expect(this.view.model.notes.length).toEqual(1);
  273. expect(this.view.model.notes.last().isNew()).toBeTruthy();
  274. });
  275. it("doesn't add a blank note if the story is new", function() {
  276. var stub = sinon.stub(this.view.model, 'isNew');
  277. stub.returns(true);
  278. this.view.model.notes.reset();
  279. expect(this.view.model.notes.length).toEqual(0);
  280. this.view.addEmptyNote();
  281. expect(this.view.model.notes.length).toEqual(0);
  282. });
  283. it("doesn't add a blank note if there is already one", function() {
  284. this.view.model.notes.last = sinon.stub().returns({
  285. isNew: sinon.stub().returns(true)
  286. });
  287. expect(this.view.model.notes.last().isNew()).toBeTruthy();
  288. var oldLength = this.view.model.notes.length;
  289. this.view.addEmptyNote();
  290. expect(this.view.model.notes.length).toEqual(oldLength);
  291. });
  292. });
  293. describe("description", function() {
  294. beforeEach(function() {
  295. this.view.model.set({editing: true});
  296. });
  297. afterEach(function() {
  298. this.view.model.set({editing: false});
  299. });
  300. it("is text area when story is new", function() {
  301. this.view.model.isNew = sinon.stub().returns(true);
  302. this.view.render();
  303. expect(this.view.$('textarea[name="description"]').length).toEqual(1);
  304. expect(this.view.$('div.description').length).toEqual(0);
  305. expect(this.view.$('input#edit-description').length).toEqual(0);
  306. });
  307. it("isn't text area when story isn't new", function() {
  308. this.view.model.isNew = sinon.stub().returns(false);
  309. this.view.render();
  310. expect(this.view.$('textarea[name="description"]').length).toEqual(0);
  311. expect(this.view.$('div.description').length).toEqual(1);
  312. expect(this.view.$('input#edit-description').length).toEqual(1);
  313. });
  314. it('is a text area after #edit-description is clicked', function() {
  315. this.view.model.isNew = sinon.stub().returns(false);
  316. this.view.editDescription();
  317. expect(this.view.model.get('editingDescription')).toBeTruthy();
  318. });
  319. it('is reset to false after startEdit is called', function() {
  320. this.view.model.set({editingDescription: true});
  321. this.view.startEdit();
  322. expect(this.view.model.get('editingDescription')).toBeFalsy();
  323. });
  324. });
  325. describe("makeFormControl", function() {
  326. beforeEach(function() {
  327. this.div = {};
  328. this.view.make = sinon.stub().returns(this.div);
  329. });
  330. it("calls make('div')", function() {
  331. this.view.makeFormControl();
  332. expect(this.view.make).toHaveBeenCalled();
  333. });
  334. it("returns the div", function() {
  335. expect(this.view.makeFormControl()).toBe(this.div);
  336. });
  337. it("invokes its callback", function() {
  338. var callback = sinon.stub();
  339. this.view.makeFormControl(callback);
  340. expect(callback).toHaveBeenCalledWith(this.div);
  341. });
  342. describe("when passed an object", function() {
  343. beforeEach(function() {
  344. this.content = {name: "foo", label: "Foo", control: "bar"};
  345. this.appendSpy = sinon.spy(jQuery.fn, 'append');
  346. });
  347. afterEach(function() {
  348. this.appendSpy.restore();
  349. });
  350. it("creates a label", function() {
  351. var label = '<label for="foo">Foo</label>';
  352. var stub = sinon.stub(this.view, 'label').withArgs('foo', 'Foo').returns(label);
  353. this.view.makeFormControl(this.content);
  354. expect(this.appendSpy).toHaveBeenCalledWith(label);
  355. expect(this.appendSpy).toHaveBeenCalledWith('<br/>');
  356. });
  357. it("appends the control", function() {
  358. this.view.makeFormControl(this.content);
  359. expect(this.appendSpy).toHaveBeenCalledWith(this.content.control);
  360. });
  361. });
  362. });
  363. });