Merge pull request #12170 from edx/renzo/program-listing-bokchoy
Acceptance tests for program listing page
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
22
common/test/acceptance/pages/lms/programs.py
Normal file
22
common/test/acceptance/pages/lms/programs.py
Normal file
@@ -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
|
||||
102
common/test/acceptance/tests/lms/test_programs.py
Normal file
102
common/test/acceptance/tests/lms/test_programs.py
Normal file
@@ -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()
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user