From e27c007653a43a12e3e0fb5b1899ee468230cfa5 Mon Sep 17 00:00:00 2001 From: Wojciech Zawistowski Date: Thu, 23 Jan 2014 20:25:37 +0100 Subject: [PATCH] Adds unit tests for SearchController --- .../controllers/search_controller.js | 1 + .../controllers/search_controller_test.js | 290 ++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 test/javascripts/controllers/search_controller_test.js diff --git a/app/assets/javascripts/discourse/controllers/search_controller.js b/app/assets/javascripts/discourse/controllers/search_controller.js index 8b45c945bfb..985244c6211 100644 --- a/app/assets/javascripts/discourse/controllers/search_controller.js +++ b/app/assets/javascripts/discourse/controllers/search_controller.js @@ -17,6 +17,7 @@ Discourse.SearchController = Em.ArrayController.extend(Discourse.Presence, { this.searchTerm(term, this.get('typeFilter')); } else { this.set('content', Em.A()); + this.set('resultCount', 0); } this.set('selectedIndex', 0); }.observes('term', 'typeFilter'), diff --git a/test/javascripts/controllers/search_controller_test.js b/test/javascripts/controllers/search_controller_test.js new file mode 100644 index 00000000000..4cdad3f1504 --- /dev/null +++ b/test/javascripts/controllers/search_controller_test.js @@ -0,0 +1,290 @@ +var controller, searcherStub; + +module("Discourse.SearchController", { + setup: function() { + Discourse.SiteSettings.min_search_term_length = 2; + + // cancels debouncing behavior (calls the debounced function immediately) + sinon.stub(Ember.run, "debounce").callsArg(1); + + searcherStub = Ember.Deferred.create(); + sinon.stub(Discourse.Search, "forTerm").returns(searcherStub); + + controller = Discourse.SearchController.create(); + }, + + teardown: function() { + Ember.run.debounce.restore(); + Discourse.Search.forTerm.restore(); + } +}); + +test("when no search term is typed yet", function() { + ok(!controller.get("loading"), "loading flag is false"); + ok(!controller.get("noResults"), "noResults flag is false"); + deepEqual(controller.get("content"), [], "content is empty"); + blank(controller.get("selectedIndex"), "selectedIndex is not set"); + blank(controller.get("resultCount"), "result count is not set"); +}); + +test("when user started typing a search term but did not reach the minimum character count threshold yet", function() { + Ember.run(function() { + controller.set("term", "a"); + }); + + ok(!controller.get("loading"), "loading flag is false"); + ok(!controller.get("noResults"), "noResults flag is false"); + deepEqual(controller.get("content"), [], "content is empty"); + equal(controller.get("selectedIndex"), 0, "selectedIndex is set to 0"); + equal(controller.get("resultCount"), 0, "result count is set to 0"); +}); + +test("when user typed a search term that is equal to or exceeds the minimum character count threshold, but results have not yet finished loading", function() { + Ember.run(function() { + controller.set("term", "ab"); + }); + + ok(controller.get("loading"), "loading flag is true"); + ok(!controller.get("noResults"), "noResults flag is false"); + deepEqual(controller.get("content"), [], "content is empty"); + equal(controller.get("selectedIndex"), 0, "selectedIndex is set to 0"); + equal(controller.get("resultCount"), 0, "result count is set to 0"); +}); + +test("when user typed a search term that is equal to or exceeds the minimum character count threshold and results have finished loading, but there are no results found", function() { + Ember.run(function() { + controller.set("term", "ab"); + searcherStub.resolve([]); + }); + + ok(!controller.get("loading"), "loading flag is false"); + ok(controller.get("noResults"), "noResults flag is true"); + deepEqual(controller.get("content"), [], "content is empty"); + equal(controller.get("selectedIndex"), 0, "selectedIndex is set to 0"); + equal(controller.get("resultCount"), 0, "result count is set to 0"); +}); + +test("when user typed a search term that is equal to or exceeds the minimum character count threshold and results have finished loading, and there are results found", function() { + Ember.run(function() { + controller.set("term", "ab"); + searcherStub.resolve([{ + type: "user", + results: [{}] + }]); + }); + + ok(!controller.get("loading"), "loading flag is false"); + ok(!controller.get("noResults"), "noResults flag is false"); + deepEqual(controller.get("content"), [{ + type: "user", + results: [{index: 0}] + }], "content is correctly set"); + equal(controller.get("selectedIndex"), 0, "selectedIndex is set to 0"); + equal(controller.get("resultCount"), 1, "resultCount is correctly set"); +}); + +test("starting to type a new term resets the previous search results", function() { + Ember.run(function() { + controller.set("term", "ab"); + searcherStub.resolve([ + { + type: "user", + results: [{}] + } + ]); + }); + + Ember.run(function() { + controller.set("term", "x"); + }); + + ok(!controller.get("loading"), "loading flag is reset correctly"); + ok(!controller.get("noResults"), "noResults flag is reset correctly"); + deepEqual(controller.get("content"), [], "content is reset correctly"); + equal(controller.get("selectedIndex"), 0, "selected index is reset correctly"); + equal(controller.get("resultCount"), 0, "resultCount is reset correctly"); +}); + +test("search results from the server are correctly reformatted (sections are sorted, section fields are preserved, item sorting is preserved, item fields are preserved, items are globally indexed across all sections)", function() { + Ember.run(function() { + controller.set("term", "ab"); + searcherStub.resolve([ + { + type: "user", + results: [ + {itemField: "user-item-1"}, + {itemField: "user-item-2"} + ], + sectionField: "user-section" + }, + { + type: "topic", + results: [ + {itemField: "topic-item-1"}, + {itemField: "topic-item-2"} + ], + sectionField: "topic-section" + }, + { + type: "category", + results: [ + {itemField: "category-item-1"}, + {itemField: "category-item-2"} + ], + sectionField: "category-section" + } + ]); + }); + + deepEqual(controller.get("content"), [ + { + type: "topic", + results: [ + {index: 0, itemField: "topic-item-1"}, + {index: 1, itemField: "topic-item-2"} + ], + sectionField: "topic-section" + }, + { + type: "category", + results: [ + {index: 2, itemField: "category-item-1"}, + {index: 3, itemField: "category-item-2"} + ], + sectionField: "category-section" + }, + { + type: "user", + results: [ + {index: 4, itemField: "user-item-1"}, + {index: 5, itemField: "user-item-2"} + ], + sectionField: "user-section" + } + ]); +}); + +test("keyboard navigation", function() { + Ember.run(function() { + controller.set("term", "ab"); + searcherStub.resolve([ + { + type: "user", + results: [{}, {}, {}] + } + ]); + }); + + equal(controller.get("selectedIndex"), 0, "initially the first item is selected"); + + controller.moveUp(); + equal(controller.get("selectedIndex"), 0, "you can't move up above the first item"); + + controller.moveDown(); + equal(controller.get("selectedIndex"), 1, "you can go down from the first item"); + + controller.moveDown(); + equal(controller.get("selectedIndex"), 2, "you can go down from the middle item"); + + controller.moveDown(); + equal(controller.get("selectedIndex"), 2, "you can't go down below the last item"); + + controller.moveUp(); + equal(controller.get("selectedIndex"), 1, "you can go up from the last item"); + + controller.moveUp(); + equal(controller.get("selectedIndex"), 0, "you can go up from the middle item"); +}); + +// This test is a bit hackish due to the current design +// of the SearchController that manipulates the DOM directly. +// The alternative was to skip this test completely +// and verify selecting highlighted item in an end-to-end +// test, but I think that testing all the edge cases +// (existing/missing href, loading flag true/false) is too +// fine grained for an e2e test, so untill SearchController +// is refactored I decided to keep the test here. +test("selecting a highlighted item", function() { + this.stub(Discourse.URL, "routeTo"); + + fixture().append($('
')); + + Ember.run(function() { + controller.set("loading", false); + }); + controller.select(); + ok(!Discourse.URL.routeTo.called, "when selected item's link has no href, there is no redirect"); + + Ember.run(function() { + controller.set("loading", true); + }); + fixture("a").attr("href", "some-url"); + controller.select(); + ok(!Discourse.URL.routeTo.called, "when loading flag is set to true, there is no redirect"); + + Ember.run(function() { + controller.set("loading", false); + }); + controller.select(); + ok(Discourse.URL.routeTo.calledWith(sinon.match(/\/some-url$/)), "when loading flag is set to false and selected item's link has href, redirect to the selected item's link href is fired"); +}); + +test("search query / the flow of the search", function() { + Ember.run(function() { + controller.set("searchContext", "context"); + controller.set("term", "ab"); + }); + ok(Discourse.Search.forTerm.calledWithExactly( + "ab", + { + searchContext: "context", + typeFilter: null + } + ), "when an initial search (with term but without a type filter) is issued, query is built correctly and results are refreshed"); + ok(!controller.get("showCancelFilter"), "when an initial search (with term but without a type filter) is issued, showCancelFilter flag is false"); + + Discourse.Search.forTerm.reset(); + Ember.run(function() { + controller.send("moreOfType", "topic"); + }); + ok(Discourse.Search.forTerm.calledWithExactly( + "ab", + { + searchContext: "context", + typeFilter: "topic" + } + ), "when after the initial search a type filter is applied (moreOfType action is invoked), query is built correctly and results are refreshed"); + ok(!controller.get("showCancelFilter"), "when after the initial search a type filter is applied (moreOfType action is invoked) but the results did not yet finished loading, showCancelFilter flag is still false"); + Ember.run(function() { + searcherStub.resolve([]); + }); + ok(controller.get("showCancelFilter"), "when after the initial search a type filter is applied (moreOfType action is invoked) and the results finished loading, showCancelFilter flag is set to true"); + + Discourse.Search.forTerm.reset(); + Ember.run(function() { + controller.send("cancelType"); + }); + ok(Discourse.Search.forTerm.calledWithExactly( + "ab", + { + searchContext: "context", + typeFilter: null + } + ), "when cancelType action is invoked after the results were filtered by type, query is built correctly and results are refreshed"); + ok(!controller.get("showCancelFilter"), "when cancelType action is invoked after the results were filtered by type, showCancelFilter flag is set to false"); +}); + +test("typing new term when the results are filtered by type cancels type filter", function() { + Ember.run(function() { + controller.set("term", "ab"); + controller.send("moreOfType", "topic"); + searcherStub.resolve([]); + }); + + Discourse.Search.forTerm.reset(); + Ember.run(function() { + controller.set("term", "xy"); + }); + ok(Discourse.Search.forTerm.calledWith("xy"), "a new search is issued and results are refreshed"); + ok(!controller.get("showCancelFilter"), "showCancelFilter flag is set to false"); +});