Commit 3b55d882 by Daniel Friedman

Implement paginated Team Topics UI

Authors:
  - Dan Friedman
  - Ben McMorran
  - Peter Fogg

TNL-1892
parent 351e491c
"""
Common mixin for paginated UIs.
"""
from selenium.webdriver.common.keys import Keys
class PaginatedUIMixin(object):
"""Common methods used for paginated UI."""
PAGINATION_FOOTER_CSS = 'nav.bottom'
PAGE_NUMBER_INPUT_CSS = 'input#page-number-input'
NEXT_PAGE_BUTTON_CSS = 'button.next-page-link'
PREVIOUS_PAGE_BUTTON_CSS = 'button.previous-page-link'
PAGINATION_HEADER_TEXT_CSS = 'div.search-tools'
CURRENT_PAGE_NUMBER_CSS = 'span.current-page'
def get_pagination_header_text(self):
"""Return the text showing which items the user is currently viewing."""
return self.q(css=self.PAGINATION_HEADER_TEXT_CSS).text[0]
def pagination_controls_visible(self):
"""Return true if the pagination controls in the footer are visible."""
footer_nav = self.q(css=self.PAGINATION_FOOTER_CSS).results[0]
# The footer element itself is non-generic, so check above it
footer_el = footer_nav.find_element_by_xpath('..')
return 'hidden' not in footer_el.get_attribute('class').split()
def get_current_page_number(self):
"""Return the the current page number."""
return int(self.q(css=self.CURRENT_PAGE_NUMBER_CSS).text[0])
def go_to_page(self, page_number):
"""Go to the given page_number in the paginated list results."""
self.q(css=self.PAGE_NUMBER_INPUT_CSS).results[0].send_keys(unicode(page_number), Keys.ENTER)
self.wait_for_ajax()
def press_next_page_button(self):
"""Press the next page button in the paginated list results."""
self.q(css=self.NEXT_PAGE_BUTTON_CSS).click()
self.wait_for_ajax()
def press_previous_page_button(self):
"""Press the previous page button in the paginated list results."""
self.q(css=self.PREVIOUS_PAGE_BUTTON_CSS).click()
self.wait_for_ajax()
def is_next_page_button_enabled(self):
"""Return whether the 'next page' button can be clicked."""
return self.is_enabled(self.NEXT_PAGE_BUTTON_CSS)
def is_previous_page_button_enabled(self):
"""Return whether the 'previous page' button can be clicked."""
return self.is_enabled(self.PREVIOUS_PAGE_BUTTON_CSS)
def is_enabled(self, css):
"""Return whether the given element is not disabled."""
return 'is-disabled' not in self.q(css=css).attrs('class')[0]
......@@ -4,6 +4,11 @@ Teams page.
"""
from .course_page import CoursePage
from ..common.paging import PaginatedUIMixin
TOPIC_CARD_CSS = 'div.wrapper-card-core'
BROWSE_BUTTON_CSS = 'a.nav-item[data-index="1"]'
class TeamsPage(CoursePage):
......@@ -24,3 +29,27 @@ class TeamsPage(CoursePage):
description="Body text is present"
)
return self.q(css=main_page_content_css).text[0]
def browse_topics(self):
""" View the Browse tab of the Teams page. """
self.q(css=BROWSE_BUTTON_CSS).click()
class BrowseTopicsPage(CoursePage, PaginatedUIMixin):
"""
The 'Browse' tab of the Teams page.
"""
url_path = "teams/#browse"
def is_browser_on_page(self):
"""Check if the Browse tab is being viewed."""
button_classes = self.q(css=BROWSE_BUTTON_CSS).attrs('class')
if len(button_classes) == 0:
return False
return 'is-active' in button_classes[0]
@property
def topic_cards(self):
"""Return a list of the topic cards present on the page."""
return self.q(css=TOPIC_CARD_CSS).results
......@@ -17,7 +17,7 @@ class PaginatedMixin(object):
To specify a specific arrow, pass an iterable with a single element, 'next' or 'previous'.
"""
return all([
self.q(css='nav.%s * a.%s-page-link.is-disabled' % (position, arrow))
self.q(css='nav.%s * .%s-page-link.is-disabled' % (position, arrow))
for arrow in arrows
])
......@@ -25,14 +25,14 @@ class PaginatedMixin(object):
"""
Clicks one of the forward nav buttons. Position can be 'top' or 'bottom'.
"""
self.q(css='nav.%s * a.previous-page-link' % position)[0].click()
self.q(css='nav.%s * .previous-page-link' % position)[0].click()
self.wait_until_ready()
def move_forward(self, position):
"""
Clicks one of the forward nav buttons. Position can be 'top' or 'bottom'.
"""
self.q(css='nav.%s * a.next-page-link' % position)[0].click()
self.q(css='nav.%s * .next-page-link' % position)[0].click()
self.wait_until_ready()
def go_to_page(self, number):
......
......@@ -2,7 +2,7 @@
Acceptance tests for the teams feature.
"""
from ..helpers import UniqueCourseTest
from ...pages.lms.teams import TeamsPage
from ...pages.lms.teams import TeamsPage, BrowseTopicsPage
from nose.plugins.attrib import attr
from ...fixtures.course import CourseFixture
from ...pages.lms.tab_nav import TabNavPage
......@@ -21,7 +21,10 @@ class TeamsTabTest(UniqueCourseTest):
self.tab_nav = TabNavPage(self.browser)
self.course_info_page = CourseInfoPage(self.browser, self.course_id)
self.teams_page = TeamsPage(self.browser, self.course_id)
self.test_topic = {u"name": u"a topic", u"description": u"test topic", u"id": 0}
def create_topics(self, num_topics):
"""Create `num_topics` test topics."""
return [{u"description": str(i), u"name": str(i), u"id": i} for i in xrange(num_topics)]
def set_team_configuration(self, configuration, enroll_in_course=True, global_staff=False):
"""
......@@ -75,11 +78,15 @@ class TeamsTabTest(UniqueCourseTest):
"""
Scenario: teams tab should not be present if student is not enrolled in the course
Given there is a course with team configuration and topics
And I am not enrolled in that course, and am not global staff
When I view the course info page
Then I should not see the Teams tab
"""
self.set_team_configuration({u"max_team_size": 10, u"topics": [self.test_topic]}, enroll_in_course=False)
self.set_team_configuration(
{u"max_team_size": 10, u"topics": self.create_topics(1)},
enroll_in_course=False
)
self.verify_teams_present(False)
def test_teams_enabled(self):
......@@ -90,7 +97,7 @@ class TeamsTabTest(UniqueCourseTest):
Then I should see the Teams tab
And the correct content should be on the page
"""
self.set_team_configuration({u"max_team_size": 10, u"topics": [self.test_topic]})
self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(1)})
self.verify_teams_present(True)
def test_teams_enabled_global_staff(self):
......@@ -103,6 +110,121 @@ class TeamsTabTest(UniqueCourseTest):
And the correct content should be on the page
"""
self.set_team_configuration(
{u"max_team_size": 10, u"topics": [self.test_topic]}, enroll_in_course=False, global_staff=True
{u"max_team_size": 10, u"topics": self.create_topics(1)},
enroll_in_course=False,
global_staff=True
)
self.verify_teams_present(True)
@attr('shard_5')
class BrowseTopicsTest(TeamsTabTest):
"""
Tests for the Browse tab of the Teams page.
"""
def setUp(self):
super(BrowseTopicsTest, self).setUp()
self.topics_page = BrowseTopicsPage(self.browser, self.course_id)
def test_list_topics(self):
"""
Scenario: a list of topics should be visible in the "Browse" tab
Given I am enrolled in a course with team configuration and topics
When I visit the Teams page
And I browse topics
Then I should see a list of topics for the course
"""
self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(2)})
self.topics_page.visit()
self.assertEqual(len(self.topics_page.topic_cards), 2)
self.assertEqual(self.topics_page.get_pagination_header_text(), 'Showing 1-2 out of 2 total')
self.assertFalse(self.topics_page.pagination_controls_visible())
self.assertFalse(self.topics_page.is_previous_page_button_enabled())
self.assertFalse(self.topics_page.is_next_page_button_enabled())
def test_topic_pagination(self):
"""
Scenario: a list of topics should be visible in the "Browse" tab, paginated 12 per page
Given I am enrolled in a course with team configuration and topics
When I visit the Teams page
And I browse topics
Then I should see only the first 12 topics
"""
self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(20)})
self.topics_page.visit()
self.assertEqual(len(self.topics_page.topic_cards), 12)
self.assertEqual(self.topics_page.get_pagination_header_text(), 'Showing 1-12 out of 20 total')
self.assertTrue(self.topics_page.pagination_controls_visible())
self.assertFalse(self.topics_page.is_previous_page_button_enabled())
self.assertTrue(self.topics_page.is_next_page_button_enabled())
def test_go_to_numbered_page(self):
"""
Scenario: topics should be able to be navigated by page number
Given I am enrolled in a course with team configuration and topics
When I visit the Teams page
And I browse topics
And I enter a valid page number in the page number input
Then I should see that page of topics
"""
self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(25)})
self.topics_page.visit()
self.topics_page.go_to_page(3)
self.assertEqual(len(self.topics_page.topic_cards), 1)
self.assertTrue(self.topics_page.is_previous_page_button_enabled())
self.assertFalse(self.topics_page.is_next_page_button_enabled())
def test_go_to_invalid_page(self):
"""
Scenario: browsing topics should not respond to invalid page numbers
Given I am enrolled in a course with team configuration and topics
When I visit the Teams page
And I browse topics
And I enter an invalid page number in the page number input
Then I should stay on the current page
"""
self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(13)})
self.topics_page.visit()
self.topics_page.go_to_page(3)
self.assertEqual(self.topics_page.get_current_page_number(), 1)
def test_page_navigation_buttons(self):
"""
Scenario: browsing topics should not respond to invalid page numbers
Given I am enrolled in a course with team configuration and topics
When I visit the Teams page
And I browse topics
When I press the next page button
Then I should move to the next page
When I press the previous page button
Then I should move to the previous page
"""
self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(13)})
self.topics_page.visit()
self.topics_page.press_next_page_button()
self.assertEqual(len(self.topics_page.topic_cards), 1)
self.assertEqual(self.topics_page.get_pagination_header_text(), 'Showing 13-13 out of 13 total')
self.topics_page.press_previous_page_button()
self.assertEqual(len(self.topics_page.topic_cards), 12)
self.assertEqual(self.topics_page.get_pagination_header_text(), 'Showing 1-12 out of 13 total')
def test_topic_description_truncation(self):
"""
Scenario: excessively long topic descriptions should be truncated so
as to fit within a topic card.
Given I am enrolled in a course with a team configuration and a topic
with a long description
When I visit the Teams page
And I browse topics
Then I should see a truncated topic description
"""
initial_description = "A" + " really" * 50 + " long description"
self.set_team_configuration(
{u"max_team_size": 1, u"topics": [{"name": "", "id": "", "description": initial_description}]}
)
self.topics_page.visit()
truncated_description = self.topics_page.topic_cards[0].text
self.assertLess(len(truncated_description), len(initial_description))
self.assertTrue(truncated_description.endswith('...'))
self.assertIn(truncated_description.split('...')[0], initial_description)
;(function (define) {
'use strict';
define(['common/js/components/collections/paging_collection', 'teams/js/models/topic', 'gettext'],
function(PagingCollection, TopicModel, gettext) {
var TopicCollection = PagingCollection.extend({
initialize: function(topics, options) {
PagingCollection.prototype.initialize.call(this);
this.course_id = options.course_id;
this.perPage = topics.results.length;
this.server_api['course_id'] = function () { return encodeURIComponent(this.course_id); };
this.server_api['order_by'] = function () { return this.sortField; };
delete this.server_api['sort_order']; // Sort order is not specified for the Team API
this.registerSortableField('name', gettext('name'));
this.registerSortableField('team_count', gettext('team count'));
},
model: TopicModel
});
return TopicCollection;
});
}).call(this, define || RequireJS.define);
......@@ -13,5 +13,5 @@
}
});
return Topic;
})
});
}).call(this, define || RequireJS.define);
......@@ -7,7 +7,7 @@ define(["jquery", "backbone", "teams/js/teams_tab_factory"],
beforeEach(function() {
setFixtures('<section class="teams-content"></section>');
teamsTab = new TeamsTabFactory();
teamsTab = new TeamsTabFactory({results: []}, '', 'edX/DemoX/Demo_Course');
});
afterEach(function() {
......
define(['URI', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/collections/topic'],
function (URI, _, AjaxHelpers, TopicCollection) {
'use strict';
describe('TopicCollection', function () {
var topicCollection;
beforeEach(function () {
topicCollection = new TopicCollection(
{
"count": 6,
"num_pages": 2,
"current_page": 1,
"start": 0,
"results": [
{
"description": "asdf description",
"name": "asdf",
"id": "_asdf"
},
{
"description": "bar description",
"name": "bar",
"id": "_bar"
},
{
"description": "baz description",
"name": "baz",
"id": "_baz"
},
{
"description": "foo description",
"name": "foo",
"id": "_foo"
},
{
"description": "qwerty description",
"name": "qwerty",
"id": "_qwerty"
}
],
"sort_order": "name"
},
{course_id: 'my/course/id', parse: true});
});
var testRequestParam = function (self, param, value) {
var requests = AjaxHelpers.requests(self),
url,
params;
topicCollection.fetch();
expect(requests.length).toBe(1);
url = new URI(requests[0].url);
params = url.query(true);
expect(params[param]).toBe(value);
};
it('sets its perPage based on initial page size', function () {
expect(topicCollection.perPage).toBe(5);
});
it('sorts by name', function () {
testRequestParam(this, 'order_by', 'name');
});
it('passes a course_id to the server', function () {
testRequestParam(this, 'course_id', 'my/course/id');
});
it('URL encodes its course_id ', function () {
topicCollection.course_id = 'my+course+id';
testRequestParam(this, 'course_id', 'my+course+id');
});
});
});
define([
'common/js/spec_helpers/ajax_helpers', 'teams/js/collections/topic', 'teams/js/views/topics'
], function (AjaxHelpers, TopicCollection, TopicsView) {
'use strict';
describe('TopicsView', function () {
var initialTopics, topicCollection, topicsView, nextPageButtonCss;
nextPageButtonCss = '.next-page-link';
function generateTopics(startIndex, stopIndex) {
return _.map(_.range(startIndex, stopIndex + 1), function (i) {
return {
"description": "description " + i,
"name": "topic " + i,
"id": "id " + i,
"team_count": 0
};
});
}
beforeEach(function () {
setFixtures('<div class="topics-container"></div>');
initialTopics = generateTopics(1, 5);
topicCollection = new TopicCollection(
{
"count": 6,
"num_pages": 2,
"current_page": 1,
"start": 0,
"results": initialTopics
},
{course_id: 'my/course/id', parse: true}
);
topicsView = new TopicsView({el: '.topics-container', collection: topicCollection}).render();
});
/**
* Verify that the topics view's header reflects the page we're currently viewing.
* @param matchString the header we expect to see
*/
function expectHeader(matchString) {
expect(topicsView.$('.topics-paging-header').text()).toMatch(matchString);
}
/**
* Verify that the topics list view renders the expected topics
* @param expectedTopics an array of topic objects we expect to see
*/
function expectTopics(expectedTopics) {
var topicCards;
topicCards = topicsView.$('.topic-card');
_.each(expectedTopics, function (topic, index) {
var currentCard = topicCards.eq(index);
expect(currentCard.text()).toMatch(topic.name);
expect(currentCard.text()).toMatch(topic.description);
expect(currentCard.text()).toMatch(topic.team_count + ' Teams');
});
}
/**
* Verify that the topics footer reflects the current pagination
* @param options a parameters hash containing:
* - currentPage: the one-indexed page we expect to be viewing
* - totalPages: the total number of pages to page through
* - isHidden: whether the footer is expected to be visible
*/
function expectFooter(options) {
var footerEl = topicsView.$('.topics-paging-footer');
expect(footerEl.text())
.toMatch(new RegExp(options.currentPage + '\\s+out of\\s+\/\\s+' + topicCollection.totalPages));
expect(footerEl.hasClass('hidden')).toBe(options.isHidden);
}
it('can render the first of many pages', function () {
expectHeader('Showing 1-5 out of 6 total');
expectTopics(initialTopics);
expectFooter({currentPage: 1, totalPages: 2, isHidden: false});
});
it('can render the only page', function () {
initialTopics = generateTopics(1, 1);
topicCollection.set(
{
"count": 1,
"num_pages": 1,
"current_page": 1,
"start": 0,
"results": initialTopics
},
{parse: true}
);
expectHeader('Showing 1 out of 1 total');
expectTopics(initialTopics);
expectFooter({currentPage: 1, totalPages: 1, isHidden: true});
});
it('can change to the next page', function () {
var requests = AjaxHelpers.requests(this),
newTopics = generateTopics(1, 1);
expectHeader('Showing 1-5 out of 6 total');
expectTopics(initialTopics);
expectFooter({currentPage: 1, totalPages: 2, isHidden: false});
expect(requests.length).toBe(0);
topicsView.$(nextPageButtonCss).click();
expect(requests.length).toBe(1);
AjaxHelpers.respondWithJson(requests, {
"count": 6,
"num_pages": 2,
"current_page": 2,
"start": 5,
"results": newTopics
});
expectHeader('Showing 6-6 out of 6 total');
expectTopics(newTopics);
expectFooter({currentPage: 2, totalPages: 2, isHidden: false});
});
it('can change to the previous page', function () {
var requests = AjaxHelpers.requests(this),
previousPageTopics;
initialTopics = generateTopics(1, 1);
topicCollection.set(
{
"count": 6,
"num_pages": 2,
"current_page": 2,
"start": 5,
"results": initialTopics
},
{parse: true}
);
expectHeader('Showing 6-6 out of 6 total');
expectTopics(initialTopics);
expectFooter({currentPage: 2, totalPages: 2, isHidden: false});
topicsView.$('.previous-page-link').click();
previousPageTopics = generateTopics(1, 5);
AjaxHelpers.respondWithJson(requests, {
"count": 6,
"num_pages": 2,
"current_page": 1,
"start": 0,
"results": previousPageTopics
});
expectHeader('Showing 1-5 out of 6 total');
expectTopics(previousPageTopics);
expectFooter({currentPage: 1, totalPages: 2, isHidden: false});
});
it('sets focus for screen readers', function () {
var requests = AjaxHelpers.requests(this);
spyOn($.fn, 'focus');
topicsView.$(nextPageButtonCss).click();
AjaxHelpers.respondWithJson(requests, {
"count": 6,
"num_pages": 2,
"current_page": 2,
"start": 5,
"results": generateTopics(1, 1)
});
expect(topicsView.$('.sr-is-focusable').focus).toHaveBeenCalled();
});
it('does not change on server error', function () {
var requests = AjaxHelpers.requests(this),
expectInitialState = function () {
expectHeader('Showing 1-5 out of 6 total');
expectTopics(initialTopics);
expectFooter({currentPage: 1, totalPages: 2, isHidden: false});
};
expectInitialState();
topicsView.$(nextPageButtonCss).click();
requests[0].respond(500);
expectInitialState();
});
});
});
;(function (define) {
'use strict';
define(['jquery','teams/js/views/teams_tab'],
function ($, TeamsTabView) {
return function () {
define(['jquery', 'teams/js/views/teams_tab', 'teams/js/collections/topic'],
function ($, TeamsTabView, TopicCollection) {
return function (topics, topics_url, course_id) {
var topicCollection = new TopicCollection(topics, {url: topics_url, course_id: course_id, parse: true});
topicCollection.bootstrap();
var view = new TeamsTabView({
el: $('.teams-content')
el: $('.teams-content'),
topicCollection: topicCollection
});
view.render();
};
......
......@@ -6,10 +6,11 @@
'gettext',
'js/components/header/views/header',
'js/components/header/models/header',
'js/components/tabbed/views/tabbed_view'],
function (Backbone, _, gettext, HeaderView, HeaderModel, TabbedView) {
'js/components/tabbed/views/tabbed_view',
'teams/js/views/topics'],
function (Backbone, _, gettext, HeaderView, HeaderModel, TabbedView, TopicsView) {
var TeamTabView = Backbone.View.extend({
initialize: function() {
initialize: function(options) {
this.headerModel = new HeaderModel({
description: gettext("Course teams are organized into topics created by course instructors. Try to join others in an existing team before you decide to create a new team!"),
title: gettext("Teams")
......@@ -24,7 +25,7 @@
},
render: function () {
this.$el.text(this.text)
this.$el.text(this.text);
}
});
this.tabbedView = new TabbedView({
......@@ -35,7 +36,9 @@
}, {
title: gettext('Browse'),
url: 'browse',
view: new TempTabView({text: 'Browse team topics here.'})
view: new TopicsView({
collection: options.topicCollection
})
}]
});
Backbone.history.start();
......
;(function (define) {
'use strict';
define([
'backbone',
'underscore',
'gettext',
'common/js/components/views/list',
'common/js/components/views/paging_header',
'common/js/components/views/paging_footer',
'teams/js/views/topic_card',
'text!teams/templates/topics.underscore'
], function (Backbone, _, gettext, ListView, PagingHeader, PagingFooterView, TopicCardView, topics_template) {
var TopicsListView = ListView.extend({
tagName: 'div',
className: 'topics-container',
itemViewClass: TopicCardView
});
var TopicsView = Backbone.View.extend({
initialize: function() {
this.listView = new TopicsListView({collection: this.collection});
this.headerView = new PagingHeader({collection: this.collection});
this.pagingFooterView = new PagingFooterView({
collection: this.collection, hideWhenOnePage: true
});
// Focus top of view for screen readers
this.collection.on('page_changed', function () {
this.$('.sr-is-focusable.sr-topics-view').focus();
}, this);
},
render: function() {
this.$el.html(_.template(topics_template));
this.assign(this.listView, '.topics-list');
this.assign(this.headerView, '.topics-paging-header');
this.assign(this.pagingFooterView, '.topics-paging-footer');
return this;
},
/**
* Helper method to render subviews and re-bind events.
*
* Borrowed from http://ianstormtaylor.com/rendering-views-in-backbonejs-isnt-always-simple/
*
* @param view The Backbone view to render
* @param selector The string CSS selector which the view should attach to
*/
assign: function(view, selector) {
view.setElement(this.$(selector)).render();
}
});
return TopicsView;
});
}).call(this, define || RequireJS.define);
<div class="sr-is-focusable sr-topics-view" tabindex="-1"></div>
<div class="topics-paging-header"></div>
<div class="topics-list"></div>
<div class="topics-paging-footer"></div>
## mako
<%! import json %>
<%! from django.utils.translation import ugettext as _ %>
<%! from openedx.core.lib.json_utils import EscapedEdxJSONEncoder %>
<%namespace name='static' file='/static_content.html'/>
<%inherit file="/main.html" />
......@@ -22,7 +24,7 @@
<script type="text/javascript">
(function (require) {
require(['teams/js/teams_tab_factory'], function (TeamsTabFactory) {
var pageView = new TeamsTabFactory();
new TeamsTabFactory(${ json.dumps(topics, cls=EscapedEdxJSONEncoder) }, '${ topics_url }', '${ unicode(course.id) }');
});
}).call(this, require || RequireJS.require);
</script>
......
......@@ -8,7 +8,7 @@
function (Backbone, _, $, tabbedViewTemplate, tabTemplate) {
var TabbedView = Backbone.View.extend({
events: {
'click .nav-item': 'switchTab'
'click .nav-item[role="tab"]': 'switchTab'
},
template: _.template(tabbedViewTemplate),
......@@ -45,9 +45,8 @@
view = tab.view;
this.$('a.is-active').removeClass('is-active').attr('aria-selected', 'false');
this.$('a[data-index='+index+']').addClass('is-active').attr('aria-selected', 'true');
view.render();
this.$('.page-content-main').html(view.$el.html());
this.$('.sr-is-focusable').focus();
view.setElement(this.$('.page-content-main')).render();
this.$('.sr-is-focusable.sr-tab').focus();
this.router.navigate(tab.url, {replace: true});
},
......
......@@ -40,7 +40,7 @@
});
it('can render itself', function () {
expect(view.$el.html()).toContain('<nav class="page-content-nav" role="tablist">')
expect(view.$el.html()).toContain('<nav class="page-content-nav"');
});
it('shows its first tab by default', function () {
......@@ -77,6 +77,12 @@
view.$('.nav-item[data-index=1]').click();
expect(Backbone.history.navigate).toHaveBeenCalledWith('test 2', {replace: true});
});
it('sets focus for screen readers', function () {
spyOn($.fn, 'focus');
view.$('.nav-item[data-index=1]').click();
expect(view.$('.sr-is-focusable.sr-tab').focus).toHaveBeenCalled();
});
});
}
);
......
......@@ -614,6 +614,8 @@
// Run the LMS tests
'lms/include/teams/js/spec/teams_factory_spec.js',
'lms/include/teams/js/spec/topic_card_spec.js',
'lms/include/teams/js/spec/topic_collection_spec.js',
'lms/include/teams/js/spec/topics_spec.js',
'lms/include/js/spec/components/header/header_spec.js',
'lms/include/js/spec/components/tabbed/tabbed_view_spec.js',
'lms/include/js/spec/components/card/card_spec.js',
......
<div class="page-content">
<nav class="page-content-nav" role="tablist"></nav>
<div class="sr-is-focusable" tabindex="-1"></div>
<nav class="page-content-nav" aria-label="Teams"></nav>
<div class="sr-is-focusable sr-tab" tabindex="-1"></div>
<div class="page-content-main"></div>
</div>
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