Commit 74757ed7 by Andy Armstrong

Merge pull request #9745 from edx/andya/fix-duplicate-search-request

Fix duplicate calls to search API
parents 50bcc496 f80b6221
...@@ -90,16 +90,20 @@ ...@@ -90,16 +90,20 @@
*/ */
setPage: function (page) { setPage: function (page) {
var oldPage = this.currentPage, var oldPage = this.currentPage,
self = this; self = this,
return this.goTo(page - (this.isZeroIndexed ? 1 : 0), {reset: true}).then( deferred = $.Deferred();
this.goTo(page - (this.isZeroIndexed ? 1 : 0), {reset: true}).then(
function () { function () {
self.isStale = false; self.isStale = false;
self.trigger('page_changed'); self.trigger('page_changed');
deferred.resolve();
}, },
function () { function () {
self.currentPage = oldPage; self.currentPage = oldPage;
deferred.fail();
} }
); );
return deferred.promise();
}, },
......
...@@ -334,7 +334,7 @@ class EventsTestMixin(TestCase): ...@@ -334,7 +334,7 @@ class EventsTestMixin(TestCase):
captured_events.append(event) captured_events.append(event)
@contextmanager @contextmanager
def assert_events_match_during(self, event_filter=None, expected_events=None): def assert_events_match_during(self, event_filter=None, expected_events=None, in_order=True):
""" """
Context manager that ensures that events matching the `event_filter` and `expected_events` are emitted. Context manager that ensures that events matching the `event_filter` and `expected_events` are emitted.
...@@ -351,7 +351,7 @@ class EventsTestMixin(TestCase): ...@@ -351,7 +351,7 @@ class EventsTestMixin(TestCase):
with self.capture_events(event_filter, len(expected_events), captured_events): with self.capture_events(event_filter, len(expected_events), captured_events):
yield yield
self.assert_events_match(expected_events, captured_events) self.assert_events_match(expected_events, captured_events, in_order=in_order)
def wait_for_events(self, start_time=None, event_filter=None, number_of_matches=1, timeout=None): def wait_for_events(self, start_time=None, event_filter=None, number_of_matches=1, timeout=None):
""" """
...@@ -477,17 +477,29 @@ class EventsTestMixin(TestCase): ...@@ -477,17 +477,29 @@ class EventsTestMixin(TestCase):
self.assertEquals(len(matching_events), 0, description) self.assertEquals(len(matching_events), 0, description)
def assert_events_match(self, expected_events, actual_events): def assert_events_match(self, expected_events, actual_events, in_order=True):
""" """Assert that each actual event matches one of the expected events.
Assert that each item in the expected events sequence matches its counterpart at the same index in the actual
events sequence. Args:
expected_events (List): a list of dicts representing the expected events.
actual_events (List): a list of dicts that were actually recorded.
in_order (bool): if True then the events must be in the same order (defaults to True).
""" """
for expected_event, actual_event in zip(expected_events, actual_events): if in_order:
assert_event_matches( for expected_event, actual_event in zip(expected_events, actual_events):
expected_event, assert_event_matches(
actual_event, expected_event,
tolerate=EventMatchTolerates.lenient() actual_event,
) tolerate=EventMatchTolerates.lenient()
)
else:
for expected_event in expected_events:
actual_event = next(event for event in actual_events if is_matching_event(expected_event, event))
assert_event_matches(
expected_event,
actual_event or {},
tolerate=EventMatchTolerates.lenient()
)
def relative_path_to_absolute_uri(self, relative_path): def relative_path_to_absolute_uri(self, relative_path):
"""Return an aboslute URI given a relative path taking into account the test context.""" """Return an aboslute URI given a relative path taking into account the test context."""
......
...@@ -9,7 +9,6 @@ from dateutil.parser import parse ...@@ -9,7 +9,6 @@ from dateutil.parser import parse
import ddt import ddt
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from uuid import uuid4 from uuid import uuid4
from unittest import skip
from ..helpers import EventsTestMixin, UniqueCourseTest from ..helpers import EventsTestMixin, UniqueCourseTest
from ...fixtures import LMS_BASE_URL from ...fixtures import LMS_BASE_URL
...@@ -783,7 +782,6 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -783,7 +782,6 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
self.browse_teams_page.click_browse_all_teams_link() self.browse_teams_page.click_browse_all_teams_link()
self.assertTrue(self.topics_page.is_browser_on_page()) self.assertTrue(self.topics_page.is_browser_on_page())
@skip("Skip until TNL-3198 (searching teams makes two AJAX requests) is resolved")
def test_search(self): def test_search(self):
""" """
Scenario: User should be able to search for a team Scenario: User should be able to search for a team
...@@ -794,6 +792,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -794,6 +792,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
And the search header should be shown And the search header should be shown
And 0 results should be shown And 0 results should be shown
And my browser should fire a page viewed event for the search page And my browser should fire a page viewed event for the search page
And a searched event should have been fired
""" """
# Note: all searches will return 0 results with the mock search server # Note: all searches will return 0 results with the mock search server
# used by Bok Choy. # used by Bok Choy.
...@@ -801,21 +800,21 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -801,21 +800,21 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
self.create_teams(self.topic, 5) self.create_teams(self.topic, 5)
self.browse_teams_page.visit() self.browse_teams_page.visit()
events = [{ events = [{
'event_type': 'edx.team.searched', 'event_type': 'edx.team.page_viewed',
'event': { 'event': {
'search_text': search_text, 'page_name': 'search-teams',
'topic_id': self.topic['id'], 'topic_id': self.topic['id'],
'number_of_results': 0 'team_id': None
} }
}, { }, {
'event_type': 'edx.team.page_viewed', 'event_type': 'edx.team.searched',
'event': { 'event': {
'page_name': 'search-teams', 'search_text': search_text,
'topic_id': self.topic['id'], 'topic_id': self.topic['id'],
'team_id': None 'number_of_results': 0
} }
}] }]
with self.assert_events_match_during(self.only_team_events, expected_events=events): with self.assert_events_match_during(self.only_team_events, expected_events=events, in_order=False):
search_results_page = self.browse_teams_page.search(search_text) search_results_page = self.browse_teams_page.search(search_text)
self.verify_search_header(search_results_page, search_text) self.verify_search_header(search_results_page, search_text)
self.assertTrue(search_results_page.get_pagination_header_text().startswith('Showing 0 out of 0 total')) self.assertTrue(search_results_page.get_pagination_header_text().startswith('Showing 0 out of 0 total'))
......
...@@ -24,7 +24,9 @@ define([ ...@@ -24,7 +24,9 @@ define([
it('can render itself', function () { it('can render itself', function () {
var testTeamData = TeamSpecHelpers.createMockTeamData(1, 5), var testTeamData = TeamSpecHelpers.createMockTeamData(1, 5),
teamsView = createTeamsView({ teamsView = createTeamsView({
teams: TeamSpecHelpers.createMockTeams(testTeamData) teams: TeamSpecHelpers.createMockTeams({
results: testTeamData
})
}); });
expect(teamsView.$('.teams-paging-header').text()).toMatch('Showing 1-5 out of 6 total'); expect(teamsView.$('.teams-paging-header').text()).toMatch('Showing 1-5 out of 6 total');
......
...@@ -29,19 +29,6 @@ define([ ...@@ -29,19 +29,6 @@ define([
return teamsTabView; return teamsTabView;
}; };
/**
* Filters out all team events from a list of requests.
*/
var removeTeamEvents = function (requests) {
return requests.filter(function (request) {
if (request.requestBody && request.requestBody.startsWith('event_type=edx.team')) {
return false;
} else {
return true;
}
});
};
beforeEach(function () { beforeEach(function () {
setFixtures('<div class="teams-content"></div>'); setFixtures('<div class="teams-content"></div>');
spyOn($.fn, 'focus'); spyOn($.fn, 'focus');
...@@ -233,25 +220,33 @@ define([ ...@@ -233,25 +220,33 @@ define([
options options
)); ));
}; };
var performSearch = function(requests, teamsTabView) {
teamsTabView.$('.search-field').val('foo');
teamsTabView.$('.action-search').click();
verifyTeamsRequest(requests, {
order_by: '',
text_search: 'foo'
});
AjaxHelpers.respondWithJson(requests, TeamSpecHelpers.createMockTeamsResponse({results: []}));
};
it('can search teams', function () { it('can search teams', function () {
var requests = AjaxHelpers.requests(this), var requests = AjaxHelpers.requests(this),
teamsTabView = createTeamsTabView(); teamsTabView = createTeamsTabView(),
requestCountBeforeSearch;
teamsTabView.browseTopic(TeamSpecHelpers.testTopicID); teamsTabView.browseTopic(TeamSpecHelpers.testTopicID);
verifyTeamsRequest(requests, { verifyTeamsRequest(requests, {
order_by: 'last_activity_at', order_by: 'last_activity_at',
text_search: '' text_search: ''
}); });
AjaxHelpers.respondWithJson(requests, {}); AjaxHelpers.respondWithJson(requests, {});
teamsTabView.$('.search-field').val('foo'); requestCountBeforeSearch = requests.length;
teamsTabView.$('.action-search').click(); performSearch(requests, teamsTabView);
verifyTeamsRequest(requests, {
order_by: '',
text_search: 'foo'
});
AjaxHelpers.respondWithJson(requests, {});
expect(teamsTabView.$('.page-title').text()).toBe('Team Search'); expect(teamsTabView.$('.page-title').text()).toBe('Team Search');
expect(teamsTabView.$('.page-description').text()).toBe('Showing results for "foo"'); expect(teamsTabView.$('.page-description').text()).toBe('Showing results for "foo"');
// Expect exactly one search request to be fired
expect(requests.length).toBe(requestCountBeforeSearch + 1);
}); });
it('can clear a search', function () { it('can clear a search', function () {
...@@ -259,17 +254,10 @@ define([ ...@@ -259,17 +254,10 @@ define([
teamsTabView = createTeamsTabView(); teamsTabView = createTeamsTabView();
teamsTabView.browseTopic(TeamSpecHelpers.testTopicID); teamsTabView.browseTopic(TeamSpecHelpers.testTopicID);
AjaxHelpers.respondWithJson(requests, {}); AjaxHelpers.respondWithJson(requests, {});
performSearch(requests, teamsTabView);
// Perform a search // Perform a search
teamsTabView.$('.search-field').val('foo'); performSearch(requests, teamsTabView);
teamsTabView.$('.action-search').click();
// Note: this is a bit of a hack -- without it the URL
// fragment won't be what it would be in the real
// app. This line sets the fragment without triggering
// callbacks, allowing teams_tab.js to detect the
// fragment correctly.
Backbone.history.navigate('topics/' + TeamSpecHelpers.testTopicID + '/search', {trigger: false});
AjaxHelpers.respondWithJson(requests, {});
// Clear the search and submit it again // Clear the search and submit it again
teamsTabView.$('.search-field').val(''); teamsTabView.$('.search-field').val('');
...@@ -283,16 +271,39 @@ define([ ...@@ -283,16 +271,39 @@ define([
expect(teamsTabView.$('.page-description').text()).toBe('Test description 1'); expect(teamsTabView.$('.page-description').text()).toBe('Test description 1');
}); });
it('does not switch to showing results when the search returns an error', function () { it('can navigate back to all teams from a search', function () {
var requests = AjaxHelpers.requests(this), var requests = AjaxHelpers.requests(this),
teamsTabView = createTeamsTabView(); teamsTabView = createTeamsTabView();
teamsTabView.browseTopic(TeamSpecHelpers.testTopicID); teamsTabView.browseTopic(TeamSpecHelpers.testTopicID);
AjaxHelpers.respondWithJson(requests, {}); AjaxHelpers.respondWithJson(requests, {});
// Perform a search // Perform a search
performSearch(requests, teamsTabView);
// Verify the breadcrumbs have a link back to the teams list, and click on it
expect(teamsTabView.$('.breadcrumbs a').length).toBe(2);
teamsTabView.$('.breadcrumbs a').last().click();
verifyTeamsRequest(requests, {
order_by: 'last_activity_at',
text_search: ''
});
AjaxHelpers.respondWithJson(requests, {});
expect(teamsTabView.$('.page-title').text()).toBe('Test Topic 1');
expect(teamsTabView.$('.page-description').text()).toBe('Test description 1');
});
it('does not switch to showing results when the search returns an error', function () {
var requests = AjaxHelpers.requests(this),
teamsTabView = createTeamsTabView();
teamsTabView.browseTopic(TeamSpecHelpers.testTopicID);
AjaxHelpers.respondWithJson(requests, {});
// Perform a search but respond with a 500
teamsTabView.$('.search-field').val('foo'); teamsTabView.$('.search-field').val('foo');
teamsTabView.$('.action-search').click(); teamsTabView.$('.action-search').click();
AjaxHelpers.respondWithError(requests); AjaxHelpers.respondWithError(requests);
// Verify that the team list is still shown
expect(teamsTabView.$('.page-title').text()).toBe('Test Topic 1'); expect(teamsTabView.$('.page-title').text()).toBe('Test Topic 1');
expect(teamsTabView.$('.page-description').text()).toBe('Test description 1'); expect(teamsTabView.$('.page-description').text()).toBe('Test description 1');
expect(teamsTabView.$('.search-field').val(), 'foo'); expect(teamsTabView.$('.search-field').val(), 'foo');
......
...@@ -44,7 +44,9 @@ define([ ...@@ -44,7 +44,9 @@ define([
it('can render itself', function () { it('can render itself', function () {
var testTeamData = TeamSpecHelpers.createMockTeamData(1, 5), var testTeamData = TeamSpecHelpers.createMockTeamData(1, 5),
teamsView = createTopicTeamsView({ teamsView = createTopicTeamsView({
teams: TeamSpecHelpers.createMockTeams(testTeamData), teams: TeamSpecHelpers.createMockTeams({
results: testTeamData
}),
teamMemberships: TeamSpecHelpers.createMockTeamMemberships([]) teamMemberships: TeamSpecHelpers.createMockTeamMemberships([])
}); });
......
...@@ -43,18 +43,22 @@ define([ ...@@ -43,18 +43,22 @@ define([
}); });
}; };
var createMockTeams = function(teamData) { var createMockTeamsResponse = function(options) {
if (!teamData) { return _.extend(
teamData = createMockTeamData(1, 5);
}
return new TeamCollection(
{ {
count: 6, count: 6,
num_pages: 2, num_pages: 2,
current_page: 1, current_page: 1,
start: 0, start: 0,
results: teamData results: createMockTeamData(1, 5)
}, },
options
);
};
var createMockTeams = function(options) {
return new TeamCollection(
createMockTeamsResponse(options),
{ {
teamEvents: teamEvents, teamEvents: teamEvents,
course_id: testCourseID, course_id: testCourseID,
...@@ -325,6 +329,7 @@ define([ ...@@ -325,6 +329,7 @@ define([
testTeamDiscussionID: testTeamDiscussionID, testTeamDiscussionID: testTeamDiscussionID,
testContext: testContext, testContext: testContext,
createMockTeamData: createMockTeamData, createMockTeamData: createMockTeamData,
createMockTeamsResponse: createMockTeamsResponse,
createMockTeams: createMockTeams, createMockTeams: createMockTeams,
createMockTeamMembershipsData: createMockTeamMembershipsData, createMockTeamMembershipsData: createMockTeamMembershipsData,
createMockTeamMemberships: createMockTeamMemberships, createMockTeamMemberships: createMockTeamMemberships,
......
...@@ -214,6 +214,7 @@ ...@@ -214,6 +214,7 @@
view.mainView = view.createTeamsListView({ view.mainView = view.createTeamsListView({
topic: topic, topic: topic,
collection: view.teamsCollection, collection: view.teamsCollection,
breadcrumbs: view.createBreadcrumbs(topic),
title: gettext('Team Search'), title: gettext('Team Search'),
description: interpolate( description: interpolate(
gettext('Showing results for "%(searchString)s"'), gettext('Showing results for "%(searchString)s"'),
...@@ -337,6 +338,7 @@ ...@@ -337,6 +338,7 @@
var teamsView = view.createTeamsListView({ var teamsView = view.createTeamsListView({
topic: topic, topic: topic,
collection: collection, collection: collection,
breadcrumbs: view.createBreadcrumbs(),
showSortControls: true showSortControls: true
}); });
deferred.resolve(teamsView); deferred.resolve(teamsView);
...@@ -368,7 +370,7 @@ ...@@ -368,7 +370,7 @@
headerActionsView: searchFieldView, headerActionsView: searchFieldView,
title: options.title, title: options.title,
description: options.description, description: options.description,
breadcrumbs: this.createBreadcrumbs() breadcrumbs: options.breadcrumbs
}), }),
searchUrl = 'topics/' + topic.get('id') + '/search'; searchUrl = 'topics/' + topic.get('id') + '/search';
// Listen to requests to sync the collection and redirect it as follows: // Listen to requests to sync the collection and redirect it as follows:
...@@ -378,6 +380,11 @@ ...@@ -378,6 +380,11 @@
// 3. Otherwise, do nothing and remain on the current page. // 3. Otherwise, do nothing and remain on the current page.
// Note: Backbone makes this a no-op if redirecting to the current page. // Note: Backbone makes this a no-op if redirecting to the current page.
this.listenTo(collection, 'sync', function() { this.listenTo(collection, 'sync', function() {
// Clear the stale flag here as by definition the collection is up-to-date,
// and the flag itself isn't guaranteed to be cleared yet. This is to ensure
// that the collection doesn't unnecessarily get refreshed again.
collection.isStale = false;
if (collection.searchString) { if (collection.searchString) {
Backbone.history.navigate(searchUrl, {trigger: true}); Backbone.history.navigate(searchUrl, {trigger: true});
} else if (Backbone.history.getFragment() === searchUrl) { } else if (Backbone.history.getFragment() === searchUrl) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment