diff --git a/common/test/acceptance/fixtures/__init__.py b/common/test/acceptance/fixtures/__init__.py index 2cccb14c63..bbe146b430 100644 --- a/common/test/acceptance/fixtures/__init__.py +++ b/common/test/acceptance/fixtures/__init__.py @@ -18,5 +18,5 @@ COMMENTS_STUB_URL = os.environ.get('comments_url', 'http://localhost:4567') # Get the URL of the EdxNotes service stub used in the test EDXNOTES_STUB_URL = os.environ.get('edxnotes_url', 'http://localhost:8042') -# Get the URL of the EdxNotes service stub used in the test +# Get the URL of the Programs service stub used in the test PROGRAMS_STUB_URL = os.environ.get('programs_url', 'http://localhost:8090') diff --git a/common/test/acceptance/fixtures/config.py b/common/test/acceptance/fixtures/config.py index 402ca030b1..1a030415a4 100644 --- a/common/test/acceptance/fixtures/config.py +++ b/common/test/acceptance/fixtures/config.py @@ -9,7 +9,7 @@ from lazy import lazy from . import LMS_BASE_URL -class ConfigModelFixureError(Exception): +class ConfigModelFixtureError(Exception): """ Error occurred while configuring the stub XQueue. """ @@ -41,7 +41,7 @@ class ConfigModelFixture(object): ) if not response.ok: - raise ConfigModelFixureError( + raise ConfigModelFixtureError( "Could not configure url '{}'. response: {} - {}".format( self._api_base, response, @@ -53,7 +53,7 @@ class ConfigModelFixture(object): def session_cookies(self): """ Log in as a staff user, then return the cookies for the session (as a dict) - Raises a `ConfigModelFixureError` if the login fails. + Raises a `ConfigModelFixtureError` if the login fails. """ return {key: val for key, val in self.session.cookies.items()} @@ -92,4 +92,4 @@ class ConfigModelFixture(object): else: msg = "Could not log in to use ConfigModel restful API. Status code: {0}".format(response.status_code) - raise ConfigModelFixureError(msg) + raise ConfigModelFixtureError(msg) diff --git a/common/test/acceptance/fixtures/programs.py b/common/test/acceptance/fixtures/programs.py index 485b691beb..72ac918248 100644 --- a/common/test/acceptance/fixtures/programs.py +++ b/common/test/acceptance/fixtures/programs.py @@ -1,12 +1,17 @@ """ Tools to create programs-related data for use in bok choy tests. """ - +from collections import namedtuple import json + import factory import requests from . import PROGRAMS_STUB_URL +from .config import ConfigModelFixture + + +FakeProgram = namedtuple('FakeProgram', ['name', 'status', 'org_key', 'course_id']) class Program(factory.Factory): @@ -17,12 +22,14 @@ class Program(factory.Factory): model = dict id = factory.Sequence(lambda n: n) # pylint: disable=invalid-name - name = "dummy-program-name" - subtitle = "dummy-program-subtitle" - category = "xseries" - status = "unpublished" + name = 'dummy-program-name' + subtitle = 'dummy-program-subtitle' + category = 'xseries' + status = 'unpublished' + marketing_slug = factory.Sequence(lambda n: 'slug-{}'.format(n)) # pylint: disable=unnecessary-lambda organizations = [] course_codes = [] + banner_image_urls = {} class Organization(factory.Factory): @@ -32,8 +39,30 @@ class Organization(factory.Factory): class Meta(object): model = dict - key = "dummyX" - display_name = "dummy-org-display-name" + key = 'dummyX' + display_name = 'dummy-org-display-name' + + +class CourseCode(factory.Factory): + """ + Factory for stubbing nested course code resources from the Programs API (v1). + """ + class Meta(object): + model = dict + + display_name = 'dummy-org-display-name' + run_modes = [] + + +class RunMode(factory.Factory): + """ + Factory for stubbing nested run mode resources from the Programs API (v1). + """ + class Meta(object): + model = dict + + course_key = 'org/course/run' + mode_slug = 'verified' class ProgramsFixture(object): @@ -41,16 +70,24 @@ class ProgramsFixture(object): Interface to set up mock responses from the Programs stub server. """ - def install_programs(self, program_values): + def install_programs(self, fake_programs): """ Sets the response data for the programs list endpoint. - At present, `program_values` needs to be a sequence of sequences of (program_name, org_key). + At present, `fake_programs` must be a iterable of FakeProgram named tuples. """ programs = [] - for program_name, org_key in program_values: - org = Organization(key=org_key) - program = Program(name=program_name, organizations=[org]) + for program in fake_programs: + run_mode = RunMode(course_key=program.course_id) + course_code = CourseCode(run_modes=[run_mode]) + org = Organization(key=program.org_key) + + program = Program( + name=program.name, + status=program.status, + organizations=[org], + course_codes=[course_code] + ) programs.append(program) api_result = {'results': programs} @@ -59,3 +96,24 @@ class ProgramsFixture(object): '{}/set_config'.format(PROGRAMS_STUB_URL), data={'programs': json.dumps(api_result)}, ) + + +class ProgramsConfigMixin(object): + """Mixin providing a method used to configure the programs feature.""" + def set_programs_api_configuration(self, is_enabled=False, api_version=1, api_url=PROGRAMS_STUB_URL, + js_path='/js', css_path='/css'): + """Dynamically adjusts the Programs config model during tests.""" + ConfigModelFixture('/config/programs', { + 'enabled': is_enabled, + 'api_version_number': api_version, + 'internal_service_url': api_url, + 'public_service_url': api_url, + 'authoring_app_js_path': js_path, + 'authoring_app_css_path': css_path, + 'cache_ttl': 0, + 'enable_student_dashboard': is_enabled, + 'enable_studio_tab': is_enabled, + 'enable_certification': is_enabled, + 'xseries_ad_enabled': is_enabled, + 'program_listing_enabled': is_enabled, + }).install() diff --git a/common/test/acceptance/pages/lms/programs.py b/common/test/acceptance/pages/lms/programs.py new file mode 100644 index 0000000000..08efd8558a --- /dev/null +++ b/common/test/acceptance/pages/lms/programs.py @@ -0,0 +1,22 @@ +"""LMS-hosted Programs pages""" +from bok_choy.page_object import PageObject + +from . import BASE_URL + + +class ProgramListingPage(PageObject): + """Program listing page.""" + url = BASE_URL + '/dashboard/programs/' + + def is_browser_on_page(self): + return self.q(css='.program-list-wrapper').present + + @property + def are_cards_present(self): + """Check whether program cards are present.""" + return self.q(css='.program-card').present + + @property + def is_sidebar_present(self): + """Check whether sidebar is present.""" + return self.q(css='.sidebar').present diff --git a/common/test/acceptance/tests/lms/test_programs.py b/common/test/acceptance/tests/lms/test_programs.py new file mode 100644 index 0000000000..2d69fbcc14 --- /dev/null +++ b/common/test/acceptance/tests/lms/test_programs.py @@ -0,0 +1,102 @@ +"""Acceptance tests for LMS-hosted Programs pages""" +from nose.plugins.attrib import attr + +from ...fixtures.programs import FakeProgram, ProgramsFixture, ProgramsConfigMixin +from ...fixtures.course import CourseFixture +from ..helpers import UniqueCourseTest +from ...pages.lms.auto_auth import AutoAuthPage +from ...pages.lms.programs import ProgramListingPage + + +class ProgramListingPageBase(ProgramsConfigMixin, UniqueCourseTest): + """Base class used for program listing page tests.""" + def setUp(self): + super(ProgramListingPageBase, self).setUp() + + self.set_programs_api_configuration(is_enabled=True) + self.listing_page = ProgramListingPage(self.browser) + + def stub_api(self, course_id=None): + """Stub out the programs API with fake data.""" + name = 'Fake Program' + status = 'active' + org_key = self.course_info['org'] + course_id = course_id if course_id else self.course_id + + ProgramsFixture().install_programs([ + FakeProgram(name=name, status=status, org_key=org_key, course_id=course_id), + ]) + + def auth(self, enroll=True): + """Authenticate, enrolling the user in the configured course if requested.""" + CourseFixture(**self.course_info).install() + + course_id = self.course_id if enroll else None + AutoAuthPage(self.browser, course_id=course_id).visit() + + +class ProgramListingPageTest(ProgramListingPageBase): + """Verify user-facing behavior of the program listing page.""" + def test_no_enrollments(self): + """Verify that no cards appear when the user has no enrollments.""" + self.stub_api() + self.auth(enroll=False) + self.listing_page.visit() + + self.assertTrue(self.listing_page.is_sidebar_present) + self.assertFalse(self.listing_page.are_cards_present) + + def test_no_programs(self): + """ + Verify that no cards appear when the user has enrollments + but none are included in an active program. + """ + course_id = self.course_id.replace( + self.course_info['run'], + 'other_run' + ) + self.stub_api(course_id=course_id) + self.auth() + self.listing_page.visit() + + self.assertTrue(self.listing_page.is_sidebar_present) + self.assertFalse(self.listing_page.are_cards_present) + + def test_enrollments_and_programs(self): + """ + Verify that cards appear when the user has enrollments + which are included in at least one active program. + """ + self.stub_api() + self.auth() + self.listing_page.visit() + + self.assertTrue(self.listing_page.is_sidebar_present) + self.assertTrue(self.listing_page.are_cards_present) + + +@attr('a11y') +class ProgramListingPageA11yTest(ProgramListingPageBase): + """Test program listing page accessibility.""" + + def test_empty_a11y(self): + """Test a11y of the page's empty state.""" + self.stub_api() + self.auth(enroll=False) + self.listing_page.visit() + + self.assertTrue(self.listing_page.is_sidebar_present) + self.assertFalse(self.listing_page.are_cards_present) + + self.listing_page.a11y_audit.check_for_accessibility_errors() + + def test_cards_a11y(self): + """Test a11y when program cards are present.""" + self.stub_api() + self.auth() + self.listing_page.visit() + + self.assertTrue(self.listing_page.is_sidebar_present) + self.assertTrue(self.listing_page.are_cards_present) + + self.listing_page.a11y_audit.check_for_accessibility_errors() diff --git a/common/test/acceptance/tests/studio/test_studio_home.py b/common/test/acceptance/tests/studio/test_studio_home.py index 8240135f89..ddd58d4aed 100644 --- a/common/test/acceptance/tests/studio/test_studio_home.py +++ b/common/test/acceptance/tests/studio/test_studio_home.py @@ -8,7 +8,7 @@ from uuid import uuid4 from ...fixtures import PROGRAMS_STUB_URL from ...fixtures.config import ConfigModelFixture -from ...fixtures.programs import ProgramsFixture +from ...fixtures.programs import FakeProgram, ProgramsFixture, ProgramsConfigMixin from ...pages.studio.auto_auth import AutoAuthPage from ...pages.studio.library import LibraryEditPage from ...pages.studio.index import DashboardPage, DashboardPageWithPrograms @@ -69,7 +69,7 @@ class CreateLibraryTest(WebAppTest): self.assertTrue(self.dashboard_page.has_library(name=name, org=org, number=number)) -class DashboardProgramsTabTest(WebAppTest): +class DashboardProgramsTabTest(ProgramsConfigMixin, WebAppTest): """ Test the programs tab on the studio home page. """ @@ -81,23 +81,6 @@ class DashboardProgramsTabTest(WebAppTest): self.dashboard_page = DashboardPageWithPrograms(self.browser) self.auth_page.visit() - def set_programs_api_configuration(self, is_enabled=False, api_version=1, api_url=PROGRAMS_STUB_URL, - js_path='/js', css_path='/css'): - """ - Dynamically adjusts the programs API config model during tests. - """ - ConfigModelFixture('/config/programs', { - 'enabled': is_enabled, - 'enable_studio_tab': is_enabled, - 'enable_student_dashboard': is_enabled, - 'api_version_number': api_version, - 'internal_service_url': api_url, - 'public_service_url': api_url, - 'authoring_app_js_path': js_path, - 'authoring_app_css_path': css_path, - 'cache_ttl': 0 - }).install() - def test_tab_is_disabled(self): """ The programs tab and "new program" button should not appear at all @@ -128,16 +111,24 @@ class DashboardProgramsTabTest(WebAppTest): via config, and the results of the program list should display when the list is nonempty. """ - test_program_values = [('first program', 'org1'), ('second program', 'org2')] + test_program_values = [ + FakeProgram(name='first program', status='unpublished', org_key='org1', course_id='foo/bar/baz'), + FakeProgram(name='second program', status='unpublished', org_key='org2', course_id='qux/quux/corge'), + ] ProgramsFixture().install_programs(test_program_values) + self.set_programs_api_configuration(True) + self.dashboard_page.visit() + self.assertTrue(self.dashboard_page.is_programs_tab_present()) self.assertTrue(self.dashboard_page.is_new_program_button_present()) - results = self.dashboard_page.get_program_list() - self.assertEqual(results, test_program_values) self.assertFalse(self.dashboard_page.is_empty_list_create_button_present()) + results = self.dashboard_page.get_program_list() + expected = [(p.name, p.org_key) for p in test_program_values] + self.assertEqual(results, expected) + def test_tab_requires_staff(self): """ The programs tab and "new program" button will not be available, even