From aa31f3b2558dfbc8cb63f701d3c6ce3bd4e1418e Mon Sep 17 00:00:00 2001 From: Aarif Date: Tue, 15 Feb 2022 18:56:34 +0500 Subject: [PATCH] refactor: remove a11y tests setup (#29813) --- common/test/acceptance/.a11ycoveragerc | 36 - common/test/acceptance/.coveragerc | 44 - .../test/acceptance/.pa11ycrawlercoveragerc | 40 - common/test/acceptance/__init__.py | 0 common/test/acceptance/fixtures/__init__.py | 31 - common/test/acceptance/fixtures/base.py | 193 - common/test/acceptance/fixtures/catalog.py | 79 - .../test/acceptance/fixtures/certificates.py | 73 - common/test/acceptance/fixtures/config.py | 102 - common/test/acceptance/fixtures/course.py | 447 - common/test/acceptance/fixtures/discussion.py | 184 - common/test/acceptance/fixtures/library.py | 92 - common/test/acceptance/fixtures/programs.py | 16 - common/test/acceptance/pages/__init__.py | 0 .../test/acceptance/pages/common/__init__.py | 16 - .../test/acceptance/pages/common/auto_auth.py | 121 - common/test/acceptance/pages/common/logout.py | 19 - common/test/acceptance/pages/common/utils.py | 42 - common/test/acceptance/pages/lms/__init__.py | 11 - .../acceptance/pages/lms/account_settings.py | 103 - common/test/acceptance/pages/lms/catalog.py | 27 - .../test/acceptance/pages/lms/course_home.py | 27 - .../test/acceptance/pages/lms/course_page.py | 42 - .../test/acceptance/pages/lms/course_wiki.py | 145 - .../test/acceptance/pages/lms/courseware.py | 79 - common/test/acceptance/pages/lms/dashboard.py | 25 - .../test/acceptance/pages/lms/discussion.py | 111 - common/test/acceptance/pages/lms/fields.py | 251 - .../pages/lms/instructor_dashboard.py | 661 -- .../acceptance/pages/lms/learner_profile.py | 329 - common/test/acceptance/pages/lms/problem.py | 48 - common/test/acceptance/pages/lms/programs.py | 35 - common/test/acceptance/pages/lms/progress.py | 57 - .../test/acceptance/pages/lms/staff_view.py | 61 - common/test/acceptance/pages/lms/tab_nav.py | 109 - .../acceptance/pages/lms/video/__init__.py | 0 .../test/acceptance/pages/lms/video/video.py | 227 - .../test/acceptance/pages/studio/__init__.py | 13 - .../test/acceptance/pages/studio/container.py | 712 -- .../acceptance/pages/studio/course_page.py | 61 - .../test/acceptance/pages/studio/library.py | 50 - .../test/acceptance/pages/studio/overview.py | 392 - .../acceptance/pages/studio/pagination.py | 61 - .../test/acceptance/pages/studio/settings.py | 28 - common/test/acceptance/pages/studio/users.py | 50 - common/test/acceptance/pages/studio/utils.py | 133 - .../acceptance/pages/studio/video/__init__.py | 0 .../acceptance/pages/studio/video/video.py | 713 -- common/test/acceptance/tests/__init__.py | 16 - .../acceptance/tests/data/formula_problem.xml | 17 - .../acceptance/tests/data/multiple_choice.xml | 28 - .../acceptance/tests/data/poll_markdown.xml | 3 - .../acceptance/tests/discussion/__init__.py | 0 .../acceptance/tests/discussion/helpers.py | 116 - .../discussion/test_cohort_management.py | 81 - .../tests/discussion/test_discussion.py | 349 - common/test/acceptance/tests/helpers.py | 538 - common/test/acceptance/tests/lms/__init__.py | 0 .../tests/lms/test_account_settings.py | 69 - .../tests/lms/test_learner_profile.py | 172 - common/test/acceptance/tests/lms/test_lms.py | 116 - .../tests/lms/test_lms_course_home.py | 80 - .../tests/lms/test_lms_dashboard.py | 135 - .../lms/test_lms_instructor_dashboard.py | 244 - .../acceptance/tests/lms/test_lms_problems.py | 205 - .../tests/lms/test_lms_user_preview.py | 153 - .../tests/lms/test_problem_types.py | 1109 --- .../acceptance/tests/lms/test_programs.py | 153 - .../tests/lms/test_progress_page.py | 275 - .../test/acceptance/tests/studio/__init__.py | 0 .../tests/studio/base_studio_test.py | 177 - .../tests/studio/test_studio_library.py | 35 - .../tests/studio/test_studio_settings.py | 110 - .../test/acceptance/tests/video/__init__.py | 0 .../tests/video/test_video_module.py | 249 - ...5df15b80d7bc245aa80eaf6da30be406af8.tar.gz | Bin 130353 -> 0 bytes .../test/db_cache/bok_choy_data_default.json | 1 - .../bok_choy_data_student_module_history.json | 1 - common/test/db_cache/bok_choy_migrations.sha1 | 1 - .../bok_choy_migrations_data_default.sql | 27 - ...migrations_data_student_module_history.sql | 27 - .../test/db_cache/bok_choy_schema_default.sql | 8631 ----------------- ...bok_choy_schema_student_module_history.sql | 50 - common/test/test_sites/default/readme.txt | 3 - .../site_with_logistration/readme.txt | 1 - .../test_sites/test_site/css/test_site.css | 15 - .../test_site/images/background-image.jpg | Bin 158490 -> 0 bytes .../test_site/images/header-logo.png | Bin 1178 -> 0 bytes .../images/login-and-register-banner.png | Bin 18076 -> 0 bytes .../templates/courseware/syllabus.html | 6 - .../test_site/templates/courseware/tabs.html | 33 - .../courseware/test_absolute_path.html | 4 - .../courseware/test_relative_path.html | 4 - .../templates/emails/email_change.txt | 15 - .../test_site/templates/footer.html | 19 - .../test_site/templates/head-extra.html | 7 - .../test_site/templates/login-sidebar.html | 19 - .../test_site/templates/register-sidebar.html | 38 - .../templates/static_templates/about.html | 64 - .../templates/static_templates/contact.html | 67 - .../templates/static_templates/copyright.html | 2 - .../templates/static_templates/faq.html | 143 - .../templates/static_templates/tos.html | 124 - openedx/core/djangoapps/catalog/views.py | 11 - pavelib/__init__.py | 2 +- pavelib/bok_choy.py | 164 - .../paver_tests/test_paver_bok_choy_cmds.py | 159 - pavelib/utils/db_utils.py | 247 - pavelib/utils/test/bokchoy_options.py | 97 - pavelib/utils/test/bokchoy_utils.py | 215 - pavelib/utils/test/suites/__init__.py | 1 - pavelib/utils/test/suites/bokchoy_suite.py | 325 - pavelib/utils/test/utils.py | 92 - scripts/Jenkinsfiles/bokchoy | 177 - scripts/accessibility-tests.sh | 34 - scripts/generic-ci-tests.sh | 43 - scripts/paver_autocomplete.sh | 30 - 117 files changed, 1 insertion(+), 21424 deletions(-) delete mode 100644 common/test/acceptance/.a11ycoveragerc delete mode 100644 common/test/acceptance/.coveragerc delete mode 100644 common/test/acceptance/.pa11ycrawlercoveragerc delete mode 100644 common/test/acceptance/__init__.py delete mode 100644 common/test/acceptance/fixtures/__init__.py delete mode 100644 common/test/acceptance/fixtures/base.py delete mode 100644 common/test/acceptance/fixtures/catalog.py delete mode 100644 common/test/acceptance/fixtures/certificates.py delete mode 100644 common/test/acceptance/fixtures/config.py delete mode 100644 common/test/acceptance/fixtures/course.py delete mode 100644 common/test/acceptance/fixtures/discussion.py delete mode 100644 common/test/acceptance/fixtures/library.py delete mode 100644 common/test/acceptance/fixtures/programs.py delete mode 100644 common/test/acceptance/pages/__init__.py delete mode 100644 common/test/acceptance/pages/common/__init__.py delete mode 100644 common/test/acceptance/pages/common/auto_auth.py delete mode 100644 common/test/acceptance/pages/common/logout.py delete mode 100644 common/test/acceptance/pages/common/utils.py delete mode 100644 common/test/acceptance/pages/lms/__init__.py delete mode 100644 common/test/acceptance/pages/lms/account_settings.py delete mode 100644 common/test/acceptance/pages/lms/catalog.py delete mode 100644 common/test/acceptance/pages/lms/course_home.py delete mode 100644 common/test/acceptance/pages/lms/course_page.py delete mode 100644 common/test/acceptance/pages/lms/course_wiki.py delete mode 100644 common/test/acceptance/pages/lms/courseware.py delete mode 100644 common/test/acceptance/pages/lms/dashboard.py delete mode 100644 common/test/acceptance/pages/lms/discussion.py delete mode 100644 common/test/acceptance/pages/lms/fields.py delete mode 100644 common/test/acceptance/pages/lms/instructor_dashboard.py delete mode 100644 common/test/acceptance/pages/lms/learner_profile.py delete mode 100644 common/test/acceptance/pages/lms/problem.py delete mode 100644 common/test/acceptance/pages/lms/programs.py delete mode 100644 common/test/acceptance/pages/lms/progress.py delete mode 100644 common/test/acceptance/pages/lms/staff_view.py delete mode 100644 common/test/acceptance/pages/lms/tab_nav.py delete mode 100644 common/test/acceptance/pages/lms/video/__init__.py delete mode 100644 common/test/acceptance/pages/lms/video/video.py delete mode 100644 common/test/acceptance/pages/studio/__init__.py delete mode 100644 common/test/acceptance/pages/studio/container.py delete mode 100644 common/test/acceptance/pages/studio/course_page.py delete mode 100644 common/test/acceptance/pages/studio/library.py delete mode 100644 common/test/acceptance/pages/studio/overview.py delete mode 100644 common/test/acceptance/pages/studio/pagination.py delete mode 100644 common/test/acceptance/pages/studio/settings.py delete mode 100644 common/test/acceptance/pages/studio/users.py delete mode 100644 common/test/acceptance/pages/studio/utils.py delete mode 100644 common/test/acceptance/pages/studio/video/__init__.py delete mode 100644 common/test/acceptance/pages/studio/video/video.py delete mode 100644 common/test/acceptance/tests/__init__.py delete mode 100644 common/test/acceptance/tests/data/formula_problem.xml delete mode 100644 common/test/acceptance/tests/data/multiple_choice.xml delete mode 100644 common/test/acceptance/tests/data/poll_markdown.xml delete mode 100644 common/test/acceptance/tests/discussion/__init__.py delete mode 100644 common/test/acceptance/tests/discussion/helpers.py delete mode 100644 common/test/acceptance/tests/discussion/test_cohort_management.py delete mode 100644 common/test/acceptance/tests/discussion/test_discussion.py delete mode 100644 common/test/acceptance/tests/helpers.py delete mode 100644 common/test/acceptance/tests/lms/__init__.py delete mode 100644 common/test/acceptance/tests/lms/test_account_settings.py delete mode 100644 common/test/acceptance/tests/lms/test_learner_profile.py delete mode 100644 common/test/acceptance/tests/lms/test_lms.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_course_home.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_dashboard.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_problems.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_user_preview.py delete mode 100644 common/test/acceptance/tests/lms/test_problem_types.py delete mode 100644 common/test/acceptance/tests/lms/test_programs.py delete mode 100644 common/test/acceptance/tests/lms/test_progress_page.py delete mode 100644 common/test/acceptance/tests/studio/__init__.py delete mode 100644 common/test/acceptance/tests/studio/base_studio_test.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_library.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_settings.py delete mode 100644 common/test/acceptance/tests/video/__init__.py delete mode 100644 common/test/acceptance/tests/video/test_video_module.py delete mode 100644 common/test/db_cache/ab74a5df15b80d7bc245aa80eaf6da30be406af8.tar.gz delete mode 100644 common/test/db_cache/bok_choy_data_default.json delete mode 100644 common/test/db_cache/bok_choy_data_student_module_history.json delete mode 100644 common/test/db_cache/bok_choy_migrations.sha1 delete mode 100644 common/test/db_cache/bok_choy_migrations_data_default.sql delete mode 100644 common/test/db_cache/bok_choy_migrations_data_student_module_history.sql delete mode 100644 common/test/db_cache/bok_choy_schema_default.sql delete mode 100644 common/test/db_cache/bok_choy_schema_student_module_history.sql delete mode 100644 common/test/test_sites/default/readme.txt delete mode 100644 common/test/test_sites/site_with_logistration/readme.txt delete mode 100644 common/test/test_sites/test_site/css/test_site.css delete mode 100644 common/test/test_sites/test_site/images/background-image.jpg delete mode 100644 common/test/test_sites/test_site/images/header-logo.png delete mode 100644 common/test/test_sites/test_site/images/login-and-register-banner.png delete mode 100644 common/test/test_sites/test_site/templates/courseware/syllabus.html delete mode 100644 common/test/test_sites/test_site/templates/courseware/tabs.html delete mode 100644 common/test/test_sites/test_site/templates/courseware/test_absolute_path.html delete mode 100644 common/test/test_sites/test_site/templates/courseware/test_relative_path.html delete mode 100644 common/test/test_sites/test_site/templates/emails/email_change.txt delete mode 100644 common/test/test_sites/test_site/templates/footer.html delete mode 100644 common/test/test_sites/test_site/templates/head-extra.html delete mode 100644 common/test/test_sites/test_site/templates/login-sidebar.html delete mode 100644 common/test/test_sites/test_site/templates/register-sidebar.html delete mode 100644 common/test/test_sites/test_site/templates/static_templates/about.html delete mode 100644 common/test/test_sites/test_site/templates/static_templates/contact.html delete mode 100755 common/test/test_sites/test_site/templates/static_templates/copyright.html delete mode 100644 common/test/test_sites/test_site/templates/static_templates/faq.html delete mode 100644 common/test/test_sites/test_site/templates/static_templates/tos.html delete mode 100644 pavelib/bok_choy.py delete mode 100644 pavelib/paver_tests/test_paver_bok_choy_cmds.py delete mode 100644 pavelib/utils/db_utils.py delete mode 100644 pavelib/utils/test/bokchoy_options.py delete mode 100644 pavelib/utils/test/bokchoy_utils.py delete mode 100644 pavelib/utils/test/suites/bokchoy_suite.py delete mode 100644 scripts/Jenkinsfiles/bokchoy delete mode 100755 scripts/accessibility-tests.sh diff --git a/common/test/acceptance/.a11ycoveragerc b/common/test/acceptance/.a11ycoveragerc deleted file mode 100644 index 7518a0c288..0000000000 --- a/common/test/acceptance/.a11ycoveragerc +++ /dev/null @@ -1,36 +0,0 @@ -[run] -data_file = reports/a11y/.coverage -source = - lms - cms - common/djangoapps - common/lib - openedx - -omit = - lms/envs/* - cms/envs/* - common/djangoapps/terrain/* - common/djangoapps/*/migrations/* - openedx/core/djangoapps/*/migrations/* - */test* - */management/* - */urls* - */wsgi* - lms/djangoapps/*/migrations/* - cms/djangoapps/*/migrations/* - -parallel = True - -[report] -ignore_errors = True -include = - **/views/*.py - **/views.py - -[html] -title = Bok Choy A11y Test Coverage Report -directory = reports/a11y/cover - -[xml] -output = reports/a11y/coverage.xml diff --git a/common/test/acceptance/.coveragerc b/common/test/acceptance/.coveragerc deleted file mode 100644 index d125b60946..0000000000 --- a/common/test/acceptance/.coveragerc +++ /dev/null @@ -1,44 +0,0 @@ -[run] -data_file = reports/bok_choy/${TEST_SUITE}.coverage -source = - lms - cms - common/djangoapps - common/lib - openedx - -omit = - lms/envs/* - cms/envs/* - cms/manage.py - cms/djangoapps/contentstore/views/dev.py - common/djangoapps/terrain/* - common/djangoapps/*/migrations/* - openedx/core/djangoapps/debug/* - openedx/core/djangoapps/*/migrations/* - */test* - */management/* - */urls* - */wsgi* - lms/debug/* - lms/djangoapps/*/features/* - lms/djangoapps/*/migrations/* - cms/djangoapps/*/features/* - cms/djangoapps/*/migrations/* - -concurrency = multiprocessing -parallel = True - -[report] -ignore_errors = True - -exclude_lines = - pragma: no cover - raise NotImplementedError - -[html] -title = Bok Choy Test Coverage Report -directory = reports/bok_choy/cover - -[xml] -output = reports/bok_choy/acceptance_coverage.xml diff --git a/common/test/acceptance/.pa11ycrawlercoveragerc b/common/test/acceptance/.pa11ycrawlercoveragerc deleted file mode 100644 index 3610491369..0000000000 --- a/common/test/acceptance/.pa11ycrawlercoveragerc +++ /dev/null @@ -1,40 +0,0 @@ -[run] -data_file = reports/pa11ycrawler/.coverage - -source = - lms - cms - common/djangoapps - common/lib - openedx - **/mako_lms/ - **/mako_cms/ - -omit = - lms/envs/* - cms/envs/* - common/djangoapps/terrain/* - common/djangoapps/*/migrations/* - openedx/core/djangoapps/*/migrations/* - */test* - */management/* - */urls* - */wsgi* - lms/djangoapps/*/migrations/* - cms/djangoapps/*/migrations/* - -parallel = True - -[report] -ignore_errors = True -include = - **/views/*.py - **/views.py - - -[html] -title = pa11ycrawler Coverage Report -directory = reports/pa11ycrawler/cover - -[xml] -output = reports/pa11ycrawler/coverage.xml diff --git a/common/test/acceptance/__init__.py b/common/test/acceptance/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/test/acceptance/fixtures/__init__.py b/common/test/acceptance/fixtures/__init__.py deleted file mode 100644 index c4b65ec27c..0000000000 --- a/common/test/acceptance/fixtures/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Envirement Setup for fixtures. -""" - - -import os - -HOSTNAME = os.environ.get('BOK_CHOY_HOSTNAME', 'localhost') -CMS_PORT = os.environ.get('BOK_CHOY_CMS_PORT', '8031') -LMS_PORT = os.environ.get('BOK_CHOY_LMS_PORT', '8003') - -# Get the URL of the Studio instance under test -STUDIO_BASE_URL = os.environ.get('studio_url', f'http://{HOSTNAME}:{CMS_PORT}') - -# Get the URL of the LMS instance under test -LMS_BASE_URL = os.environ.get('lms_url', f'http://{HOSTNAME}:{LMS_PORT}') - -# Get the URL of the XQueue stub used in the test -XQUEUE_STUB_URL = os.environ.get('xqueue_url', 'http://localhost:8040') - -# Get the URL of the Ora stub used in the test -ORA_STUB_URL = os.environ.get('ora_url', 'http://localhost:8041') - -# Get the URL of the comments service stub used in the test -COMMENTS_STUB_URL = os.environ.get('comments_url', f'http://{HOSTNAME}:4567') - -# Get the URL of the EdxNotes service stub used in the test -EDXNOTES_STUB_URL = os.environ.get('edxnotes_url', f'http://{HOSTNAME}:8042') - -# Get the URL of the Catalog service stub used in the test -CATALOG_STUB_URL = os.environ.get('catalog_url', 'http://localhost:8091') diff --git a/common/test/acceptance/fixtures/base.py b/common/test/acceptance/fixtures/base.py deleted file mode 100644 index b0aed9b7e8..0000000000 --- a/common/test/acceptance/fixtures/base.py +++ /dev/null @@ -1,193 +0,0 @@ -""" -Common code shared by course and library fixtures. -""" - - -import json - -import requests -from lazy import lazy - -from common.test.acceptance.fixtures import STUDIO_BASE_URL - - -class StudioApiLoginError(Exception): - """ - Error occurred while logging in to the Studio API. - """ - pass # lint-amnesty, pylint: disable=unnecessary-pass - - -class StudioApiFixture: - """ - Base class for fixtures that use the Studio restful API. - """ - def __init__(self): - # Info about the auto-auth user used to create the course/library. - self.user = {} - - @lazy - def session(self): - """ - Log in as a staff user, then return a `requests` `session` object for the logged in user. - Raises a `StudioApiLoginError` if the login fails. - """ - # Use auto-auth to retrieve the session for a logged in user - session = requests.Session() - response = session.get(STUDIO_BASE_URL + '/auto_auth?staff=true') - - # Return the session from the request - if response.ok: - # Capture the details of the authenticated user - self.user = response.json() - - if not self.user: - raise StudioApiLoginError(f'Auto-auth failed. Response was: {self.user}') - - return session - - else: - msg = f'Could not log in to use Studio restful API. Status code: {response.status_code}' - raise StudioApiLoginError(msg) - - @lazy - def session_cookies(self): - """ - Log in as a staff user, then return the cookies for the session (as a dict) - Raises a `StudioApiLoginError` if the login fails. - """ - return dict(self.session.cookies.items()) - - @lazy - def headers(self): - """ - Default HTTP headers dict. - """ - return { - 'Content-type': 'application/json', - 'Accept': 'application/json', - 'X-CSRFToken': self.session_cookies.get('csrftoken', '') - } - - -class FixtureError(Exception): - """ - Error occurred while installing a course or library fixture. - """ - pass # lint-amnesty, pylint: disable=unnecessary-pass - - -class XBlockContainerFixture(StudioApiFixture): - """ - Base class for course and library fixtures. - """ - - def __init__(self): - self.children = [] - super().__init__() - - def add_children(self, *args): - """ - Add children XBlock to the container. - Each item in `args` is an `XBlockFixtureDesc` object. - - Returns the fixture to allow chaining. - """ - self.children.extend(args) - return self - - def _create_xblock_children(self, parent_loc, xblock_descriptions): - """ - Recursively create XBlock children. - """ - for desc in xblock_descriptions: - loc = self.create_xblock(parent_loc, desc) - self._create_xblock_children(loc, desc.children) - - def create_xblock(self, parent_loc, xblock_desc): - """ - Create an XBlock with `parent_loc` (the location of the parent block) - and `xblock_desc` (an `XBlockFixtureDesc` instance). - """ - create_payload = { - 'category': xblock_desc.category, - 'display_name': xblock_desc.display_name, - } - - if parent_loc is not None: - create_payload['parent_locator'] = parent_loc - - # Create the new XBlock - response = self.session.post( - STUDIO_BASE_URL + '/xblock/', - data=json.dumps(create_payload), - headers=self.headers, - ) - - if not response.ok: - msg = f"Could not create {xblock_desc}. Status was {response.status_code}" - raise FixtureError(msg) - - try: - loc = response.json().get('locator') - xblock_desc.locator = loc - except ValueError: - raise FixtureError(f"Could not decode JSON from '{response.content}'") # lint-amnesty, pylint: disable=raise-missing-from - - # Configure the XBlock - response = self.session.post( - STUDIO_BASE_URL + '/xblock/' + loc, - data=xblock_desc.serialize(), - headers=self.headers, - ) - - if response.ok: - return loc - else: - raise FixtureError(f"Could not update {xblock_desc}. Status code: {response.status_code}") - - def _update_xblock(self, locator, data): - """ - Update the xblock at `locator`. - """ - # Create the new XBlock - response = self.session.put( - f"{STUDIO_BASE_URL}/xblock/{locator}", - data=json.dumps(data), - headers=self.headers, - ) - - if not response.ok: - msg = f"Could not update {locator} with data {data}. Status was {response.status_code}" - raise FixtureError(msg) - - def _encode_post_dict(self, post_dict): - """ - Encode `post_dict` (a dictionary) as UTF-8 encoded JSON. - """ - return json.dumps(post_dict).encode('utf-8') - - def get_nested_xblocks(self, category=None): - """ - Return a list of nested XBlocks for the container that can be filtered by - category. - """ - xblocks = self._get_nested_xblocks(self) - if category: - xblocks = [x for x in xblocks if x.category == category] - return xblocks - - def _get_nested_xblocks(self, xblock_descriptor): - """ - Return a list of nested XBlocks for the container. - """ - xblocks = list(xblock_descriptor.children) - for child in xblock_descriptor.children: - xblocks.extend(self._get_nested_xblocks(child)) - return xblocks - - def _publish_xblock(self, locator): - """ - Publish the xblock at `locator`. - """ - self._update_xblock(locator, {'publish': 'make_public'}) diff --git a/common/test/acceptance/fixtures/catalog.py b/common/test/acceptance/fixtures/catalog.py deleted file mode 100644 index 5cc3e2e649..0000000000 --- a/common/test/acceptance/fixtures/catalog.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Tools to create catalog-related data for use in bok choy tests. -""" - - -import json - -import requests - -from common.test.acceptance.fixtures import CATALOG_STUB_URL -from common.test.acceptance.fixtures.config import ConfigModelFixture - - -class CatalogFixture: - """ - Interface to set up mock responses from the Catalog stub server. - """ - def install_programs(self, programs): - """ - Stub the discovery service's program list and detail API endpoints. - - Arguments: - programs (list): A list of programs. Both list and detail endpoints - will be stubbed using data from this list. - """ - key = 'catalog.programs' - - uuids = [] - for program in programs: - uuid = program['uuid'] - uuids.append(uuid) - - program_key = f'{key}.{uuid}' - requests.put( - f'{CATALOG_STUB_URL}/set_config', - data={program_key: json.dumps(program)}, - ) - - # Stub list endpoint as if the uuids_only query param had been passed. - requests.put( - f'{CATALOG_STUB_URL}/set_config', - data={key: json.dumps(uuids)}, - ) - - def install_pathways(self, pathways): - """ - Stub the discovery service's credit pathways API endpoint - - Arguments: - pathways (list): A list of credit pathways. List endpoint will be stubbed using data from this list. - """ - requests.put( - f'{CATALOG_STUB_URL}/set_config', - data={'catalog.pathways': json.dumps({'results': pathways, 'next': None})} - ) - - def install_program_types(self, program_types): - """ - Stub the discovery service's program type list API endpoints. - - Arguments: - program_types (list): A list of program types. List endpoint will be stubbed using data from this list. - """ - requests.put( - f'{CATALOG_STUB_URL}/set_config', - data={'catalog.programs_types': json.dumps(program_types)}, - ) - - -class CatalogIntegrationMixin: - """Mixin providing a method used to configure the catalog integration.""" - def set_catalog_integration(self, is_enabled=False, service_username=None): - """Use this to change the catalog integration config model during tests.""" - ConfigModelFixture('/config/catalog', { - 'enabled': is_enabled, - 'internal_api_url': f'{CATALOG_STUB_URL}/api/v1/', - 'cache_ttl': 0, - 'service_username': service_username, - }).install() diff --git a/common/test/acceptance/fixtures/certificates.py b/common/test/acceptance/fixtures/certificates.py deleted file mode 100644 index df75f38722..0000000000 --- a/common/test/acceptance/fixtures/certificates.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -Tools for creating certificates config fixture data. -""" - - -import json - -from common.test.acceptance.fixtures import STUDIO_BASE_URL -from common.test.acceptance.fixtures.base import StudioApiFixture - - -class CertificateConfigFixtureError(Exception): - """ - Error occurred while installing certificate config fixture. - """ - pass # lint-amnesty, pylint: disable=unnecessary-pass - - -class CertificateConfigUpdateFixtureError(Exception): - """ - Error occurred while updating certificate config fixture. - """ - pass # lint-amnesty, pylint: disable=unnecessary-pass - - -class CertificateConfigFixture(StudioApiFixture): - """ - Fixture to create certificates configuration for a course - """ - certificates = [] - - def __init__(self, course_id, certificates_data): - self.course_id = course_id - self.certificates = certificates_data - super().__init__() - - def install(self): - """ - Push the certificates config data to certificate endpoint. - """ - response = self.session.post( - f'{STUDIO_BASE_URL}/certificates/{self.course_id}', - data=json.dumps(self.certificates), - headers=self.headers - ) - - if not response.ok: - raise CertificateConfigFixtureError( - "Could not create certificate {}. Status was {}".format( - json.dumps(self.certificates), response.status_code - ) - ) - - return self - - def update_certificate(self, certificate_id): - """ - Update the certificates config data to certificate endpoint. - """ - response = self.session.put( - f'{STUDIO_BASE_URL}/certificates/{self.course_id}/{certificate_id}', - data=json.dumps(self.certificates), - headers=self.headers - ) - - if not response.ok: - raise CertificateConfigUpdateFixtureError( - "Could not update certificate {}. Status was {}".format( - json.dumps(self.certificates), response.status_code - ) - ) - - return self diff --git a/common/test/acceptance/fixtures/config.py b/common/test/acceptance/fixtures/config.py deleted file mode 100644 index d4346d841f..0000000000 --- a/common/test/acceptance/fixtures/config.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -Fixture to manipulate configuration models. -""" - - -import json -import re - -import requests -from lazy import lazy - -from common.test.acceptance.fixtures import LMS_BASE_URL, STUDIO_BASE_URL - - -class ConfigModelFixtureError(Exception): - """ - Error occurred while configuring the stub XQueue. - """ - pass # lint-amnesty, pylint: disable=unnecessary-pass - - -class ConfigModelFixture: - """ - Configure a ConfigurationModel by using it's JSON api. - """ - - def __init__(self, api_base, configuration, platform='lms'): - """ - Configure a ConfigurationModel exposed at `api_base` to have the configuration `configuration`. - """ - self._api_base = api_base - self._configuration = configuration - self._platform = platform - - def install(self): - """ - Configure the stub via HTTP. - """ - base_url = STUDIO_BASE_URL if self._platform == 'cms' else LMS_BASE_URL - - url = base_url + self._api_base - - response = self.session.post( - url, - data=json.dumps(self._configuration), - headers=self.headers, - ) - - if not response.ok: - raise ConfigModelFixtureError( - "Could not configure url '{}'. response: {} - {}".format( - self._api_base, - response, - response.content, - ) - ) - - @lazy - def session_cookies(self): - """ - Log in as a staff user, then return the cookies for the session (as a dict) - Raises a `ConfigModelFixtureError` if the login fails. - """ - return dict(self.session.cookies.items()) - - @lazy - def headers(self): - """ - Default HTTP headers dict. - """ - return { - 'Content-type': 'application/json', - 'Accept': 'application/json', - 'X-CSRFToken': self.session_cookies.get('csrftoken', '') - } - - @lazy - def session(self): - """ - Log in as a staff user, then return a `requests` `session` object for the logged in user. - Raises a `StudioApiLoginError` if the login fails. - """ - # Use auto-auth to retrieve the session for a logged in user - session = requests.Session() - response = session.get(LMS_BASE_URL + "/auto_auth?superuser=true") - - # Return the session from the request - if response.ok: - # auto_auth returns information about the newly created user - # capture this so it can be used by by the testcases. - user_pattern = re.compile( - r'Logged in user {} \({}\) with password {} and user_id {}'.format( - r'(?P\S+)', r'(?P[^\)]+)', r'(?P\S+)', r'(?P\d+)')) - user_matches = re.match(user_pattern, response.text) - if user_matches: - self.user = user_matches.groupdict() # pylint: disable=attribute-defined-outside-init - - return session - - else: - msg = f"Could not log in to use ConfigModel restful API. Status code: {response.status_code}" - raise ConfigModelFixtureError(msg) diff --git a/common/test/acceptance/fixtures/course.py b/common/test/acceptance/fixtures/course.py deleted file mode 100644 index ff7045f386..0000000000 --- a/common/test/acceptance/fixtures/course.py +++ /dev/null @@ -1,447 +0,0 @@ -""" -Fixture to create a course and course components (XBlocks). -""" - - -import datetime -import json -import mimetypes -from collections import namedtuple -from textwrap import dedent - -from opaque_keys.edx.keys import CourseKey -from path import Path - -from common.test.acceptance.fixtures import STUDIO_BASE_URL -from common.test.acceptance.fixtures.base import FixtureError, XBlockContainerFixture -from common.djangoapps.util.course import course_location_from_key - - -class XBlockFixtureDesc: - """ - Description of an XBlock, used to configure a course fixture. - """ - - def __init__(self, category, display_name, data=None, - metadata=None, grader_type=None, publish='make_public', **kwargs): - """ - Configure the XBlock to be created by the fixture. - These arguments have the same meaning as in the Studio REST API: - * `category` - * `display_name` - * `data` - * `metadata` - * `grader_type` - * `publish` - """ - self.category = category - self.display_name = display_name - self.data = data - self.metadata = metadata - self.grader_type = grader_type - self.publish = publish - self.children = [] - self.locator = None - self.fields = kwargs - - def add_children(self, *args): - """ - Add child XBlocks to this XBlock. - Each item in `args` is an `XBlockFixtureDesc` object. - - Returns the `xblock_desc` instance to allow chaining. - """ - self.children.extend(args) - return self - - def serialize(self): - """ - Return a JSON representation of the XBlock, suitable - for sending as POST data to /xblock - - XBlocks are always set to public visibility. - """ - returned_data = { - 'display_name': self.display_name, - 'data': self.data, - 'metadata': self.metadata, - 'graderType': self.grader_type, - 'publish': self.publish, - 'fields': self.fields, - } - return json.dumps(returned_data) - - def __str__(self): - """ - Return a string representation of the description. - Useful for error messages. - """ - return dedent(""" - - """).strip().format( - self.category, self.data, self.metadata, - self.grader_type, self.publish, self.children, self.locator - ) - - -# Description of course updates to add to the course -# `date` is a str (e.g. "January 29, 2014) -# `content` is also a str (e.g. "Test course") -CourseUpdateDesc = namedtuple("CourseUpdateDesc", ['date', 'content']) - - -class CourseFixture(XBlockContainerFixture): - """ - Fixture for ensuring that a course exists. - - WARNING: This fixture is NOT idempotent. To avoid conflicts - between tests, you should use unique course identifiers for each fixture. - """ - - def __init__(self, org, number, run, display_name, start_date=None, end_date=None, settings=None): - """ - Configure the course fixture to create a course with - - `org`, `number`, `run`, and `display_name` (all unicode). - - `start_date` and `end_date` are datetime objects indicating the course start and end date. - The default is for the course to have started in the distant past, which is generally what - we want for testing so students can enroll. - - `settings` can be any additional course settings needs to be enabled. for example - to enable entrance exam settings would be a dict like this {"entrance_exam_enabled": "true"} - These have the same meaning as in the Studio restful API /course end-point. - """ - super().__init__() - self._course_dict = { - 'org': org, - 'number': number, - 'run': run, - 'display_name': display_name - } - - # Set a default start date to the past, but use Studio's - # default for the end date (meaning we don't set it here) - if start_date is None: - start_date = datetime.datetime(1970, 1, 1) - - self._course_details = { - 'start_date': start_date.isoformat(), - } - - if end_date is not None: - self._course_details['end_date'] = end_date.isoformat() - - if settings is not None: - self._course_details.update(settings) - - self._updates = [] - self._handouts = [] - self._assets = [] - self._textbooks = [] - self._advanced_settings = {} - self._course_key = None - - def __str__(self): - """ - String representation of the course fixture, useful for debugging. - """ - return "".format(**self._course_dict) - - def add_course_details(self, course_details): - """ - Add course details to dict of course details to be updated when configure_course or install is called. - - Arguments: - Dictionary containing key value pairs for course updates, - e.g. {'start_date': datetime.now() } - """ - if 'start_date' in course_details: - course_details['start_date'] = course_details['start_date'].isoformat() - if 'end_date' in course_details: - course_details['end_date'] = course_details['end_date'].isoformat() - - self._course_details.update(course_details) - - def add_update(self, update): - """ - Add an update to the course. `update` should be a `CourseUpdateDesc`. - """ - self._updates.append(update) - - def add_handout(self, asset_name): - """ - Add the handout named `asset_name` to the course info page. - Note that this does not actually *create* the static asset; it only links to it. - """ - self._handouts.append(asset_name) - - def add_asset(self, asset_name): - """ - Add the asset to the list of assets to be uploaded when the install method is called. - """ - self._assets.extend(asset_name) - - def add_textbook(self, book_title, chapters): - """ - Add textbook to the list of textbooks to be added when the install method is called. - """ - self._textbooks.append({"chapters": chapters, "tab_title": book_title}) - - def add_advanced_settings(self, settings): - """ - Adds advanced settings to be set on the course when the install method is called. - """ - self._advanced_settings.update(settings) - - def install(self): - """ - Create the course and XBlocks within the course. - This is NOT an idempotent method; if the course already exists, this will - raise a `FixtureError`. You should use unique course identifiers to avoid - conflicts between tests. - """ - self._create_course() - self._install_course_updates() - self._install_course_handouts() - self._install_course_textbooks() - self._configure_course() - self._upload_assets() - self._add_advanced_settings() - self._create_xblock_children(self._course_location, self.children) - - return self - - def configure_course(self): - """ - Configure Course Settings, take new course settings from self._course_details dict object - """ - self._configure_course() - - @property - def studio_course_outline_as_json(self): - """ - Retrieves Studio course outline in JSON format. - """ - url = STUDIO_BASE_URL + '/course/' + self._course_key + "?format=json" - response = self.session.get(url, headers=self.headers) - - if not response.ok: - raise FixtureError( - "Could not retrieve course outline json. Status was {}".format( - response.status_code)) - - try: - course_outline_json = response.json() - except ValueError: - raise FixtureError( # lint-amnesty, pylint: disable=raise-missing-from - f"Could not decode course outline as JSON: '{response}'" - ) - return course_outline_json - - @property - def _course_location(self): - """ - Return the locator string for the course. - """ - course_key = CourseKey.from_string(self._course_key) - return str(course_location_from_key(course_key)) - - @property - def _assets_url(self): - """ - Return the url string for the assets - """ - return "/assets/" + self._course_key + "/" - - @property - def _handouts_loc(self): - """ - Return the locator string for the course handouts - """ - course_key = CourseKey.from_string(self._course_key) - return str(course_key.make_usage_key('course_info', 'handouts')) - - def _create_course(self): - """ - Create the course described in the fixture. - """ - # If the course already exists, this will respond - # with a 200 and an error message, which we ignore. - response = self.session.post( - STUDIO_BASE_URL + '/course/', - data=self._encode_post_dict(self._course_dict), - headers=self.headers - ) - - try: - err = response.json().get('ErrMsg') - - except ValueError: - raise FixtureError( # lint-amnesty, pylint: disable=raise-missing-from - "Could not parse response from course request as JSON: '{}'".format( - response.content)) - - # This will occur if the course identifier is not unique - if err is not None: - raise FixtureError(f"Could not create course {self}. Error message: '{err}'") - - if response.ok: - self._course_key = response.json()['course_key'] - else: - raise FixtureError( - "Could not create course {}. Status was {}\nResponse content was: {}".format( - self._course_dict, response.status_code, response.content)) - - def _configure_course(self): - """ - Configure course settings (e.g. start and end date) - """ - url = STUDIO_BASE_URL + '/settings/details/' + self._course_key - - # First, get the current values - response = self.session.get(url, headers=self.headers) - - if not response.ok: - raise FixtureError( - "Could not retrieve course details. Status was {}".format( - response.status_code)) - - try: - details = response.json() - except ValueError: - raise FixtureError( # lint-amnesty, pylint: disable=raise-missing-from - f"Could not decode course details as JSON: '{details}'" - ) - - # Update the old details with our overrides - details.update(self._course_details) - - # POST the updated details to Studio - response = self.session.post( - url, data=self._encode_post_dict(details), - headers=self.headers, - ) - - if not response.ok: - raise FixtureError( - "Could not update course details to '{}' with {}: Status was {}.".format( - self._course_details, url, response.status_code)) - - def _install_course_handouts(self): - """ - Add handouts to the course info page. - """ - url = STUDIO_BASE_URL + '/xblock/' + self._handouts_loc - - # Construct HTML with each of the handout links - handouts_li = [ - f'
  • Example Handout
  • ' - for handout in self._handouts - ] - handouts_html = '
      {}
    '.format("".join(handouts_li)) - - # Update the course's handouts HTML - payload = json.dumps({ - 'children': None, - 'data': handouts_html, - 'id': self._handouts_loc, - 'metadata': {}, - }) - - response = self.session.post(url, data=payload, headers=self.headers) - - if not response.ok: - raise FixtureError( - f"Could not update course handouts with {url}. Status was {response.status_code}") - - def _install_course_updates(self): - """ - Add updates to the course, if any are configured. - """ - url = STUDIO_BASE_URL + '/course_info_update/' + self._course_key + '/' - - for update in self._updates: - - # Add the update to the course - date, content = update - payload = json.dumps({'date': date, 'content': content}) - response = self.session.post(url, headers=self.headers, data=payload) - - if not response.ok: - raise FixtureError( - "Could not add update to course: {} with {}. Status was {}".format( - update, url, response.status_code)) - - def _upload_assets(self): - """ - Upload assets - :raise FixtureError: - """ - url = STUDIO_BASE_URL + self._assets_url - - test_dir = Path(__file__).abspath().dirname().dirname().dirname() - - for asset_name in self._assets: - asset_file_path = test_dir + '/data/uploads/' + asset_name - - asset_file = open(asset_file_path, mode='rb') # lint-amnesty, pylint: disable=consider-using-with - files = {'file': (asset_name, asset_file, mimetypes.guess_type(asset_file_path)[0])} - - headers = { - 'Accept': 'application/json', - 'X-CSRFToken': self.session_cookies.get('csrftoken', '') - } - - upload_response = self.session.post(url, files=files, headers=headers) - - if not upload_response.ok: - raise FixtureError('Could not upload {asset_name} with {url}. Status code: {code}'.format( - asset_name=asset_name, url=url, code=upload_response.status_code)) - - def _install_course_textbooks(self): - """ - Add textbooks to the course, if any are configured. - """ - url = STUDIO_BASE_URL + '/textbooks/' + self._course_key - - for book in self._textbooks: - payload = json.dumps(book) - response = self.session.post(url, headers=self.headers, data=payload) - - if not response.ok: - raise FixtureError( - "Could not add book to course: {} with {}. Status was {}".format( - book, url, response.status_code)) - - def _add_advanced_settings(self): - """ - Add advanced settings. - """ - url = STUDIO_BASE_URL + "/settings/advanced/" + self._course_key - - # POST advanced settings to Studio - response = self.session.post( - url, data=self._encode_post_dict(self._advanced_settings), - headers=self.headers, - ) - - if not response.ok: - raise FixtureError( - "Could not update advanced details to '{}' with {}: Status was {}.".format( - self._advanced_settings, url, response.status_code)) - - def _create_xblock_children(self, parent_loc, xblock_descriptions): - """ - Recursively create XBlock children. - """ - super()._create_xblock_children(parent_loc, xblock_descriptions) - self._publish_xblock(parent_loc) diff --git a/common/test/acceptance/fixtures/discussion.py b/common/test/acceptance/fixtures/discussion.py deleted file mode 100644 index 418626b407..0000000000 --- a/common/test/acceptance/fixtures/discussion.py +++ /dev/null @@ -1,184 +0,0 @@ -""" -Tools for creating discussion content fixture data. -""" - - -import json -from datetime import datetime - -import factory -import requests - -from common.test.acceptance.fixtures import COMMENTS_STUB_URL -from common.test.acceptance.fixtures.config import ConfigModelFixture - - -class ContentFactory(factory.Factory): # lint-amnesty, pylint: disable=missing-class-docstring - class Meta: - model = dict - - id = None - user_id = "1234" - username = "dummy-username" - course_id = "dummy-course-id" - commentable_id = "dummy-commentable-id" - anonymous = False - anonymous_to_peers = False - at_position_list = [] - abuse_flaggers = [] - created_at = datetime.utcnow().isoformat() - updated_at = datetime.utcnow().isoformat() - endorsed = False - closed = False - votes = {"up_count": 0} - - @classmethod - def _adjust_kwargs(cls, **kwargs): - # The discussion code assumes that user_id is a string. This ensures that it always will be. - if 'user_id' in kwargs: - kwargs['user_id'] = str(kwargs['user_id']) - return kwargs - - -class Thread(ContentFactory): # lint-amnesty, pylint: disable=missing-class-docstring - thread_type = "discussion" - anonymous = False - anonymous_to_peers = False - comments_count = 0 - unread_comments_count = 0 - title = "dummy thread title" - body = "dummy thread body" - type = "thread" - group_id = None - pinned = False - read = False - context = "course" - - -class Comment(ContentFactory): - thread_id = "dummy thread" - depth = 0 - type = "comment" - body = "dummy comment body" - - -class Response(Comment): - depth = 1 - body = "dummy response body" - - -class SearchResult(factory.Factory): # lint-amnesty, pylint: disable=missing-class-docstring - class Meta: - model = dict - - discussion_data = [] - annotated_content_info = {} - num_pages = 1 - page = 1 - corrected_text = None - - -class DiscussionContentFixture: # lint-amnesty, pylint: disable=missing-class-docstring - - def push(self): - """ - Push the data to the stub comments service. - """ - return requests.put( - f'{COMMENTS_STUB_URL}/set_config', - data=self.get_config_data() - ) - - def get_config_data(self): - """ - return a dictionary with the fixture's data serialized for PUTting to the stub server's config endpoint. - """ - raise NotImplementedError() - - -class SingleThreadViewFixture(DiscussionContentFixture): # lint-amnesty, pylint: disable=missing-class-docstring - - def __init__(self, thread): - self.thread = thread - - def addResponse(self, response, comments=[]): # lint-amnesty, pylint: disable=dangerous-default-value, missing-function-docstring - response['children'] = comments - if self.thread["thread_type"] == "discussion": - responseListAttr = "children" - elif response["endorsed"]: - responseListAttr = "endorsed_responses" - else: - responseListAttr = "non_endorsed_responses" - self.thread.setdefault(responseListAttr, []).append(response) - self.thread['comments_count'] += len(comments) + 1 - - def _get_comment_map(self): - """ - Generate a dict mapping each response/comment in the thread - by its `id`. - """ - def _visit(obj): - res = [] - for child in obj.get('children', []): - res.append((child['id'], child)) - if 'children' in child: - res += _visit(child) - return res - return dict(_visit(self.thread)) - - def get_config_data(self): - return { - "threads": json.dumps({self.thread['id']: self.thread}), - "comments": json.dumps(self._get_comment_map()) - } - - -class MultipleThreadFixture(DiscussionContentFixture): # lint-amnesty, pylint: disable=missing-class-docstring - - def __init__(self, threads): - self.threads = threads - - def get_config_data(self): - threads_list = {thread['id']: thread for thread in self.threads} - return {"threads": json.dumps(threads_list), "comments": '{}'} - - def add_response(self, response, comments, thread): - """ - Add responses to the thread - """ - response['children'] = comments - if thread["thread_type"] == "discussion": - response_list_attr = "children" - elif response["endorsed"]: - response_list_attr = "endorsed_responses" - else: - response_list_attr = "non_endorsed_responses" - thread.setdefault(response_list_attr, []).append(response) - thread['comments_count'] += len(comments) + 1 - - -class UserProfileViewFixture(DiscussionContentFixture): # lint-amnesty, pylint: disable=missing-class-docstring - - def __init__(self, threads): - self.threads = threads - - def get_config_data(self): - return {"active_threads": json.dumps(self.threads)} - - -class SearchResultFixture(DiscussionContentFixture): # lint-amnesty, pylint: disable=missing-class-docstring - - def __init__(self, result): - self.result = result - - def get_config_data(self): - return {"search_result": json.dumps(self.result)} - - -class ForumsConfigMixin: - """Mixin providing a method used to configure the forums integration.""" - def enable_forums(self, is_enabled=True): - """Configures whether or not forums are enabled.""" - ConfigModelFixture('/config/forums', { - 'enabled': is_enabled, - }).install() diff --git a/common/test/acceptance/fixtures/library.py b/common/test/acceptance/fixtures/library.py deleted file mode 100644 index c4e98684f5..0000000000 --- a/common/test/acceptance/fixtures/library.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Fixture to create a Content Library -""" -from opaque_keys.edx.keys import CourseKey - -from common.test.acceptance.fixtures import STUDIO_BASE_URL -from common.test.acceptance.fixtures.base import FixtureError, XBlockContainerFixture - - -class LibraryFixture(XBlockContainerFixture): - """ - Fixture for ensuring that a library exists. - - WARNING: This fixture is NOT idempotent. To avoid conflicts - between tests, you should use unique library identifiers for each fixture. - """ - - def __init__(self, org, number, display_name): - """ - Configure the library fixture to create a library with - """ - super().__init__() - self.library_info = { - 'org': org, - 'number': number, - 'display_name': display_name - } - - self.display_name = display_name - self._library_key = None - super().__init__() - - def __str__(self): - """ - String representation of the library fixture, useful for debugging. - """ - return "".format(**self.library_info) - - def install(self): - """ - Create the library and XBlocks within the library. - This is NOT an idempotent method; if the library already exists, this will - raise a `FixtureError`. You should use unique library identifiers to avoid - conflicts between tests. - """ - self._create_library() - self._create_xblock_children(self.library_location, self.children) - - return self - - @property - def library_key(self): - """ - Get the LibraryLocator for this library, as a string. - """ - return self._library_key - - @property - def library_location(self): - """ - Return the locator string for the LibraryRoot XBlock that is the root of the library hierarchy. - """ - lib_key = CourseKey.from_string(self._library_key) - return str(lib_key.make_usage_key('library', 'library')) - - def _create_library(self): - """ - Create the library described in the fixture. - Will fail if the library already exists. - """ - response = self.session.post( - STUDIO_BASE_URL + '/library/', - data=self._encode_post_dict(self.library_info), - headers=self.headers - ) - - if response.ok: - self._library_key = response.json()['library_key'] - else: - try: - err_msg = response.json().get('ErrMsg') - except ValueError: - err_msg = "Unknown Error" - raise FixtureError("Could not create library {}. Status was {}, error was: {}".format( - self.library_info, response.status_code, err_msg - )) - - def create_xblock(self, parent_loc, xblock_desc): - # Disable publishing for library XBlocks: - xblock_desc.publish = "not-applicable" - - return super().create_xblock(parent_loc, xblock_desc) diff --git a/common/test/acceptance/fixtures/programs.py b/common/test/acceptance/fixtures/programs.py deleted file mode 100644 index 935cb49981..0000000000 --- a/common/test/acceptance/fixtures/programs.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Tools to create programs-related data for use in bok choy tests. -""" - - -from common.test.acceptance.fixtures.config import ConfigModelFixture - - -class ProgramsConfigMixin: - """Mixin providing a method used to configure the programs feature.""" - def set_programs_api_configuration(self, is_enabled=False): - """Dynamically adjusts the Programs config model during tests.""" - ConfigModelFixture('/config/programs', { - 'enabled': is_enabled, - 'marketing_path': '/foo', - }).install() diff --git a/common/test/acceptance/pages/__init__.py b/common/test/acceptance/pages/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/test/acceptance/pages/common/__init__.py b/common/test/acceptance/pages/common/__init__.py deleted file mode 100644 index a949ee2d31..0000000000 --- a/common/test/acceptance/pages/common/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Package of common page objects for acceptance tests -""" - - -import os - -HOSTNAME = os.environ.get('BOK_CHOY_HOSTNAME', 'localhost') -CMS_PORT = os.environ.get('BOK_CHOY_CMS_PORT', 8031) -LMS_PORT = os.environ.get('BOK_CHOY_LMS_PORT', 8003) - -# Get the URL of the instance under test -BASE_URL = os.environ.get('test_url', f'http://{HOSTNAME}:{LMS_PORT}') - -# The URL used for user auth in testing -AUTH_BASE_URL = os.environ.get('test_url', f'http://{HOSTNAME}:{CMS_PORT}') diff --git a/common/test/acceptance/pages/common/auto_auth.py b/common/test/acceptance/pages/common/auto_auth.py deleted file mode 100644 index 60d06bfc19..0000000000 --- a/common/test/acceptance/pages/common/auto_auth.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -Auto-auth page (used to automatically log in during testing). -""" - - -import json -import os -from urllib import parse - -from bok_choy.page_object import PageObject, unguarded - -# The URL used for user auth in testing -HOSTNAME = os.environ.get('BOK_CHOY_HOSTNAME', 'localhost') -CMS_PORT = os.environ.get('BOK_CHOY_CMS_PORT', 8031) -AUTH_BASE_URL = os.environ.get('test_url', f'http://{HOSTNAME}:{CMS_PORT}') -FULL_NAME = 'Test' - - -class AutoAuthPage(PageObject): - """ - The automatic authorization page. - - When enabled via the Django settings file, visiting this url will create a user and log them in. - """ - - # Internal cache for parsed user info. - _user_info = None - - def __init__(self, browser, username=None, email=None, password=None, full_name=FULL_NAME, staff=False, - superuser=None, course_id=None, enrollment_mode=None, roles=None, no_login=False, is_active=True, - course_access_roles=None, should_manually_verify=False): - """ - Auto-auth is an end-point for HTTP GET requests. - By default, it will create accounts with random user credentials, - but you can also specify credentials using querystring parameters. - - `username`, `email`, and `password` are the user's credentials (strings) - 'full_name' is the profile full name value - `staff` is a boolean indicating whether the user is global staff. - `superuser` is a boolean indicating whether the user is a super user. - `course_id` is the ID of the course to enroll the student in. - Currently, this has the form "org/number/run" - `should_manually_verify` is a boolean indicating whether the - created user should have their identification verified - - Note that "global staff" is NOT the same as course staff. - """ - super().__init__(browser) - - # This will eventually hold the details about the user account - self._user_info = None - - course_access_roles = course_access_roles or [] - course_access_roles = ','.join(course_access_roles) - - self._params = { - 'full_name': full_name, - 'staff': staff, - 'superuser': superuser, - 'is_active': is_active, - 'course_access_roles': course_access_roles, - } - - if username: - self._params['username'] = username - - if email: - self._params['email'] = email - - if password: - self._params['password'] = password - - if superuser is not None: - self._params['superuser'] = "true" if superuser else "false" - - if course_id: - self._params['course_id'] = course_id - - if enrollment_mode: - self._params['enrollment_mode'] = enrollment_mode - - if roles: - self._params['roles'] = roles - - if no_login: - self._params['no_login'] = True - - if should_manually_verify: - self._params['should_manually_verify'] = True - - @property - def url(self): - """ - Construct the URL. - """ - url = AUTH_BASE_URL + "/auto_auth" - query_str = parse.urlencode(self._params) - - if query_str: - url += "?" + query_str - - return url - - def is_browser_on_page(self): - return bool(self.user_info) - - @property - @unguarded - def user_info(self): - """A dictionary containing details about the user account.""" - if not self._user_info: - body = self.q(css='BODY').text[0] - self._user_info = json.loads(body) - - return self._user_info - - def get_user_id(self): - """ - Finds and returns the user_id - """ - return self.user_info['user_id'] diff --git a/common/test/acceptance/pages/common/logout.py b/common/test/acceptance/pages/common/logout.py deleted file mode 100644 index f8b5eaf50f..0000000000 --- a/common/test/acceptance/pages/common/logout.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Logout Page. -""" - - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.common import BASE_URL - - -class LogoutPage(PageObject): - """ - Logout page to logout current logged in user. - """ - - url = BASE_URL + "/logout" - - def is_browser_on_page(self): - return self.q(css='.sign-in-btn').present diff --git a/common/test/acceptance/pages/common/utils.py b/common/test/acceptance/pages/common/utils.py deleted file mode 100644 index 4dc47f5fa5..0000000000 --- a/common/test/acceptance/pages/common/utils.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Utility methods common to Studio and the LMS. -""" -from common.test.acceptance.tests.helpers import disable_animations - - -def click_css(page, css, source_index=0): - """ - Click the button/link with the given css and index on the specified page (subclass of PageObject). - - Will only consider elements that are displayed and have a height and width greater than zero. - - If require_notification is False (default value is True), the method will return immediately. - Otherwise, it will wait for the "mini-notification" to appear and disappear. - """ - def _is_visible(element): - """Is the given element visible?""" - # Only make the call to size once (instead of once for the height and once for the width) - # because otherwise you will trigger a extra query on a remote element. - return element.is_displayed() and all(size > 0 for size in element.size.values()) - - # Disable all animations for faster testing with more reliable synchronization - disable_animations(page) - # Click on the element in the browser - page.q(css=css).filter(_is_visible).nth(source_index).click() - - # Some buttons trigger ajax posts - # (e.g. .add-missing-groups-button as configured in split_test_author_view.js) - # so after you click anything wait for the ajax call to finish - page.wait_for_ajax() - - -def confirm_prompt(page, cancel=False, require_notification=None): - """ - Ensures that a modal prompt and confirmation button are visible, then clicks the button. The prompt is canceled iff - cancel is True. - """ - page.wait_for_element_visibility('.prompt', 'Prompt is visible') - confirmation_button_css = '.prompt .action-' + ('secondary' if cancel else 'primary') - page.wait_for_element_visibility(confirmation_button_css, 'Confirmation button is visible') - require_notification = (not cancel) if require_notification is None else require_notification - click_css(page, confirmation_button_css, require_notification=require_notification) # lint-amnesty, pylint: disable=unexpected-keyword-arg diff --git a/common/test/acceptance/pages/lms/__init__.py b/common/test/acceptance/pages/lms/__init__.py deleted file mode 100644 index 4b834fdd7c..0000000000 --- a/common/test/acceptance/pages/lms/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -Package of lms page objects for acceptance tests -""" - - -import os - -# Get the URL of the instance under test -HOSTNAME = os.environ.get('BOK_CHOY_HOSTNAME', 'localhost') -LMS_PORT = os.environ.get('BOK_CHOY_LMS_PORT', 8003) -BASE_URL = os.environ.get('test_url', f'http://{HOSTNAME}:{LMS_PORT}') diff --git a/common/test/acceptance/pages/lms/account_settings.py b/common/test/acceptance/pages/lms/account_settings.py deleted file mode 100644 index 3c1cb13130..0000000000 --- a/common/test/acceptance/pages/lms/account_settings.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -Base class for account settings page. -""" - - -from bok_choy.page_object import PageObject -from bok_choy.promise import EmptyPromise - -from common.test.acceptance.pages.lms import BASE_URL -from common.test.acceptance.pages.lms.fields import FieldsMixin - - -class AccountSettingsPage(FieldsMixin, PageObject): - """ - Tests for Account Settings Page. - """ - - url = "{base}/{settings}".format(base=BASE_URL, settings='account/settings') - - def is_browser_on_page(self): - return self.q(css='.account-settings-container').present - - def sections_structure(self): - """ - Return list of section titles and field titles for each section. - - Example: [ - { - 'title': 'Section Title' - 'fields': ['Field 1 title', 'Field 2 title',...] - }, - ... - ] - """ - structure = [] - - sections = self.q(css='#aboutTabSections-tabpanel .section') - for section in sections: - section_title_element = section.find_element_by_class_name('section-header') - field_title_elements = section.find_elements_by_class_name('u-field-title') - - structure.append({ - 'title': section_title_element.text, - 'fields': [element.text for element in field_title_elements], - }) - - return structure - - def _is_loading_in_progress(self): - """ - Check if loading indicator is visible. - """ - query = self.q(css='.ui-loading-indicator') - return query.present and 'is-hidden' not in query.attrs('class')[0].split() - - def wait_for_loading_indicator(self): - """ - Wait for loading indicator to become visible. - """ - EmptyPromise(self._is_loading_in_progress, "Loading is in progress.").fulfill() - - def switch_account_settings_tabs(self, tab_id): - """ - Switch between the different account settings tabs. - """ - self.q(css=f'#{tab_id}').click() - - @property - def is_order_history_tab_visible(self): - """ Check if tab with the name "Order History" is visible.""" - return self.q(css='.u-field-orderHistory').visible - - def get_value_of_order_history_row_item(self, field_id, field_name): - """ Return the text value of the provided order field name.""" - query = self.q(css=f'.u-field-{field_id} .u-field-order-{field_name}') - return query.text if query.present else None - - def order_button_is_visible(self, field_id): - """ Check that if hovering over the order history row shows the - order detail link or not. - """ - return self.q(css='.u-field-{} .u-field-{}'.format(field_id, 'link')).visible - - @property - def is_delete_button_visible(self): - self.scroll_to_element('#account-deletion-container') - return self.q(css='#delete-account-btn').visible - - def click_delete_button(self): - self.q(css="#delete-account-btn").click() - - @property - def is_delete_modal_visible(self): - return self.q(css='.delete-confirmation-wrapper').visible - - def delete_confirm_button_enabled(self): - return self.q(css='.paragon__modal-footer .paragon__btn')[0].is_enabled() - - def click_delete_confirm_button(self): - return self.q(css='.paragon__modal-footer .paragon__btn')[0].click() - - def fill_in_password_field(self, password): - self.q(css='#asInput1').fill(password) diff --git a/common/test/acceptance/pages/lms/catalog.py b/common/test/acceptance/pages/lms/catalog.py deleted file mode 100644 index 1930b11261..0000000000 --- a/common/test/acceptance/pages/lms/catalog.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -Course catalog page -""" - - -import re - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.lms import BASE_URL - - -class CacheProgramsPage(PageObject): - """ - Visit this page to call the cache_programs management command. - - This page makes a GET request to a view which is only meant to be enabled in - testing contexts where the LMS can only be reached over HTTP. Stub the - discovery service before visiting this page. - """ - url = BASE_URL + '/catalog/management/cache_programs/' - - def is_browser_on_page(self): - body = self.q(css='body').text[0] - match = re.search(r'programs cached', body, flags=re.IGNORECASE) - - return True if match else False # lint-amnesty, pylint: disable=simplifiable-if-expression diff --git a/common/test/acceptance/pages/lms/course_home.py b/common/test/acceptance/pages/lms/course_home.py deleted file mode 100644 index eb2ec99720..0000000000 --- a/common/test/acceptance/pages/lms/course_home.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -LMS Course Home page object -""" - - -from .course_page import CoursePage -from .staff_view import StaffPreviewPage - - -class CourseHomePage(CoursePage): - """ - Course home page, including course outline. - """ - - url_path = "course/" - - HEADER_RESUME_COURSE_SELECTOR = '.page-header .action-resume-course' - - def is_browser_on_page(self): - return self.q(css='.course-outline').present - - def __init__(self, browser, course_id): - super().__init__(browser, course_id) - self.course_id = course_id - self.preview = StaffPreviewPage(browser, self) - # TODO: TNL-6546: Remove the following - self.course_outline_page = False diff --git a/common/test/acceptance/pages/lms/course_page.py b/common/test/acceptance/pages/lms/course_page.py deleted file mode 100644 index c2cbdde832..0000000000 --- a/common/test/acceptance/pages/lms/course_page.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Base class for pages in courseware. -""" - - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.lms import BASE_URL -from common.test.acceptance.pages.lms.tab_nav import TabNavPage - - -class CoursePage(PageObject): # lint-amnesty, pylint: disable=abstract-method - """ - Abstract base class for page objects within a course. - """ - - # Overridden by subclasses to provide the relative path within the course - # Paths should not include the leading forward slash. - url_path = "" - - def __init__(self, browser, course_id): - """ - Course ID is currently of the form "edx/999/2013_Spring" - but this format could change. - """ - super().__init__(browser) - self.course_id = course_id - - @property - def url(self): - """ - Construct a URL to the page within the course. - """ - return BASE_URL + "/courses/" + self.course_id + "/" + self.url_path - - def has_tab(self, tab_name): - """ - Returns true if the current page is showing a tab with the given name. - :return: - """ - tab_nav = TabNavPage(self.browser) - return tab_name in tab_nav.tab_names diff --git a/common/test/acceptance/pages/lms/course_wiki.py b/common/test/acceptance/pages/lms/course_wiki.py deleted file mode 100644 index b3ed78d3fe..0000000000 --- a/common/test/acceptance/pages/lms/course_wiki.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -Wiki tab on courses -""" - - -from common.test.acceptance.pages.lms.course_page import CoursePage -from common.test.acceptance.pages.studio.utils import get_codemirror_value, type_in_codemirror - - -class CourseWikiPage(CoursePage): - """ - Course wiki navigation and objects. - """ - - url_path = "wiki" - - def is_browser_on_page(self): - """ - Browser is on the wiki page if the wiki breadcrumb is present - """ - return self.q(css='.breadcrumb').present - - def open_editor(self): - """ - Display the editor for a wiki article. - """ - edit_button = self.q(css='.fa-pencil') - edit_button.click() - - def show_history(self): - """ - Show the change history for a wiki article. - """ - edit_button = self.q(css='.fa-clock-o') - edit_button.click() - - def show_children(self): - """ - Show the children of a wiki article. - """ - children_link = self.q(css='.see-children>a') - children_link.click() - - @property - def article_name(self): - """ - Return the name of the article - """ - return str(self.q(css='.main-article .entry-title').text[0]) - - -class CourseWikiSubviewPage(CoursePage): # pylint: disable=abstract-method - """ Abstract base page for subviews within the wiki. """ - - def __init__(self, browser, course_id, course_info): - """ - Course ID is currently of the form "edx/999/2013_Spring" - but this format could change. - """ - super().__init__(browser, course_id) - self.course_id = course_id - self.course_info = course_info - self.article_name = "{org}.{course_number}.{course_run}".format( - org=self.course_info['org'], - course_number=self.course_info['number'], - course_run=self.course_info['run'] - ) - - -class CourseWikiEditPage(CourseWikiSubviewPage): - """ - Editor page - """ - - @property - def url_path(self): - """ - Construct a URL to the page within the course. - """ - return "/wiki/" + self.article_name + "/_edit" - - def is_browser_on_page(self): - """ - The wiki page editor - """ - return self.q(css='.CodeMirror-scroll').present - - def replace_wiki_content(self, content): - """ - Editor must be open already. This will replace any content in the editor - with new content - """ - type_in_codemirror(self, 0, content) - - def get_wiki_editor_content(self): - """ - Returns the content currently in the wiki editor. - """ - - return get_codemirror_value(self, 0) - - def save_wiki_content(self): - """ - When the editor is open, click save - """ - self.q(css='button[name="save"]').click() - self.wait_for_element_presence('.alert-success', 'wait for the article to be saved') - - -class CourseWikiHistoryPage(CourseWikiSubviewPage): - """ - Course wiki change history page. - """ - - def is_browser_on_page(self): - """ - Return if the browser is on the history page. - """ - return self.q(css='section.history').present - - @property - def url_path(self): - """ - Construct a URL to the page within the course. - """ - return "/wiki/" + self.article_name + "/_history" - - -class CourseWikiChildrenPage(CourseWikiSubviewPage): - """ - Course wiki "All Children" page. - """ - - def is_browser_on_page(self): - """ - Return if the browser is on the wiki children page (which contains a search widget). - """ - return self.q(css='.form-search').present - - @property - def url_path(self): - """ - Construct a URL to the page within the course. - """ - return "/wiki/" + self.article_name + "/_dir" diff --git a/common/test/acceptance/pages/lms/courseware.py b/common/test/acceptance/pages/lms/courseware.py deleted file mode 100644 index d54bfcabd8..0000000000 --- a/common/test/acceptance/pages/lms/courseware.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Courseware page. -""" - - -from bok_choy.promise import EmptyPromise - -from common.test.acceptance.pages.lms.course_page import CoursePage - - -class CoursewarePage(CoursePage): - """ - Course info. - """ - - url_path = "courseware/" - xblock_component_selector = '.vert .xblock' - - # TODO: TNL-6546: Remove sidebar selectors - section_selector = '.chapter' - subsection_selector = '.chapter-content-container a' - - def __init__(self, browser, course_id): # lint-amnesty, pylint: disable=useless-super-delegation - super().__init__(browser, course_id) - # self.nav = CourseNavPage(browser, self) - - def is_browser_on_page(self): - return self.q(css='.course-content').present - - def go_to_sequential_position(self, sequential_position): - """ - Within a section/subsection navigate to the sequential position specified by `sequential_position`. - - Arguments: - sequential_position (int): position in sequential bar - """ - def is_at_new_position(): - """ - Returns whether the specified tab has become active. It is defensive - against the case where the page is still being loaded. - """ - active_tab = self._active_sequence_tab - try: - return active_tab and int(active_tab.attrs('data-element')[0]) == sequential_position - except IndexError: - return False - - sequential_position_css = f'#sequence-list #tab_{sequential_position - 1}' - self.q(css=sequential_position_css).first.click() - EmptyPromise(is_at_new_position, "Position navigation fulfilled").fulfill() - - @property - def _active_sequence_tab(self): - return self.q(css='#sequence-list .nav-item.active') - - def click_next_button_on_top(self): - self._click_navigation_button('sequence-nav', 'button-next') - - def _click_navigation_button(self, top_or_bottom_class, next_or_previous_class): - """ - Clicks the navigation button, given the respective CSS classes. - """ - previous_tab_id = self._active_sequence_tab.attrs('data-id')[0] - - def is_at_new_tab_id(): - """ - Returns whether the active tab has changed. It is defensive - against the case where the page is still being loaded. - """ - active_tab = self._active_sequence_tab - try: - return active_tab and previous_tab_id != active_tab.attrs('data-id')[0] - except IndexError: - return False - - self.q( - css=f'.{top_or_bottom_class} > .sequence-nav-button.{next_or_previous_class}' - ).first.click() - EmptyPromise(is_at_new_tab_id, "Button navigation fulfilled").fulfill() diff --git a/common/test/acceptance/pages/lms/dashboard.py b/common/test/acceptance/pages/lms/dashboard.py deleted file mode 100644 index 69d907bc0c..0000000000 --- a/common/test/acceptance/pages/lms/dashboard.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Student dashboard page. -""" - - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.lms import BASE_URL - - -class DashboardPage(PageObject): - """ - Student dashboard, where the student can view - courses she/he has registered for. - """ - url = f"{BASE_URL}/dashboard" - - def is_browser_on_page(self): - return self.q(css='.my-courses').present - - def get_courses(self): - """ - Get all courses shown in the dashboard - """ - return self.q(css='ul.listing-courses .course-item') diff --git a/common/test/acceptance/pages/lms/discussion.py b/common/test/acceptance/pages/lms/discussion.py deleted file mode 100644 index b24c8ef8fe..0000000000 --- a/common/test/acceptance/pages/lms/discussion.py +++ /dev/null @@ -1,111 +0,0 @@ -""" -LMS discussion page -""" - - -from contextlib import contextmanager - -from bok_choy.page_object import PageObject -from bok_choy.promise import EmptyPromise - -from common.test.acceptance.pages.lms.course_page import CoursePage - - -class DiscussionThreadPage(PageObject): - """ - Discussion thread page - """ - url = None - - def __init__(self, browser, thread_selector): - super().__init__(browser) - self.thread_selector = thread_selector - - def _find_within(self, selector): - """ - Returns a query corresponding to the given CSS selector within the scope - of this thread page - """ - return self.q(css=self.thread_selector + " " + selector) - - def is_browser_on_page(self): - return self.q(css=self.thread_selector).visible - - def _get_element_text(self, selector): - """ - Returns the text of the first element matching the given selector, or - None if no such element exists - """ - text_list = self._find_within(selector).text - return text_list[0] if text_list else None - - def is_element_visible(self, selector): - """ - Returns true if the element matching the specified selector is visible. - - Args: - selector (str): The CSS selector that matches the desired element. - - Returns: - bool: True if the element is visible. - - """ - query = self._find_within(selector) - return query.present and query.visible - - @contextmanager - def secondary_action_menu_open(self, ancestor_selector): - """ - Given the selector for an ancestor of a secondary menu, return a context - manager that will open and close the menu - """ - self.wait_for_ajax() - self._find_within(ancestor_selector + " .action-more").click() - EmptyPromise( - lambda: self.is_element_visible(ancestor_selector + " .actions-dropdown"), - "Secondary action menu opened" - ).fulfill() - yield - if self.is_element_visible(ancestor_selector + " .actions-dropdown"): - self._find_within(ancestor_selector + " .action-more").click() - EmptyPromise( - lambda: not self.is_element_visible(ancestor_selector + " .actions-dropdown"), - "Secondary action menu closed" - ).fulfill() - - -class DiscussionTabSingleThreadPage(CoursePage): # lint-amnesty, pylint: disable=missing-class-docstring - def __init__(self, browser, course_id, discussion_id, thread_id): - super().__init__(browser, course_id) - self.thread_page = DiscussionThreadPage( - browser, - f"body.discussion .discussion-article[data-id='{thread_id}']" - ) - self.url_path = "discussion/forum/{discussion_id}/threads/{thread_id}".format( - discussion_id=discussion_id, thread_id=thread_id - ) - - def is_browser_on_page(self): - return self.thread_page.is_browser_on_page() - - def __getattr__(self, name): - return getattr(self.thread_page, name) - - def close_open_thread(self): - with self.thread_page.secondary_action_menu_open(".thread-main-wrapper"): - self._find_within(".thread-main-wrapper .action-close").first.click() - - -class DiscussionTabHomePage(CoursePage): - """ - Discussion tab home page - """ - ALERT_SELECTOR = ".discussion-body .forum-nav .search-alert" - - def __init__(self, browser, course_id): - super().__init__(browser, course_id) - self.url_path = "discussion/forum/" - self.root_selector = None - - def is_browser_on_page(self): - return self.q(css=".discussion-body section.home-header").present diff --git a/common/test/acceptance/pages/lms/fields.py b/common/test/acceptance/pages/lms/fields.py deleted file mode 100644 index c8a3066295..0000000000 --- a/common/test/acceptance/pages/lms/fields.py +++ /dev/null @@ -1,251 +0,0 @@ -""" -Mixins for fields. -""" -from bok_choy.promise import EmptyPromise - -from common.test.acceptance.tests.helpers import get_selected_option_text, select_option_by_text - - -class FieldsMixin: - """ - Methods for testing fields in pages. - """ - - def field(self, field_id): - """ - Return field with field_id. - """ - query = self.q(css=f'.u-field-{field_id}') - return query.text[0] if query.present else None - - def wait_for_field(self, field_id): - """ - Wait for a field to appear in DOM. - """ - EmptyPromise( - lambda: self.field(field_id) is not None, - f"Field with id \"{field_id}\" is in DOM." - ).fulfill() - - def mode_for_field(self, field_id): - """ - Extract current field mode. - - Returns: - `placeholder`/`edit`/`display` - """ - self.wait_for_field(field_id) - - query = self.q(css=f'.u-field-{field_id}') - - if not query.present: - return None - - field_classes = query.attrs('class')[0].split() - - if 'mode-placeholder' in field_classes: - return 'placeholder' - - if 'mode-display' in field_classes: - return 'display' - - if 'mode-edit' in field_classes: - return 'edit' - - def icon_for_field(self, field_id, icon_id): - """ - Check if field icon is present. - """ - self.wait_for_field(field_id) - - query = self.q(css=f'.u-field-{field_id} .u-field-icon') - return query.present and icon_id in query.attrs('class')[0].split() - - def title_for_field(self, field_id): - """ - Return the title of a field. - """ - self.wait_for_field(field_id) - query = self.q(css=f'.u-field-{field_id} .u-field-title') - return query.text[0] if query.present else None - - def message_for_field(self, field_id): - """ - Return the current message in a field. - """ - self.wait_for_field(field_id) - query = self.q(css=f'.u-field-{field_id} .u-field-message') - return query.text[0] if query.present else None - - def message_for_textarea_field(self, field_id): - """ - Return the current message for textarea field. - """ - self.wait_for_field(field_id) - - query = self.q(css=f'.u-field-{field_id} .u-field-message-help') - return query.text[0] if query.present else None - - def wait_for_message(self, field_id, message): - """ - Wait for a message to appear in a field. - """ - EmptyPromise( - lambda: message in (self.message_for_field(field_id) or ''), - f"Messsage \"{message}\" is visible." - ).fulfill() - - def indicator_for_field(self, field_id): - """ - Return the name of the current indicator in a field. - """ - self.wait_for_field(field_id) - - query = self.q(css=f'.u-field-{field_id} .u-field-message .fa') - return [ - class_name for class_name - in query.attrs('class')[0].split(' ') - if class_name.startswith('message') - ][0].partition('-')[2] if query.present else None - - def wait_for_indicator(self, field_id, indicator): - """ - Wait for an indicator to appear in a field. - """ - EmptyPromise( - lambda: indicator == self.indicator_for_field(field_id), - f"Indicator \"{self.indicator_for_field(field_id)}\" is visible." - ).fulfill() - - def make_field_editable(self, field_id): - """ - Make a field editable. - """ - query = self.q(css=f'.u-field-{field_id}') - - if not query.present: - return None - - field_classes = query.attrs('class')[0].split() - - if 'mode-placeholder' in field_classes or 'mode-display' in field_classes: - if field_id == 'bio': - bio_field_selector = '.u-field-bio > .wrapper-u-field' - self.wait_for_element_visibility(bio_field_selector, 'Bio field is visible') - self.browser.execute_script("$('" + bio_field_selector + "').click();") - else: - self.q(css=f'.u-field-{field_id}').first.click() - - def value_for_readonly_field(self, field_id): - """ - Return the value in a readonly field. - """ - self.wait_for_field(field_id) - - query = self.q(css=f'.u-field-{field_id} .u-field-value') - if not query.present: - return None - - return query.text[0] - - def value_for_text_field(self, field_id, value=None, press_enter=True): - """ - Get or set the value of a text field. - """ - self.wait_for_field(field_id) - query = self.q(css=f'.u-field-{field_id} input') - if not query.present: - return None - - if value is not None: - current_value = query.attrs('value')[0] - query.results[0].send_keys('\ue003' * len(current_value)) # Delete existing value. - query.results[0].send_keys(value) # Input new value - if press_enter: - query.results[0].send_keys('\ue007') # Press Enter - return query.attrs('value')[0] - - def set_value_for_textarea_field(self, field_id, value): - """ - Set the value of a textarea field. - """ - self.wait_for_field(field_id) - self.make_field_editable(field_id) - - field_selector = f'.u-field-{field_id} textarea' - self.wait_for_element_presence(field_selector, 'Editable textarea is present.') - - query = self.q(css=field_selector) - query.fill(value) - query.results[0].send_keys('\ue007') # Press Enter - - def get_non_editable_mode_value(self, field_id): - """ - Return value of field in `display` or `placeholder` mode. - """ - self.wait_for_field(field_id) - self.wait_for_ajax() - - return self.q(css=f'.u-field-{field_id} .u-field-value .u-field-value-readonly').text[0] - - def value_for_dropdown_field(self, field_id, value=None, focus_out=False): - """ - Get or set the value in a dropdown field. - """ - self.wait_for_field(field_id) - - self.make_field_editable(field_id) - - query = self.q(css=f'.u-field-{field_id} select') - if not query.present: - return None - - if value is not None: - select_option_by_text(query, value, focus_out) - - if self.mode_for_field(field_id) == 'edit': - return get_selected_option_text(query) - else: - return self.get_non_editable_mode_value(field_id) - - def link_title_for_link_field(self, field_id): - """ - Return the title of the link in a link field. - """ - self.wait_for_field(field_id) - - query = self.q(css=f'.u-field-link-title-{field_id}') - return query.text[0] if query.present else None - - def wait_for_link_title_for_link_field(self, field_id, expected_title): - """ - Wait until the title of the specified link field equals expected_title. - """ - return EmptyPromise( - lambda: self.link_title_for_link_field(field_id) == expected_title, - f"Link field with link title \"{expected_title}\" is visible." - ).fulfill() - - def click_on_link_in_link_field(self, field_id, field_type='a'): - """ - Click the link in a link field. - """ - self.wait_for_field(field_id) - - query = self.q(css=f'.u-field-{field_id} {field_type}') - if query.present: - query.first.click() - - def error_for_field(self, field_id): - """ - Returns bool based on the highlighted border for field. - """ - query = self.q(css=f'.u-field-{field_id}.error') - return True if query.present else False # lint-amnesty, pylint: disable=simplifiable-if-expression - - def get_social_first_element(self): - """ - Returns the title of first social media link. - """ - query = self.q(css='.u-field-social_links > .field > .field-label') - return query[0].text diff --git a/common/test/acceptance/pages/lms/instructor_dashboard.py b/common/test/acceptance/pages/lms/instructor_dashboard.py deleted file mode 100644 index 9eefc29ccd..0000000000 --- a/common/test/acceptance/pages/lms/instructor_dashboard.py +++ /dev/null @@ -1,661 +0,0 @@ -""" -Instructor (2) dashboard page. -""" - - -import os - -from bok_choy.page_object import PageObject -from bok_choy.promise import EmptyPromise, Promise - -from common.test.acceptance.pages.lms.course_page import CoursePage -from common.test.acceptance.tests.helpers import get_options, get_selected_option_text, select_option_by_text - - -class InstructorDashboardPage(CoursePage): - """ - Instructor dashboard, where course staff can manage a course. - """ - url_path = "instructor" - - def is_browser_on_page(self): - return self.q(css='div.instructor-dashboard-wrapper-2').present - - def get_help_element(self): - """ - Returns the general Help button in the header. - """ - return self.q(css='.help-link').first - - def select_membership(self): - """ - Selects the membership tab and returns the MembershipSection - """ - self.q(css='[data-section="membership"]').first.click() - membership_section = MembershipPage(self.browser) - membership_section.wait_for_page() - return membership_section - - def select_cohort_management(self): - """ - Selects the cohort management tab and returns the CohortManagementSection - """ - self.q(css='[data-section="cohort_management"]').first.click() - cohort_management_section = CohortManagementSection(self.browser) - # The first time cohort management is selected, an ajax call is made. - cohort_management_section.wait_for_ajax() - cohort_management_section.wait_for_page() - return cohort_management_section - - def select_certificates(self): - """ - Selects the certificates tab and returns the CertificatesSection - """ - self.q(css='[data-section="certificates"]').first.click() - certificates_section = CertificatesPage(self.browser) - certificates_section.wait_for_page() - return certificates_section - - def select_bulk_email(self): - """ - Selects the email tab and returns the bulk email section - """ - self.q(css='[data-section="send_email"]').first.click() - email_section = BulkEmailPage(self.browser) - email_section.wait_for_page() - return email_section - - @staticmethod - def get_asset_path(file_name): - """ - Returns the full path of the file to upload. - These files have been placed in edx-platform/common/test/data/uploads/ - """ - - # Separate the list of folders in the path reaching to the current file, - # e.g. '... common/test/acceptance/pages/lms/instructor_dashboard.py' will result in - # [..., 'common', 'test', 'acceptance', 'pages', 'lms', 'instructor_dashboard.py'] - folders_list_in_path = os.path.abspath(__file__).split(os.sep) - - # Get rid of the last 4 elements: 'acceptance', 'pages', 'lms', and 'instructor_dashboard.py' - # to point to the 'test' folder, a shared point in the path's tree. - folders_list_in_path = folders_list_in_path[:-4] - - # Append the folders in the asset's path - folders_list_in_path.extend(['data', 'uploads', file_name]) - - # Return the joined path of the required asset. - return os.sep.join(folders_list_in_path) - - -class CohortManagementSection(PageObject): - """ - The Cohort Management section of the Instructor dashboard. - """ - url = None - cohort_help_css = '.setup-value .incontext-help.action-secondary.action-help' - csv_browse_button_selector_css = '.csv-upload #file-upload-form-file' - csv_upload_button_selector_css = '.csv-upload #file-upload-form-submit' - content_group_selector_css = 'select.input-cohort-group-association' - no_content_group_button_css = '.cohort-management-details-association-course input.radio-no' - select_content_group_button_css = '.cohort-management-details-association-course input.radio-yes' - assignment_type_buttons_css = '.cohort-management-assignment-type-settings input' - - def get_cohort_help_element(self): - """ - Returns the help element ('What does it mean') - - Returns: - help_element (WebElement): help link element - """ - return self.q(css=self.cohort_help_css).results[0] - - def is_browser_on_page(self): - """ - Cohorts management exists under one class; however, render time can be longer because of sub-classes - that must be rendered beneath it. To determine if the browser is on the cohorts management page (and - allow for it to fully-render), we need to consider three different states of the page: - * When no cohorts have been added yet - * When a new cohort is being added (a confirmation state) - * When cohorts exist (the traditional management page) - """ - cohorts_warning_title = '.message-warning .message-title' - - if self.q(css=cohorts_warning_title).visible: - return self.q(css='.message-title').text[0] == 'You currently have no cohorts configured' - # The page may be in either the traditional management state, or an 'add new cohort' state. - # Confirm the CSS class is visible because the CSS class can exist on the page even in different states. - return self.q(css='.cohorts-state-section').visible or self.q(css='.new-cohort-form').visible - - def _bounded_selector(self, selector): - """ - Return `selector`, but limited to the cohort management context. - """ - return f'.cohort-management {selector}' - - def _get_cohort_options(self): - """ - Returns the available options in the cohort dropdown, including the initial "Select a cohort". - """ - def check_func(): - """Promise Check Function""" - query = self.q(css=self._bounded_selector("#cohort-select option")) - return len(query) > 0, query - - return Promise(check_func, "Waiting for cohort selector to populate").fulfill() - - def _cohort_name(self, label): - """ - Returns the name of the cohort with the count information excluded. - """ - return label.split(' (')[0] - - def _cohort_count(self, label): - """ - Returns the count for the cohort (as specified in the label in the selector). - """ - return int(label.split(' (')[1].split(')')[0]) - - def save_cohort_settings(self): - """ - Click on Save button shown after click on Settings tab or when we add a new cohort. - """ - self.q(css=self._bounded_selector("div.form-actions .action-save")).first.click() - - @property - def is_assignment_settings_disabled(self): - """ - Check if assignment settings are disabled. - """ - attributes = self.q(css=self._bounded_selector('.cohort-management-assignment-type-settings')).attrs('class') - if 'is-disabled' in attributes[0].split(): - return True - - return False - - @property - def assignment_settings_message(self): - """ - Return assignment settings disabled message in case of default cohort. - """ - query = self.q(css=self._bounded_selector('.copy-error')) - if query.visible: - return query.text[0] - - return '' - - @property - def cohort_name_in_header(self): - """ - Return cohort name as shown in cohort header. - """ - return self._cohort_name(self.q(css=self._bounded_selector(".group-header-title .title-value")).text[0]) - - def get_cohorts(self): - """ - Returns, as a list, the names of the available cohorts in the drop-down, filtering out "Select a cohort". - """ - return [ - self._cohort_name(opt.text) - for opt in self._get_cohort_options().filter(lambda el: el.get_attribute('value') != "") - ] - - def get_selected_cohort(self): - """ - Returns the name of the selected cohort. - """ - return self._cohort_name( - self._get_cohort_options().filter(lambda el: el.is_selected()).first.text[0] - ) - - def get_selected_cohort_count(self): - """ - Returns the number of users in the selected cohort. - """ - return self._cohort_count( - self._get_cohort_options().filter(lambda el: el.is_selected()).first.text[0] - ) - - def select_cohort(self, cohort_name): - """ - Selects the given cohort in the drop-down. - """ - # Note: can't use Select to select by text because the count is also included in the displayed text. - self._get_cohort_options().filter( - lambda el: self._cohort_name(el.text) == cohort_name - ).first.click() - # wait for cohort to render as selected on screen - EmptyPromise( - lambda: self.q(css='.title-value').text[0] == cohort_name, - "Waiting to confirm cohort has been selected" - ).fulfill() - - def set_cohort_name(self, cohort_name): - """ - Set Cohort Name. - """ - textinput = self.q(css=self._bounded_selector("#cohort-name")).results[0] - textinput.clear() - textinput.send_keys(cohort_name) - - def set_assignment_type(self, assignment_type): - """ - Set assignment type for selected cohort. - - Arguments: - assignment_type (str): Should be 'random' or 'manual' - """ - css = self._bounded_selector(self.assignment_type_buttons_css) - self.q(css=css).filter(lambda el: el.get_attribute('value') == assignment_type).first.click() - - def add_cohort(self, cohort_name, content_group=None, assignment_type=None): - """ - Adds a new manual cohort with the specified name. - If a content group should also be associated, the name of the content group should be specified. - """ - add_cohort_selector = self._bounded_selector(".action-create") - - # We need to wait because sometime add cohort button is not in a state to be clickable. - self.wait_for_element_presence(add_cohort_selector, 'Add Cohort button is present.') - create_buttons = self.q(css=add_cohort_selector) - # There are 2 create buttons on the page. The second one is only present when no cohort yet exists - # (in which case the first is not visible). Click on the last present create button. - create_buttons.results[len(create_buttons.results) - 1].click() - - # Both the edit and create forms have an element with id="cohort-name". Verify that the create form - # has been rendered. - self.wait_for( - lambda: "Add a New Cohort" in self.q(css=self._bounded_selector(".form-title")).text, - "Create cohort form is visible" - ) - textinput = self.q(css=self._bounded_selector("#cohort-name")).results[0] - textinput.send_keys(cohort_name) - - # Manual assignment type will be selected by default for a new cohort - # if we are not setting the assignment type explicitly - if assignment_type: - self.set_assignment_type(assignment_type) - - if content_group: - self._select_associated_content_group(content_group) - self.save_cohort_settings() - EmptyPromise( - lambda: cohort_name == self.get_selected_cohort(), "Waiting for new cohort" - ).fulfill() - - def get_cohort_group_setup(self): - """ - Returns the description of the current cohort - """ - return self.q(css=self._bounded_selector('.cohort-management-group-setup .setup-value')).first.text[0] - - def select_edit_settings(self): - self.q(css=self._bounded_selector(".action-edit")).first.click() - - def select_manage_settings(self): - """ - Click on Manage Students Tab under cohort management section. - """ - self.q(css=self._bounded_selector(".tab-manage_students")).first.click() - - def add_students_to_selected_cohort(self, users): - """ - Adds a list of users (either usernames or email addresses) to the currently selected cohort. - """ - textinput = self.q(css=self._bounded_selector("#cohort-management-group-add-students")).results[0] - for user in users: - textinput.send_keys(user) - textinput.send_keys(",") - self.q(css=self._bounded_selector("div.cohort-management-group-add .action-primary")).first.click() - # Expect the confirmation message substring. (The full message will differ depending on 1 or >1 students added) - self.wait_for( - lambda: "added to this cohort" in self.get_cohort_confirmation_messages(wait_for_messages=True)[0], - "Student(s) added confirmation message." - ) - - def get_cohort_student_input_field_value(self): - """ - Returns the contents of the input field where students can be added to a cohort. - """ - return self.q( - css=self._bounded_selector("#cohort-management-group-add-students") - ).results[0].get_attribute("value") - - def select_studio_group_settings(self): - """ - When no content groups have been defined, a messages appears with a link - to go to Studio group settings. This method assumes the link is visible and clicks it. - """ - return self.q(css=self._bounded_selector("a.link-to-group-settings")).first.click() - - def get_all_content_groups(self): - """ - Returns all the content groups available for associating with the cohort currently being edited. - """ - selector_query = self.q(css=self._bounded_selector(self.content_group_selector_css)) - return [ - option.text for option in get_options(selector_query) if option.text != "Not selected" - ] - - def get_cohort_associated_content_group(self): - """ - Returns the content group associated with the cohort currently being edited. - If no content group is associated, returns None. - """ - self.select_cohort_settings() - radio_button = self.q(css=self._bounded_selector(self.no_content_group_button_css)).results[0] - if radio_button.is_selected(): - return None - return get_selected_option_text(self.q(css=self._bounded_selector(self.content_group_selector_css))) - - def get_cohort_associated_assignment_type(self): - """ - Returns the assignment type associated with the cohort currently being edited. - """ - self.select_cohort_settings() - css_selector = self._bounded_selector(self.assignment_type_buttons_css) - radio_button = self.q(css=css_selector).filter(lambda el: el.is_selected()).results[0] - return radio_button.get_attribute('value') - - def set_cohort_associated_content_group(self, content_group=None, select_settings=True): - """ - Sets the content group associated with the cohort currently being edited. - If content_group is None, un-links the cohort from any content group. - Presses Save to update the cohort's settings. - """ - if select_settings: - self.select_cohort_settings() - if content_group is None: - self.q(css=self._bounded_selector(self.no_content_group_button_css)).first.click() - else: - self._select_associated_content_group(content_group) - self.save_cohort_settings() - - def _select_associated_content_group(self, content_group): - """ - Selects the specified content group from the selector. Assumes that content_group is not None. - """ - self.select_content_group_radio_button() - select_option_by_text( - self.q(css=self._bounded_selector(self.content_group_selector_css)), content_group - ) - - def select_content_group_radio_button(self): - """ - Clicks the radio button for "No Content Group" association. - Returns whether or not the radio button is in the selected state after the click. - """ - radio_button = self.q(css=self._bounded_selector(self.select_content_group_button_css)).results[0] - if not radio_button.is_enabled(): - return False - radio_button.click() - return radio_button.is_selected() - - def select_cohort_settings(self): - """ - Selects the settings tab for the cohort currently being edited. - """ - self.q(css=self._bounded_selector(".cohort-management-settings li.tab-settings>.toggle-button")).first.click() - - # pylint: disable=redefined-builtin - def get_cohort_settings_messages(self, type="confirmation", wait_for_messages=True): - """ - Returns an array of messages related to modifying cohort settings. If wait_for_messages - is True, will wait for a message to appear. - """ - title_css = "div.cohort-management-settings .message-" + type + " .message-title" - detail_css = "div.cohort-management-settings .message-" + type + " .summary-item" - - return self._get_messages(title_css, detail_css, wait_for_messages=wait_for_messages) - - def _get_cohort_messages(self, type, wait_for_messages=False): - """ - Returns array of messages related to manipulating cohorts directly through the UI for the given type. - """ - title_css = "div.cohort-management-group-add .cohort-" + type + " .message-title" - detail_css = "div.cohort-management-group-add .cohort-" + type + " .summary-item" - - return self._get_messages(title_css, detail_css, wait_for_messages) - - def get_csv_messages(self): - """ - Returns array of messages related to a CSV upload of cohort assignments. - """ - title_css = ".csv-upload .message-title" - detail_css = ".csv-upload .summary-item" - return self._get_messages(title_css, detail_css) - - def _get_messages(self, title_css, details_css, wait_for_messages=False): - """ - Helper method to get messages given title and details CSS. - """ - if wait_for_messages: - EmptyPromise( - lambda: len(self.q(css=self._bounded_selector(title_css)).results) != 0, - "Waiting for messages to appear" - ).fulfill() - message_title = self.q(css=self._bounded_selector(title_css)) - if len(message_title.results) == 0: - return [] - messages = [message_title.first.text[0]] - details = self.q(css=self._bounded_selector(details_css)).results - for detail in details: - messages.append(detail.text) - return messages - - def get_cohort_confirmation_messages(self, wait_for_messages=False): - """ - Returns an array of messages present in the confirmation area of the cohort management UI. - The first entry in the array is the title. Any further entries are the details. - """ - return self._get_cohort_messages("confirmations", wait_for_messages) - - def get_cohort_error_messages(self): - """ - Returns an array of messages present in the error area of the cohort management UI. - The first entry in the array is the title. Any further entries are the details. - """ - return self._get_cohort_messages("errors") - - def get_cohort_related_content_group_message(self): - """ - Gets the error message shown next to the content group selector for the currently selected cohort. - If no message, returns None. - """ - message = self.q(css=self._bounded_selector(".input-group-other .copy-error")) - if not message: - return None - return message.results[0].text - - def select_data_download(self): - """ - Click on the link to the Data Download Page. - """ - self.q(css=self._bounded_selector('[data-section="data_download"]')).first.click() - - def upload_cohort_file(self, filename): - """ - Uploads a file with cohort assignment information. - """ - # Toggle on the CSV upload section. - cvs_upload_toggle_css = '.toggle-cohort-management-secondary' - self.wait_for_element_visibility(cvs_upload_toggle_css, "Wait for csv upload link to appear") - cvs_upload_toggle = self.q(css=self._bounded_selector(cvs_upload_toggle_css)).first - if cvs_upload_toggle: - cvs_upload_toggle.click() - self.wait_for_element_visibility( - self._bounded_selector(self.csv_browse_button_selector_css), - 'File upload link visible' - ) - path = InstructorDashboardPage.get_asset_path(filename) - file_input = self.q(css=self._bounded_selector(self.csv_browse_button_selector_css)).results[0] - file_input.send_keys(path) - self.q(css=self._bounded_selector(self.csv_upload_button_selector_css)).first.click() - - @property - def is_cohorted(self): - """ - Returns the state of `Enable Cohorts` checkbox state. - """ - return self.q(css=self._bounded_selector('.cohorts-state')).selected - - @is_cohorted.setter - def is_cohorted(self, state): - """ - Check/Uncheck the `Enable Cohorts` checkbox state. - """ - if state != self.is_cohorted: - self.q(css=self._bounded_selector('.cohorts-state')).first.click() - self.wait_for_ajax() - - def cohort_management_controls_visible(self): - """ - Return the visibility status of cohort management controls(cohort selector section etc). - """ - return (self.q(css=self._bounded_selector('.cohort-management-nav')).visible and - self.q(css=self._bounded_selector('.wrapper-cohort-supplemental')).visible) - - -class BulkEmailPage(PageObject): - """ - Bulk email section of the instructor dashboard. - This feature is controlled by an admin panel feature flag, which is turned on via database fixture for testing. - """ - url = None - - def is_browser_on_page(self): - return self.q(css='[data-section=send_email].active-section').present - - -class MembershipPage(PageObject): - """ - Membership section of the Instructor dashboard. - """ - url = None - - def is_browser_on_page(self): - return self.q(css='[data-section=membership].active-section').present - - def select_auto_enroll_section(self): - """ - Returns the MembershipPageAutoEnrollSection page object. - """ - return MembershipPageAutoEnrollSection(self.browser) - - -class MembershipPageAutoEnrollSection(PageObject): - """ - CSV Auto Enroll section of the Membership tab of the Instructor dashboard. - """ - url = None - - auto_enroll_browse_button_selector = '.auto_enroll_csv .file-browse input.file_field#browseBtn-auto-enroll' - auto_enroll_upload_button_selector = '.auto_enroll_csv button[name="enrollment_signup_button"]' - batch_enrollment_selector = '.batch-enrollment' - NOTIFICATION_ERROR = 'error' - NOTIFICATION_WARNING = 'warning' - NOTIFICATION_SUCCESS = 'confirmation' - - def is_browser_on_page(self): - return self.q(css=self.auto_enroll_browse_button_selector).present - - def is_file_attachment_browse_button_visible(self): - """ - Returns True if the Auto-Enroll Browse button is present. - """ - return self.q(css=self.auto_enroll_browse_button_selector).is_present() - - def is_upload_button_visible(self): - """ - Returns True if the Auto-Enroll Upload button is present. - """ - return self.q(css=self.auto_enroll_upload_button_selector).is_present() - - def click_upload_file_button(self): - """ - Clicks the Auto-Enroll Upload Button. - """ - self.q(css=self.auto_enroll_upload_button_selector).click() - - def is_notification_displayed(self, section_type): - """ - Valid inputs for section_type: MembershipPageAutoEnrollSection.NOTIFICATION_SUCCESS / - MembershipPageAutoEnrollSection.NOTIFICATION_WARNING / - MembershipPageAutoEnrollSection.NOTIFICATION_ERROR - Returns True if a {section_type} notification is displayed. - """ - notification_selector = '.auto_enroll_csv .results .message-%s' % section_type - self.wait_for_element_presence(notification_selector, "%s Notification" % section_type.title()) - return self.q(css=notification_selector).is_present() - - def first_notification_message(self, section_type): - """ - Valid inputs for section_type: MembershipPageAutoEnrollSection.NOTIFICATION_WARNING / - MembershipPageAutoEnrollSection.NOTIFICATION_ERROR - Returns the first message from the list of messages in the {section_type} section. - """ - error_message_selector = '.auto_enroll_csv .results .message-%s li.summary-item' % section_type - self.wait_for_element_presence(error_message_selector, "%s message" % section_type.title()) - return self.q(css=error_message_selector).text[0] - - def upload_correct_csv_file(self): - """ - Selects the correct file and clicks the upload button. - """ - self._upload_file('auto_reg_enrollment.csv') - - def upload_csv_file_with_errors_warnings(self): - """ - Selects the file which will generate errors and warnings and clicks the upload button. - """ - self._upload_file('auto_reg_enrollment_errors_warnings.csv') - - def upload_non_csv_file(self): - """ - Selects an image file and clicks the upload button. - """ - self._upload_file('image.jpg') - - def _upload_file(self, filename): - """ - Helper method to upload a file with registration and enrollment information. - """ - file_path = InstructorDashboardPage.get_asset_path(filename) - self.q(css=self.auto_enroll_browse_button_selector).results[0].send_keys(file_path) - self.click_upload_file_button() - - def fill_enrollment_batch_text_box(self, email): - """ - Fill in the form with the provided email and submit it. - """ - email_selector = f"{self.batch_enrollment_selector} textarea" - enrollment_button = f"{self.batch_enrollment_selector} .enrollment-button[data-action='enroll']" - - # Fill the email addresses after the email selector is visible. - self.wait_for_element_visibility(email_selector, 'Email field is visible') - self.q(css=email_selector).fill(email) - - # Verify enrollment button is present before clicking - EmptyPromise( - lambda: self.q(css=enrollment_button).present, "Enrollment button" - ).fulfill() - self.q(css=enrollment_button).click() - - def get_notification_text(self): - """ - Check notification div is visible and have message. - """ - notification_selector = f'{self.batch_enrollment_selector} .request-response' - self.wait_for_element_visibility(notification_selector, 'Notification div is visible') - return self.q(css=f"{notification_selector} h3").text - - -class CertificatesPage(PageObject): - """ - Certificates section of the Instructor dashboard. - """ - url = None - PAGE_SELECTOR = 'section#certificates' - - def is_browser_on_page(self): - return self.q(css='[data-section=certificates].active-section').present diff --git a/common/test/acceptance/pages/lms/learner_profile.py b/common/test/acceptance/pages/lms/learner_profile.py deleted file mode 100644 index acd194eebf..0000000000 --- a/common/test/acceptance/pages/lms/learner_profile.py +++ /dev/null @@ -1,329 +0,0 @@ -""" -Bok-Choy PageObject class for learner profile page. -""" - - -from bok_choy.page_object import PageObject -from bok_choy.promise import EmptyPromise -from bok_choy.query import BrowserQuery -from selenium.webdriver import ActionChains - -from common.test.acceptance.pages.lms import BASE_URL -from common.test.acceptance.pages.lms.fields import FieldsMixin -from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage -from common.test.acceptance.tests.helpers import select_option_by_value - -PROFILE_VISIBILITY_SELECTOR = '#u-field-select-account_privacy option[value="{}"]' -PROFILE_VISIBILITY_INPUT = '#u-field-select-account_privacy' - - -class Badge(PageObject): - """ - Represents a single badge displayed on the learner profile page. - """ - url = None - - def __init__(self, element, browser): - self.element = element - super().__init__(browser) - - def is_browser_on_page(self): - return BrowserQuery(self.element, css=".badge-details").visible - - def modal_displayed(self): - """ - Verifies that the share modal is diplayed. - """ - # The modal is on the page at large, and not a subelement of the badge div. - return self.q(css=".badges-modal").visible - - def display_modal(self): - """ - Click the share button to display the sharing modal for the badge. - """ - BrowserQuery(self.element, css=".share-button").click() - EmptyPromise(self.modal_displayed, "Share modal displayed").fulfill() - EmptyPromise(self.modal_focused, "Focus handed to modal").fulfill() - - def modal_focused(self): - """ - Return True if the badges model has focus, False otherwise. - """ - return self.q(css=".badges-modal").is_focused() - - def bring_model_inside_window(self): - """ - Execute javascript to bring the popup(.badges-model) inside the window. - """ - script_to_execute = ("var popup = document.querySelectorAll('.badges-modal')[0];;" - "popup.style.left = '20%';") - self.browser.execute_script(script_to_execute) - - def close_modal(self): - """ - Close the badges modal and check that it is no longer displayed. - """ - # In chrome, close button is not inside window - # which causes click failures. To avoid this, just change - # the position of the popup - self.bring_model_inside_window() - self.q(css=".badges-modal .close").click() - EmptyPromise(lambda: not self.modal_displayed(), "Share modal dismissed").fulfill() - - -class LearnerProfilePage(FieldsMixin, PageObject): - """ - PageObject methods for Learning Profile Page. - """ - - def __init__(self, browser, username): - """ - Initialize the page. - - Arguments: - browser (Browser): The browser instance. - username (str): Profile username. - """ - super().__init__(browser) - self.username = username - - @property - def url(self): - """ - Construct a URL to the page. - """ - return BASE_URL + "/u/" + self.username - - def is_browser_on_page(self): - """ - Check if browser is showing correct page. - """ - return all([ - self.q(css='body.view-profile .account-settings-container').present, - not self.q(css='ui-loading-indicator').visible - ]) - - @property - def privacy(self): - """ - Get user profile privacy. - - Returns: - 'all_users' or 'private' - """ - return 'all_users' if self.q(css=PROFILE_VISIBILITY_SELECTOR.format('all_users')).selected else 'private' - - def accomplishments_available(self): - """ - Verify that the accomplishments tab is available. - """ - return self.q(css="button[data-url='accomplishments']").visible - - def display_accomplishments(self): - """ - Click the accomplishments tab and wait for the accomplishments to load. - """ - EmptyPromise(self.accomplishments_available, "Accomplishments tab is displayed").fulfill() - self.q(css="button[data-url='accomplishments']").click() - self.wait_for_element_visibility(".badge-list", "Badge list displayed") - - @property - def badges(self): - """ - Get all currently listed badges. - """ - return [Badge(element, self.browser) for element in self.q(css=".badge-display:not(.badge-placeholder)")] - - @privacy.setter - def privacy(self, privacy): - """ - Set user profile privacy. - - Arguments: - privacy (str): 'all_users' or 'private' - """ - self.wait_for_element_visibility('select#u-field-select-account_privacy', 'Privacy dropdown is visible') - - if privacy != self.privacy: - query = self.q(css=PROFILE_VISIBILITY_INPUT) - select_option_by_value(query, privacy) - EmptyPromise(lambda: privacy == self.privacy, f'Privacy is set to {privacy}').fulfill() - self.q(css='.btn-change-privacy').first.click() - self.wait_for_ajax() - - if privacy == 'all_users': - self.wait_for_public_fields() - - def field_is_visible(self, field_id): - """ - Check if a field with id set to `field_id` is shown. - - Arguments: - field_id (str): field id - - Returns: - True/False - """ - self.wait_for_ajax() - return self.q(css=f'.u-field-{field_id}').visible - - def field_is_editable(self, field_id): - """ - Check if a field with id set to `field_id` is editable. - - Arguments: - field_id (str): field id - - Returns: - True/False - """ - self.wait_for_field(field_id) - self.make_field_editable(field_id) - return self.mode_for_field(field_id) == 'edit' - - @property - def visible_fields(self): - """ - Return list of visible fields. - """ - self.wait_for_field('username') - - fields = ['username', 'country', 'language_proficiencies', 'bio'] - return [field for field in fields if self.field_is_visible(field)] - - @property - def editable_fields(self): - """ - Return list of editable fields currently shown on page. - """ - self.wait_for_ajax() - self.wait_for_element_visibility('.u-field-username', 'username is not visible') - - fields = ['country', 'language_proficiencies', 'bio'] - return [field for field in fields if self.field_is_editable(field)] - - @property - def privacy_field_visible(self): - """ - Check if profile visibility selector is shown or not. - - Returns: - True/False - """ - self.wait_for_ajax() - return self.q(css='#u-field-select-account_privacy').visible - - def wait_for_public_fields(self): - """ - Wait for `country`, `language` and `bio` fields to be visible. - """ - EmptyPromise(lambda: self.field_is_visible('country'), 'Country field is visible').fulfill() - EmptyPromise(lambda: self.field_is_visible('language_proficiencies'), 'Language field is visible').fulfill() - EmptyPromise(lambda: self.field_is_visible('bio'), 'About Me field is visible').fulfill() - - @property - def profile_forced_private_message(self): - """ - Returns age limit message. - """ - self.wait_for_ajax() - return self.q(css='#u-field-message-account_privacy').text[0] - - @property - def age_limit_message_present(self): - """ - Check if age limit message is present. - """ - self.wait_for_ajax() - return self.q(css='#u-field-message-account_privacy').visible - - @property - def profile_has_default_image(self): - """ - Return bool if image field has default photo or not. - """ - self.wait_for_field('image') - default_links = self.q(css='.image-frame').attrs('src') - return 'profiles/default' in default_links[0] if default_links else False - - def mouse_hover(self, element): - """ - Mouse over on given element. - """ - mouse_hover_action = ActionChains(self.browser).move_to_element(element) - mouse_hover_action.perform() - - def profile_has_image_with_public_access(self): - """ - Check if image is present with remove/upload access. - """ - self.wait_for_field('image') - - self.mouse_hover(self.browser.find_element_by_css_selector('.image-wrapper')) - self.wait_for_element_visibility('.u-field-upload-button', "upload button is visible") - return self.q(css='.u-field-upload-button').visible - - def profile_has_image_with_private_access(self): - """ - Check if image is present with remove/upload access. - """ - self.wait_for_field('image') - return self.q(css='.u-field-upload-button').visible - - def upload_file(self, filename, wait_for_upload_button=True): - """ - Helper method to upload an image file. - """ - if wait_for_upload_button: - self.wait_for_element_visibility('.u-field-upload-button', "upload button is visible") - file_path = InstructorDashboardPage.get_asset_path(filename) - - # make the elements visible. - self.browser.execute_script('$(".u-field-upload-button").css("opacity",1);') - self.browser.execute_script('$(".upload-button-input").css("opacity",1);') - - self.wait_for_element_visibility('.upload-button-input', "upload button is visible") - self.q(css='.upload-button-input').results[0].send_keys(file_path) - self.wait_for_ajax() - - @property - def image_upload_success(self): - """ - Returns the bool, if image is updated or not. - """ - self.wait_for_field('image') - self.wait_for_ajax() - - self.wait_for_element_visibility('.image-frame', "image box is visible") - image_link = self.q(css='.image-frame').attrs('src') - return 'default-profile' not in image_link[0] - - @property - def profile_image_message(self): - """ - Returns the text message for profile image. - """ - self.wait_for_field('image') - self.wait_for_ajax() - return self.q(css='.message-banner p').text[0] - - def remove_profile_image(self): - """ - Removes the profile image. - """ - self.wait_for_field('image') - self.wait_for_ajax() - - self.wait_for_element_visibility('.image-wrapper', "remove button is visible") - self.q(css='.u-field-remove-button').first.click() - - self.wait_for_ajax() - self.mouse_hover(self.browser.find_element_by_css_selector('.image-wrapper')) - self.wait_for_element_visibility('.u-field-upload-button', "upload button is visible") - return True - - @property - def remove_link_present(self): - self.wait_for_field('image') - self.mouse_hover(self.browser.find_element_by_css_selector('.image-wrapper')) - return self.q(css='.u-field-remove-button').visible diff --git a/common/test/acceptance/pages/lms/problem.py b/common/test/acceptance/pages/lms/problem.py deleted file mode 100644 index c622c9e2f7..0000000000 --- a/common/test/acceptance/pages/lms/problem.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -Problem Page. -""" - - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.common.utils import click_css - - -class ProblemPage(PageObject): - """ - View of problem page. - """ - - url = None - CSS_PROBLEM_HEADER = '.problem-header' - status_indicators = { - 'correct': ['span.correct'], - 'incorrect': ['span.incorrect'], - 'unanswered': ['span.unanswered'], - 'submitted': ['span.submitted'], - 'unsubmitted': ['.unsubmitted'] - } - - def is_browser_on_page(self): - return self.q(css='.xblock-student_view').present - - @property - def problem_name(self): - """ - Return the current problem name. - """ - self.wait_for_element_visibility(self.CSS_PROBLEM_HEADER, 'wait for problem header') - return self.q(css='.problem-header').text[0] - - def click_submit(self): - """ - Click the Submit button. - """ - click_css(self, '.problem .submit') - - def click_choice(self, choice_value): - """ - Click the choice input(radio, checkbox or option) where value matches `choice_value` in choice group. - """ - self.q(css='div.problem .choicegroup input[value="' + choice_value + '"]').first.click() - self.wait_for_ajax() diff --git a/common/test/acceptance/pages/lms/programs.py b/common/test/acceptance/pages/lms/programs.py deleted file mode 100644 index 2832c45ab1..0000000000 --- a/common/test/acceptance/pages/lms/programs.py +++ /dev/null @@ -1,35 +0,0 @@ -"""LMS-hosted Programs pages""" - - -from uuid import uuid4 - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.lms 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 - - -class ProgramDetailsPage(PageObject): - """Program details page.""" - program_uuid = str(uuid4()) - url = f'{BASE_URL}/dashboard/programs/{program_uuid}/' - - def is_browser_on_page(self): - return self.q(css='.js-program-details-wrapper').present diff --git a/common/test/acceptance/pages/lms/progress.py b/common/test/acceptance/pages/lms/progress.py deleted file mode 100644 index f6a81f55a6..0000000000 --- a/common/test/acceptance/pages/lms/progress.py +++ /dev/null @@ -1,57 +0,0 @@ -""" -Student progress page -""" - - -from common.test.acceptance.pages.lms.course_page import CoursePage - - -class ProgressPage(CoursePage): - """ - Student progress page. - """ - - url_path = "progress" - - def is_browser_on_page(self): - is_present = ( - self.q(css='.course-info').present and - self.q(css='.grade-detail-graph').present - ) - return is_present - - def x_tick_sr_text(self, tick_index): - """ - Return an array of the sr text for a specific x-Axis tick on the - progress chart. - """ - selector = self.q(css='#grade-detail-graph .tickLabel')[tick_index] - sr_fields = selector.find_elements_by_class_name('sr') - return [field.text for field in sr_fields] - - def x_tick_label(self, tick_index): - """ - Returns the label for the X-axis tick index, - and a boolean indicating whether or not it is aria-hidden - """ - selector = self.q(css='#grade-detail-graph .xAxis .tickLabel')[tick_index] - tick_label = selector.find_elements_by_tag_name('span')[0] - return [tick_label.text, tick_label.get_attribute('aria-hidden')] - - def y_tick_label(self, tick_index): - """ - Returns the label for the Y-axis tick index, - and a boolean indicating whether or not it is aria-hidden - """ - selector = self.q(css='#grade-detail-graph .yAxis .tickLabel')[tick_index] - tick_label = selector.find_elements_by_tag_name('span')[0] - return [tick_label.text, tick_label.get_attribute('aria-hidden')] - - def graph_overall_score(self): - """ - Returns the sr-only text for overall score on the progress chart, - and the complete text for overall score (including the same sr-text). - """ - selector = self.q(css='#grade-detail-graph .overallGrade')[0] - label = selector.find_elements_by_class_name('sr')[0] - return [label.text, selector.text] diff --git a/common/test/acceptance/pages/lms/staff_view.py b/common/test/acceptance/pages/lms/staff_view.py deleted file mode 100644 index 7b4b23ee16..0000000000 --- a/common/test/acceptance/pages/lms/staff_view.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Staff views of various tabs (e.g. courseware, course home) -""" - - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.lms.courseware import CoursewarePage - - -class StaffPreviewPage(PageObject): - """ - Handles Staff Preview for any course tab that provides that functionality. - """ - url = None - - PREVIEW_MENU_CSS = '.preview-menu' - VIEW_MODE_OPTIONS_CSS = '.preview-menu .action-preview-select option' - - def __init__(self, browser, parent_page=None): - """ - Initialize the staff preview page. - - This page can either be used as a subclass, or a child of a parent page. - - Arguments: - browser: The selenium browser. - parent_page: None if this is being used as a subclass. Otherwise, - the parent_page the contains this staff preview page fragment. - """ - super().__init__(browser) - self.parent_page = parent_page - - def is_browser_on_page(self): - if self.parent_page and not self.parent_page.is_browser_on_page: - return False - return self.q(css=self.PREVIEW_MENU_CSS).present - - @property - def staff_view_mode(self): - """ - Return the currently chosen view mode, e.g. "Staff", "Learner" or a content group. - """ - return self.q(css=self.VIEW_MODE_OPTIONS_CSS).filter(lambda el: el.is_selected()).first.text[0] - - -class StaffCoursewarePage(CoursewarePage, StaffPreviewPage): - """ - View of courseware pages while logged in as course staff - """ - - url = None - - def __init__(self, browser, course_id): - CoursewarePage.__init__(self, browser, course_id) - StaffPreviewPage.__init__(self, browser) - - def is_browser_on_page(self): - if not CoursewarePage.is_browser_on_page(self): - return False - return StaffPreviewPage.is_browser_on_page(self) diff --git a/common/test/acceptance/pages/lms/tab_nav.py b/common/test/acceptance/pages/lms/tab_nav.py deleted file mode 100644 index 325415fa02..0000000000 --- a/common/test/acceptance/pages/lms/tab_nav.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -High-level tab navigation. -""" - - -from bok_choy.page_object import PageObject -from bok_choy.promise import EmptyPromise, Promise - - -class TabNavPage(PageObject): - """ - High-level tab navigation. - """ - - url = None - - # def is_using_v1_style_tabs(self): - # return self.q(css='ol.course-tabs').present - - def is_using_boostrap_style_tabs(self): - return self.q(css='ul.navbar-nav').present - - def is_browser_on_page(self): - return (self.q(css='ol.course-tabs').present or - self.q(css='ul.navbar-nav').present) - - def go_to_tab(self, tab_name): - """ - Navigate to the tab `tab_name`. - """ - - if tab_name not in ['Course', 'Home', 'Discussion', 'Wiki', 'Progress']: - self.warning(f"'{tab_name}' is not a valid tab name") - - # The only identifier for individual tabs is the link href - # so we find the tab with `tab_name` in its text. - tab_css = self._tab_css(tab_name) - - if tab_css is not None: - self.q(css=tab_css).first.click() - else: - self.warning(f"No tabs found for '{tab_name}'") - - self.wait_for_page() - self._is_on_tab_promise(tab_name).fulfill() - - def _tab_css(self, tab_name): - """ - Return the CSS to click for `tab_name`. - If no tabs exist for that name, return `None`. - """ - all_tabs = self.tab_names - - try: - tab_index = all_tabs.index(tab_name) - except ValueError: - return None - else: - if self.is_using_boostrap_style_tabs(): - return f'ul.navbar-nav li:nth-of-type({tab_index + 1}) a' - else: - return f'ol.course-tabs li:nth-of-type({tab_index + 1}) a' - - @property - def tab_names(self): - """ - Return the list of available tab names. If no tab names - are available, wait for them to load. Raises a `BrokenPromiseError` - if the tab names fail to load. - """ - def _standard_check_func(): - tab_names = self.q(css='ol.course-tabs li a').text - return (len(tab_names) > 0, tab_names) - - def _bootstrap_check_func(): - tab_names = self.q(css='ul.navbar-nav li a').text - return (len(tab_names) > 0, tab_names) - - if self.is_using_boostrap_style_tabs(): - return Promise(_bootstrap_check_func, "Get all tab names").fulfill() - else: - return Promise(_standard_check_func, "Get all tab names").fulfill() - - def _is_on_tab(self, tab_name): - """ - Return a boolean indicating whether the current tab is `tab_name`. - This is a private method, so it does NOT enforce the page check, - which is what we want when we're polling the DOM in a promise. - """ - if self.is_using_boostrap_style_tabs(): - current_tab_list = self.q(css='ul.navbar-nav > .nav-item.active').text - else: - current_tab_list = self.q(css='ol.course-tabs > li > a.active').text - - if len(current_tab_list) == 0: - self.warning("Could not find current tab") - return False - else: - return current_tab_list[0].strip().split('\n')[0] == tab_name - - def _is_on_tab_promise(self, tab_name): - """ - Return a `Promise` that the user is on the tab `tab_name`. - """ - # Use the private version of _is_on_tab to skip the page check - return EmptyPromise( - lambda: self._is_on_tab(tab_name), - f"{tab_name} is the current tab" - ) diff --git a/common/test/acceptance/pages/lms/video/__init__.py b/common/test/acceptance/pages/lms/video/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/test/acceptance/pages/lms/video/video.py b/common/test/acceptance/pages/lms/video/video.py deleted file mode 100644 index b28bf750b5..0000000000 --- a/common/test/acceptance/pages/lms/video/video.py +++ /dev/null @@ -1,227 +0,0 @@ -""" -Video player in the courseware. -""" - - -import logging - -from bok_choy.javascript import js_defined, wait_for_js -from bok_choy.page_object import PageObject -from bok_choy.promise import EmptyPromise # lint-amnesty, pylint: disable=unused-import - -log = logging.getLogger('VideoPage') - -VIDEO_BUTTONS = { - 'transcript': '.language-menu', - 'transcript_button': '.toggle-transcript', - 'cc_button': '.toggle-captions', - 'volume': '.volume', - 'play': '.video_control.play', - 'pause': '.video_control.pause', - 'fullscreen': '.add-fullscreen', - 'download_transcript': '.video-tracks > a', - 'speed': '.speeds', - 'quality': '.quality-control', - 'do_not_show_again': '.skip-control', - 'skip_bumper': '.play-skip-control', -} - -CSS_CLASS_NAMES = { - 'captions_closed': '.video.closed', - 'captions_rendered': '.video.is-captions-rendered', - 'captions': '.subtitles', - 'captions_text': '.subtitles li span', - 'captions_text_getter': '.subtitles li span[role="link"][data-index="{}"]', - 'closed_captions': '.closed-captions', - 'error_message': '.video .video-player .video-error', - 'video_container': '.video', - 'video_sources': '.video-player video source', - 'video_spinner': '.video-wrapper .spinner', - 'video_xmodule': '.xmodule_VideoBlock', - 'video_init': '.is-initialized', - 'video_time': '.vidtime', - 'video_display_name': '.vert h3', - 'captions_lang_list': '.langs-list li', - 'video_speed': '.speeds .value', - 'poster': '.poster', - 'active_caption_text': '.subtitles-menu > li.current span', -} - -VIDEO_MODES = { - 'html5': '.video video', - 'youtube': '.video iframe', - 'hls': '.video video', -} - -VIDEO_MENUS = { - 'language': '.lang .menu', - 'speed': '.speed .menu', - 'download_transcript': '.video-tracks .a11y-menu-list', - 'transcript-format': { - 'srt': '.wrapper-download-transcripts .list-download-transcripts .btn-link[data-value="srt"]', - 'txt': '.wrapper-download-transcripts .list-download-transcripts .btn-link[data-value="txt"]' - }, - 'transcript-skip': '.sr-is-focusable.transcript-start', -} - - -@js_defined('window.Video', 'window.jQuery', 'window.MathJax') -class VideoPage(PageObject): - """ - Video player in the courseware. - """ - - url = None - current_video_display_name = None - - @wait_for_js - def is_browser_on_page(self): - return self.q(css='div{}'.format(CSS_CLASS_NAMES['video_xmodule'])).present - - @wait_for_js - def wait_for_video_class(self): - """ - Wait until element with class name `video` appeared in DOM. - - """ - self.wait_for_ajax() - - video_selector = '{}'.format(CSS_CLASS_NAMES['video_container']) - self.wait_for_element_presence(video_selector, 'Video is initialized') - - @wait_for_js - def wait_for_video_player_render(self, autoplay=False): - """ - Wait until Video Player Rendered Completely. - - """ - self.wait_for_video_class() - self.wait_for_element_presence(CSS_CLASS_NAMES['video_init'], 'Video Player Initialized') - self.wait_for_element_presence(CSS_CLASS_NAMES['video_time'], 'Video Player Initialized') - - video_player_buttons = ['volume', 'fullscreen', 'speed'] - if autoplay: - video_player_buttons.append('pause') - else: - video_player_buttons.append('play') - - for button in video_player_buttons: - self.wait_for_element_visibility(VIDEO_BUTTONS[button], f'{button} button is visible') - - def _is_finished_loading(): - """ - Check if video loading completed. - - Returns: - bool: Tells Video Finished Loading. - - """ - return not self.q(css=CSS_CLASS_NAMES['video_spinner']).visible - - EmptyPromise(_is_finished_loading, 'Finished loading the video', timeout=200).fulfill() - - self.wait_for_ajax() - - def get_video_vertical_selector(self, video_display_name=None): - """ - Get selector for a video vertical with display name specified by `video_display_name`. - - Arguments: - video_display_name (str or None): Display name of a Video. Default vertical selector if None. - - Returns: - str: Vertical Selector for video. - - """ - if video_display_name: - video_display_names = self.q(css=CSS_CLASS_NAMES['video_display_name']).text - if video_display_name not in video_display_names: - raise ValueError(f"Incorrect Video Display Name: '{video_display_name}'") - return f'.vert.vert-{video_display_names.index(video_display_name)}' - else: - return '.vert.vert-0' - - def get_element_selector(self, class_name, vertical=True): - """ - Construct unique element selector. - - Arguments: - class_name (str): css class name for an element. - vertical (bool): do we need vertical css selector or not. vertical css selector is not present in Studio - - Returns: - str: Element Selector. - - """ - if vertical: - return '{vertical} {video_element}'.format( - vertical=self.get_video_vertical_selector(self.current_video_display_name), - video_element=class_name) - else: - return class_name - - def use_video(self, video_display_name): - """ - Set current video display name. - - Arguments: - video_display_name (str): Display name of a Video. - - """ - self.current_video_display_name = video_display_name - - def is_button_shown(self, button_id): - """ - Check if a video button specified by `button_id` is visible. - - Arguments: - button_id (str): key in VIDEO_BUTTONS dictionary, its value will give us the css selector for button. - - Returns: - bool: Tells about a buttons visibility. - - """ - selector = self.get_element_selector(VIDEO_BUTTONS[button_id]) - return self.q(css=selector).visible - - def show_captions(self): - """ - Make Captions Visible. - """ - self._captions_visibility(True) - - def is_captions_visible(self): - """ - Get current visibility sate of captions. - - Returns: - bool: True means captions are visible, False means captions are not visible - - """ - self.wait_for_ajax() - caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['captions']) - return self.q(css=caption_state_selector).visible - - @wait_for_js - def _captions_visibility(self, captions_new_state): - """ - Set the video captions visibility state. - - Arguments: - captions_new_state (bool): True means show captions, False means hide captions - - """ - states = {True: 'Shown', False: 'Hidden'} - state = states[captions_new_state] - - # Make sure that the transcript button is there - EmptyPromise(lambda: self.is_button_shown('transcript_button'), - "transcript button is shown").fulfill() - - # toggle captions visibility state if needed - if self.is_captions_visible() != captions_new_state: - self.click_player_button('transcript_button') # lint-amnesty, pylint: disable=no-member - - # Verify that captions state is toggled/changed - EmptyPromise(lambda: self.is_captions_visible() == captions_new_state, - f"Transcripts are {state}").fulfill() diff --git a/common/test/acceptance/pages/studio/__init__.py b/common/test/acceptance/pages/studio/__init__.py deleted file mode 100644 index 13026028d1..0000000000 --- a/common/test/acceptance/pages/studio/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -Envirement setuo for studio video tests. -""" - - -import os - -# Get the URL of the instance under test -HOSTNAME = os.environ.get('BOK_CHOY_HOSTNAME', 'localhost') -CMS_PORT = os.environ.get('BOK_CHOY_CMS_PORT', 8031) -LMS_PORT = os.environ.get('BOK_CHOY_LMS_PORT', 8003) -BASE_URL = os.environ.get('test_url', f'http://{HOSTNAME}:{CMS_PORT}') -LMS_URL = os.environ.get('test_url', f'http://{HOSTNAME}:{LMS_PORT}') diff --git a/common/test/acceptance/pages/studio/container.py b/common/test/acceptance/pages/studio/container.py deleted file mode 100644 index 2fd548eb17..0000000000 --- a/common/test/acceptance/pages/studio/container.py +++ /dev/null @@ -1,712 +0,0 @@ -""" -Container page in Studio -""" - - -from bok_choy.page_object import PageObject -from bok_choy.promise import EmptyPromise, Promise - -from common.test.acceptance.pages.common.utils import click_css, confirm_prompt -from common.test.acceptance.pages.studio import BASE_URL -from common.test.acceptance.pages.studio.utils import HelpMixin, set_input_value_and_save, type_in_codemirror -from common.test.acceptance.tests.helpers import click_and_wait_for_window - - -class ContainerPage(PageObject, HelpMixin): - """ - Container page in Studio - """ - NAME_SELECTOR = '.page-header-title' - NAME_INPUT_SELECTOR = '.wrapper-xblock-field .xblock-field-input' - NAME_FIELD_WRAPPER_SELECTOR = '.wrapper-xblock-field' - ADD_MISSING_GROUPS_SELECTOR = '.notification-action-button[data-notification-action="add-missing-groups"]' - - def __init__(self, browser, locator): - super().__init__(browser) - self.locator = locator - - @property - def url(self): - """URL to the container page for an xblock.""" - return f"{BASE_URL}/container/{self.locator}" - - @property - def name(self): # lint-amnesty, pylint: disable=missing-function-docstring - titles = self.q(css=self.NAME_SELECTOR).text - if titles: - return titles[0] - else: - return None - - def is_browser_on_page(self): - def _xblock_count(class_name, request_token): - return len(self.q(css='{body_selector} .xblock.{class_name}[data-request-token="{request_token}"]'.format( - body_selector=XBlockWrapper.BODY_SELECTOR, class_name=class_name, request_token=request_token - )).results) - - def _is_finished_loading(): - is_done = False - # Get the request token of the first xblock rendered on the page and assume it is correct. - data_request_elements = self.q(css='[data-request-token]') - if len(data_request_elements) > 0: - request_token = data_request_elements.first.attrs('data-request-token')[0] - # Then find the number of Studio xblock wrappers on the page with that request token. - num_wrappers = len(self.q(css=f'{XBlockWrapper.BODY_SELECTOR} [data-request-token="{request_token}"]').results) # lint-amnesty, pylint: disable=line-too-long - # Wait until all components have been loaded and marked as either initialized or failed. - # See: - # - common/static/js/xblock/core.js which adds the class "xblock-initialized" - # at the end of initializeBlock. - # - common/static/js/views/xblock.js which adds the class "xblock-initialization-failed" - # if the xblock threw an error while initializing. - num_initialized_xblocks = _xblock_count('xblock-initialized', request_token) - num_failed_xblocks = _xblock_count('xblock-initialization-failed', request_token) - is_done = num_wrappers == (num_initialized_xblocks + num_failed_xblocks) - return (is_done, is_done) - - def _loading_spinner_hidden(): - """ promise function to check loading spinner state """ - is_spinner_hidden = self.q(css='div.ui-loading.is-hidden').present - return is_spinner_hidden, is_spinner_hidden - - # First make sure that an element with the view-container class is present on the page, - # and then wait for the loading spinner to go away and all the xblocks to be initialized. - return ( - self.q(css='body.view-container').present and - Promise(_loading_spinner_hidden, 'loading spinner is hidden.').fulfill() and - Promise(_is_finished_loading, 'Finished rendering the xblock wrappers.').fulfill() - ) - - def wait_for_component_menu(self): - """ - Waits until the menu bar of components is present on the page. - """ - EmptyPromise( - lambda: self.q(css='div.add-xblock-component').present, - 'Wait for the menu of components to be present' - ).fulfill() - - @property - def xblocks(self): - """ - Return a list of xblocks loaded on the container page. - """ - return self._get_xblocks() - - @property - def inactive_xblocks(self): - """ - Return a list of inactive xblocks loaded on the container page. - """ - return self._get_xblocks(".is-inactive ") - - @property - def active_xblocks(self): - """ - Return a list of active xblocks loaded on the container page. - """ - return self._get_xblocks(".is-active ") - - @property - def displayed_children(self): - """ - Return a list of displayed xblocks loaded on the container page. - """ - return self._get_xblocks()[0].children - - @property - def publish_title(self): - """ - Returns the title as displayed on the publishing sidebar component. - """ - return self.q(css='.pub-status').first.text[0] - - @property - def release_title(self): - """ - Returns the title before the release date in the publishing sidebar component. - """ - return self.q(css='.wrapper-release .title').first.text[0] - - @property - def release_date(self): - """ - Returns the release date of the unit (with ancestor inherited from), as displayed - in the publishing sidebar component. - """ - return self.q(css='.wrapper-release .copy').first.text[0] - - @property - def last_saved_text(self): - """ - Returns the last saved message as displayed in the publishing sidebar component. - """ - return self.q(css='.wrapper-last-draft').first.text[0] - - @property - def last_published_text(self): - """ - Returns the last published message as displayed in the sidebar. - """ - return self.q(css='.wrapper-last-publish').first.text[0] - - @property - def currently_visible_to_students(self): - """ - Returns True if the unit is marked as currently visible to students - (meaning that a warning is being displayed). - """ - warnings = self.q(css='.container-message .warning') - if not warnings.is_present(): - return False - warning_text = warnings.first.text[0] - return warning_text == "Caution: The last published version of this unit is live. By publishing changes you will change the student experience." # lint-amnesty, pylint: disable=line-too-long - - def shows_inherited_staff_lock(self, parent_type=None, parent_name=None): # lint-amnesty, pylint: disable=unused-argument - """ - Returns True if the unit inherits staff lock from a section or subsection. - """ - return self.q(css='.bit-publishing .wrapper-visibility .copy .inherited-from').visible - - @property - def sidebar_visibility_message(self): - """ - Returns the text within the sidebar visibility section. - """ - return self.q(css='.bit-publishing .wrapper-visibility').first.text[0] - - @property - def publish_action(self): - """ - Returns the link for publishing a unit. - """ - self.scroll_to_element('.action-publish') - return self.q(css='.action-publish').first - - def publish(self): - """ - Publishes the container. - """ - self.scroll_to_element('.action-publish') - click_css(self, '.action-publish', 0, require_notification=False) # lint-amnesty, pylint: disable=unexpected-keyword-arg - - def discard_changes(self): - """ - Discards draft changes (which will then re-render the page). - """ - self.scroll_to_element('a.action-discard') - click_css(self, 'a.action-discard', 0, require_notification=False) # lint-amnesty, pylint: disable=unexpected-keyword-arg - confirm_prompt(self) - self.wait_for_ajax() - - @property - def xblock_titles(self): - """ - Get titles of x-block present on the page. - Returns: - list: A list of X-block titles - """ - return self.q(css='.wrapper-xblock .level-element .header-details').text - - @property - def content_html(self): - """ - Gets the html of HTML module - Returns: - list: A list containing inner HTMl - """ - self.wait_for_element_visibility('.xmodule_HtmlBlock', 'Xblock content is visible') - html = self.q(css='.xmodule_HtmlBlock').html - html = html[0].strip() - return html - - @property - def is_staff_locked(self): - """ Returns True if staff lock is currently enabled, False otherwise """ - for attr in self.q(css='a.action-staff-lock>.fa').attrs('class'): - if 'fa-check-square-o' in attr: - return True - return False - - def toggle_staff_lock(self, inherits_staff_lock=False): - """ - Toggles "hide from students" which enables or disables a staff-only lock. - - Returns True if the lock is now enabled, else False. - """ - was_locked_initially = self.is_staff_locked - if not was_locked_initially: - self.q(css='a.action-staff-lock').first.click() - else: - click_css(self, 'a.action-staff-lock', 0, require_notification=False) # lint-amnesty, pylint: disable=unexpected-keyword-arg - if not inherits_staff_lock: - confirm_prompt(self) - self.wait_for_ajax() - return not was_locked_initially - - def view_published_version(self): - """ - Clicks "View Live Version", which will open the published version of the unit page in the LMS. - - Switches the browser to the newly opened LMS window. - """ - click_and_wait_for_window(self, self.q(css='.button-view').first) - self._switch_to_lms() - - def verify_publish_title(self, expected_title): - """ - Waits for the publish title to change to the expected value. - """ - def wait_for_title_change(): - """ - Promise function to check publish title. - """ - return (self.publish_title == expected_title, self.publish_title) - - Promise(wait_for_title_change, "Publish title incorrect. Found '" + self.publish_title + "'").fulfill() - - def preview(self): - """ - Clicks "Preview", which will open the draft version of the unit page in the LMS. - - Switches the browser to the newly opened LMS window. - """ - self.q(css='.button-preview').first.click() - self._switch_to_lms() - - def _switch_to_lms(self): - """ - Assumes LMS has opened-- switches to that window. - """ - browser_window_handles = self.browser.window_handles - # Switch to browser window that shows HTML Unit in LMS - # The last handle represents the latest windows opened - self.browser.switch_to_window(browser_window_handles[-1]) - - def _get_xblocks(self, prefix=""): - return self.q(css=prefix + XBlockWrapper.BODY_SELECTOR).map( - lambda el: XBlockWrapper(self.browser, el.get_attribute('data-locator'))).results - - def duplicate(self, source_index): - """ - Duplicate the item with index source_index (based on vertical placement in page). - """ - click_css(self, '.duplicate-button', source_index) - - def delete(self, source_index): - """ - Delete the item with index source_index (based on vertical placement in page). - Only visible items are counted in the source_index. - The index of the first item is 0. - """ - # Click the delete button - click_css(self, '.delete-button', source_index, require_notification=False) # lint-amnesty, pylint: disable=unexpected-keyword-arg - # Click the confirmation dialog button - confirm_prompt(self) - - def edit(self): - """ - Clicks the "edit" button for the first component on the page. - """ - return _click_edit(self, '.edit-button', '.xblock-studio_view') - - def edit_visibility(self): - """ - Clicks the edit visibility button for this container. - """ - return _click_edit(self, '.access-button', '.xblock-visibility_view') - - def verify_confirmation_message(self, message, verify_hidden=False): - """ - Verify for confirmation message is present or hidden. - """ - def _verify_message(): - """ promise function to check confirmation message state """ - text = self.q(css='#page-alert .alert.confirmation #alert-confirmation-title').text - return text and message not in text[0] if verify_hidden else text and message in text[0] - - self.wait_for(_verify_message, description='confirmation message {status}'.format( - status='hidden' if verify_hidden else 'present' - )) - - def click_undo_move_link(self): - """ - Click undo move link. - """ - click_css(self, '#page-alert .alert.confirmation .nav-actions .action-primary') - - def click_take_me_there_link(self): - """ - Click take me there link. - """ - click_css(self, '#page-alert .alert.confirmation .nav-actions .action-secondary', require_notification=False) # lint-amnesty, pylint: disable=unexpected-keyword-arg - - def add_missing_groups(self): - """ - Click the "add missing groups" link. - Note that this does an ajax call. - """ - self.q(css=self.ADD_MISSING_GROUPS_SELECTOR).first.click() - self.wait_for_ajax() - - # Wait until all xblocks rendered. - self.wait_for_page() - - def missing_groups_button_present(self): - """ - Returns True if the "add missing groups" button is present. - """ - return self.q(css=self.ADD_MISSING_GROUPS_SELECTOR).present - - def get_xblock_information_message(self): - """ - Returns an information message for the container page. - """ - return self.q(css=".xblock-message.information").first.text[0] - - def get_xblock_access_message(self): - """ - Returns a message detailing the access to the specified unit - """ - access_message = self.q(css=".access-message").first - if access_message: - return access_message.text[0] - else: - return "" - - def is_inline_editing_display_name(self): - """ - Return whether this container's display name is in its editable form. - """ - return "is-editing" in self.q(css=self.NAME_FIELD_WRAPPER_SELECTOR).first.attrs("class")[0] - - def get_category_tab_names(self, category_type): - """ - Returns list of tab name in a category. - - Arguments: - category_type (str): category type - - Returns: - list - """ - self.q(css=f'.add-xblock-component-button[data-type={category_type}]').first.click() - return self.q(css=f'.{category_type}-type-tabs>li>a').text - - def get_category_tab_components(self, category_type, tab_index): - """ - Return list of component names in a tab in a category. - - Arguments: - category_type (str): category type - tab_index (int): tab index in a category - - Returns: - list - """ - css = '#tab{tab_index} button[data-category={category_type}] span'.format( - tab_index=tab_index, - category_type=category_type - ) - return self.q(css=css).html - - def set_name(self, name): - """ - Set the name of the unit. - """ - set_input_value_and_save(self, self.NAME_INPUT_SELECTOR, name) - self.wait_for_ajax() - - -class XBlockWrapper(PageObject): - """ - A PageObject representing a wrapper around an XBlock child shown on the Studio container page. - """ - url = None - BODY_SELECTOR = '.studio-xblock-wrapper' - NAME_SELECTOR = '.xblock-display-name' - VALIDATION_SELECTOR = '.xblock-message.validation' - COMPONENT_BUTTONS = { - 'basic_tab': '.editor-tabs li.inner_tab_wrap:nth-child(1) > a', - 'advanced_tab': '.editor-tabs li.inner_tab_wrap:nth-child(2) > a', - 'settings_tab': '.editor-modes .settings-button', - 'save_settings': '.action-save', - } - - def __init__(self, browser, locator): - super().__init__(browser) - self.locator = locator - - def is_browser_on_page(self): - return self.q(css=f'{self.BODY_SELECTOR}[data-locator="{self.locator}"]').present - - def _bounded_selector(self, selector): - """ - Return `selector`, but limited to this particular `CourseOutlineChild` context - """ - return '{}[data-locator="{}"] {}'.format( - self.BODY_SELECTOR, - self.locator, - selector - ) - - @property - def student_content(self): - """ - Returns the text content of the xblock as displayed on the container page. - """ - return self.q(css=self._bounded_selector('.xblock-student_view'))[0].text - - @property - def author_content(self): - """ - Returns the text content of the xblock as displayed on the container page. - (For blocks which implement a distinct author_view). - """ - return self.q(css=self._bounded_selector('.xblock-author_view'))[0].text - - @property - def name(self): # lint-amnesty, pylint: disable=missing-function-docstring - titles = self.q(css=self._bounded_selector(self.NAME_SELECTOR)).text - if titles: - return titles[0] - else: - return None - - @property - def children(self): - """ - Will return any first-generation descendant xblocks of this xblock. - """ - descendants = self.q(css=self._bounded_selector(self.BODY_SELECTOR)).filter(lambda el: el.is_displayed()).map( - lambda el: XBlockWrapper(self.browser, el.get_attribute('data-locator'))).results - - # Now remove any non-direct descendants. - grandkids = [] - for descendant in descendants: - grandkids.extend(descendant.children) - - grand_locators = [grandkid.locator for grandkid in grandkids] - return [descendant for descendant in descendants if descendant.locator not in grand_locators] - - @property - def has_validation_message(self): - """ Is a validation warning/error/message shown? """ - return self.q(css=self._bounded_selector(self.VALIDATION_SELECTOR)).present - - def _validation_paragraph(self, css_class): - """ Helper method to return the

    element of a validation warning """ - return self.q(css=self._bounded_selector(f'{self.VALIDATION_SELECTOR} p.{css_class}')) - - @property - def has_validation_warning(self): - """ Is a validation warning shown? """ - return self._validation_paragraph('warning').present - - @property - def has_validation_error(self): - """ Is a validation error shown? """ - return self._validation_paragraph('error').present - - @property - def has_validation_not_configured_warning(self): - """ Is a validation "not configured" message shown? """ - return self._validation_paragraph('not-configured').present - - @property - def validation_warning_text(self): - """ Get the text of the validation warning. """ - return self._validation_paragraph('warning').text[0] - - @property - def validation_error_text(self): - """ Get the text of the validation error. """ - return self._validation_paragraph('error').text[0] - - @property - def validation_error_messages(self): - return self.q(css=self._bounded_selector(f'{self.VALIDATION_SELECTOR} .xblock-message-item.error')).text - - @property - def validation_not_configured_warning_text(self): - """ Get the text of the validation "not configured" message. """ - return self._validation_paragraph('not-configured').text[0] - - @property - def preview_selector(self): - return self._bounded_selector('.xblock-student_view,.xblock-author_view') - - @property - def has_group_visibility_set(self): - return self.q(css=self._bounded_selector('.wrapper-xblock.has-group-visibility-set')).is_present() - - @property - def has_duplicate_button(self): - """ - Returns true if this xblock has a 'duplicate' button - """ - return self.q(css=self._bounded_selector('.duplicate-button')) - - @property - def has_delete_button(self): - """ - Returns true if this xblock has a 'delete' button - """ - return self.q(css=self._bounded_selector('.delete-button')) - - @property - def has_edit_visibility_button(self): - """ - Returns true if this xblock has an 'edit visibility' button - :return: - """ - return self.q(css=self._bounded_selector('.access-button')).is_present() - - @property - def has_move_modal_button(self): - """ - Returns True if this xblock has move modal button else False - """ - return self.q(css=self._bounded_selector('.move-button')).is_present() - - @property - def get_partition_group_message(self): - """ - Returns the message about user partition group visibility, shown under the display name - (if not present, returns None). - """ - message = self.q(css=self._bounded_selector('.xblock-group-visibility-label')) - return None if len(message) == 0 else message.first.text[0] - - def go_to_container(self): - """ - Open the container page linked to by this xblock, and return - an initialized :class:`.ContainerPage` for that xblock. - """ - return ContainerPage(self.browser, self.locator).visit() - - def edit(self): - """ - Clicks the "edit" button for this xblock. - """ - return _click_edit(self, '.edit-button', '.xblock-studio_view', self._bounded_selector) - - def edit_visibility(self): - """ - Clicks the edit visibility button for this xblock. - """ - return _click_edit(self, '.access-button', '.xblock-visibility_view', self._bounded_selector) - - def open_advanced_tab(self): - """ - Click on Advanced Tab. - """ - self._click_button('advanced_tab') - - def open_basic_tab(self): - """ - Click on Basic Tab. - """ - self._click_button('basic_tab') - - def open_settings_tab(self): - """ - If editing, click on the "Settings" tab - """ - self._click_button('settings_tab') - - def open_move_modal(self): - """ - Opens the move modal. - """ - click_css(self, '.move-button', require_notification=False) # lint-amnesty, pylint: disable=unexpected-keyword-arg - self.wait_for( - lambda: self.q(css='.modal-window.move-modal').visible, description='move modal is visible' - ) - - def set_field_val(self, field_display_name, field_value): - """ - If editing, set the value of a field. - """ - selector = f'{self.editor_selector} li.field label:contains("{field_display_name}") + input' - script = "$(arguments[0]).val(arguments[1]).change();" - self.browser.execute_script(script, selector, field_value) - - def reset_field_val(self, field_display_name): - """ - If editing, reset the value of a field to its default. - """ - scope = f'{self.editor_selector} li.field label:contains("{field_display_name}")' - script = "$(arguments[0]).siblings('.setting-clear').click();" - self.browser.execute_script(script, scope) - - def set_codemirror_text(self, text, index=0): - """ - Set the text of a CodeMirror editor that is part of this xblock's settings. - """ - type_in_codemirror(self, index, text, find_prefix=f'$("{self.editor_selector}").find') - - def set_license(self, license_type): - """ - Uses the UI to set the course's license to the given license_type (str) - """ - css_selector = ( - "ul.license-types li[data-license={license_type}] button" - ).format(license_type=license_type) - self.wait_for_element_presence( - css_selector, - f"{license_type} button is present" - ) - self.q(css=css_selector).click() - - def save_settings(self): - """ - Click on settings Save button. - """ - self._click_button('save_settings') - - @property - def editor_selector(self): - return '.xblock-studio_view' - - def _click_button(self, button_name): - """ - Click on a button as specified by `button_name` - - Arguments: - button_name (str): button name - - """ - self.q(css=self.COMPONENT_BUTTONS[button_name]).first.click() - self.wait_for_ajax() - - def go_to_group_configuration_page(self): - """ - Go to the Group Configuration used by the component. - """ - self.q(css=self._bounded_selector('span.message-text a')).first.click() - - def is_placeholder(self): - """ - Checks to see if the XBlock is rendered as a placeholder without a preview. - """ - return not self.q(css=self._bounded_selector('.wrapper-xblock article')).present - - @property - def group_configuration_link_name(self): - """ - Get Group Configuration name from link. - """ - return self.q(css=self._bounded_selector('span.message-text a')).first.text[0] - - -def _click_edit(page_object, button_css, view_css, bounded_selector=lambda x: x): - """ - Click on the first editing button found and wait for the Studio editor to be present. - """ - page_object.q(css=bounded_selector(button_css)).first.click() - EmptyPromise( - lambda: page_object.q(css=view_css).present, - 'Wait for the Studio editor to be present' - ).fulfill() - - return page_object diff --git a/common/test/acceptance/pages/studio/course_page.py b/common/test/acceptance/pages/studio/course_page.py deleted file mode 100644 index d504f26bbe..0000000000 --- a/common/test/acceptance/pages/studio/course_page.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Base class for pages specific to a course in Studio. -""" - - -import os -from abc import abstractmethod - -from bok_choy.page_object import PageObject -from opaque_keys.edx.locator import CourseLocator - -from common.test.acceptance.pages.studio import BASE_URL -from common.test.acceptance.pages.studio.utils import HelpMixin - - -class CoursePage(PageObject, HelpMixin): - """ - Abstract base class for page objects specific to a course in Studio. - """ - - # Overridden by subclasses to provide the relative path within the course - # Does not need to include the leading forward or trailing slash - url_path = "" - - @abstractmethod - def is_browser_on_page(self): - """ - Verifies browser is on the correct page. - - Should be implemented in child classes. - """ - pass # lint-amnesty, pylint: disable=unnecessary-pass - - def __init__(self, browser, course_org, course_num, course_run): - """ - Initialize the page object for the course located at - `{course_org}.{course_num}.{course_run}` - - These identifiers will likely change in the future. - """ - super().__init__(browser) - self.course_info = { - 'course_org': course_org, - 'course_num': course_num, - 'course_run': course_run - } - - @property - def url(self): - """ - Construct a URL to the page within the course. - """ - # TODO - is there a better way to make this agnostic to the underlying default module store? - default_store = os.environ.get('DEFAULT_STORE', 'draft') - course_key = CourseLocator( - self.course_info['course_org'], - self.course_info['course_num'], - self.course_info['course_run'], - deprecated=(default_store == 'draft') - ) - return "/".join([BASE_URL, self.url_path, str(course_key)]) diff --git a/common/test/acceptance/pages/studio/library.py b/common/test/acceptance/pages/studio/library.py deleted file mode 100644 index cd1e706225..0000000000 --- a/common/test/acceptance/pages/studio/library.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Library edit page in Studio -""" - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.studio import BASE_URL -from common.test.acceptance.pages.studio.pagination import PaginatedMixin -from common.test.acceptance.pages.studio.users import UsersPageMixin -from common.test.acceptance.pages.studio.utils import HelpMixin - - -class LibraryPage(PageObject, HelpMixin): - """ - Base page for Library pages. Defaults URL to the edit page. - """ - def __init__(self, browser, locator): - super().__init__(browser) - self.locator = locator - - @property - def url(self): - """ - URL to the library edit page for the given library. - """ - return f"{BASE_URL}/library/{str(self.locator)}" - - def is_browser_on_page(self): - """ - Returns True iff the browser has loaded the library edit page. - """ - return self.q(css='body.view-library').present - - -class LibraryEditPage(LibraryPage, PaginatedMixin, UsersPageMixin): - """ - Library edit page in Studio - """ - - def wait_until_ready(self): - """ - When the page first loads, there is a loading indicator and most - functionality is not yet available. This waits for that loading to - finish. - - Always call this before using the page. It also disables animations - for improved test reliability. - """ - self.wait_for_ajax() - super().wait_until_ready() diff --git a/common/test/acceptance/pages/studio/overview.py b/common/test/acceptance/pages/studio/overview.py deleted file mode 100644 index 2c80e2f687..0000000000 --- a/common/test/acceptance/pages/studio/overview.py +++ /dev/null @@ -1,392 +0,0 @@ -""" -Course Outline page in Studio. -""" - - -from bok_choy.javascript import js_defined # lint-amnesty, pylint: disable=unused-import -from bok_choy.page_object import PageObject -from bok_choy.promise import EmptyPromise -from selenium.webdriver.support.ui import Select - -from common.test.acceptance.pages.studio.container import ContainerPage -from common.test.acceptance.pages.studio.course_page import CoursePage - - -@js_defined('jQuery') -class CourseOutlineItem: - """ - A mixin class for any :class:`PageObject` shown in a course outline. - """ - # Note there are a few pylint disable=no-member occurances in this class, because - # it was written assuming it is going to be a mixin to a PageObject and will have functions - # such as self.wait_for_ajax, which doesn't exist on a generic `object`. - BODY_SELECTOR = None - EDIT_BUTTON_SELECTOR = '.xblock-field-value-edit' - NAME_SELECTOR = '.item-title' - NAME_INPUT_SELECTOR = '.xblock-field-input' - NAME_FIELD_WRAPPER_SELECTOR = '.xblock-title .wrapper-xblock-field' - STATUS_MESSAGE_SELECTOR = '> div[class$="-status"] .status-messages' - CONFIGURATION_BUTTON_SELECTOR = '.action-item .configure-button' - - def __repr__(self): - # CourseOutlineItem is also used as a mixin for CourseOutlinePage, which doesn't have a locator - # Check for the existence of a locator so that errors when navigating to the course outline page don't show up - # as errors in the repr method instead. - try: - return f"{self.__class__.__name__}(, {self.locator!r})" - except AttributeError: - return f"{self.__class__.__name__}()" - - def _bounded_selector(self, selector): - """ - Returns `selector`, but limited to this particular `CourseOutlineItem` context - """ - # If the item doesn't have a body selector or locator, then it can't be bounded - # This happens in the context of the CourseOutlinePage - # pylint: disable=no-member - if self.BODY_SELECTOR and hasattr(self, 'locator'): - return '{}[data-locator="{}"] {}'.format( - self.BODY_SELECTOR, - self.locator, - selector - ) - else: - return selector - - def edit(self): - """ - Puts the item into editable form. - """ - self.q(css=self._bounded_selector(self.CONFIGURATION_BUTTON_SELECTOR)).first.click() # pylint: disable=no-member - if 'subsection' in self.BODY_SELECTOR: # lint-amnesty, pylint: disable=unsupported-membership-test - modal = SubsectionOutlineModal(self) - else: - modal = CourseOutlineModal(self) - EmptyPromise(lambda: modal.is_shown(), 'Modal is shown.') # pylint: disable=unnecessary-lambda - return modal - - -class CourseOutlineContainer(CourseOutlineItem): - """ - A mixin to a CourseOutline page object that adds the ability to load - a child page object by title or by index. - - CHILD_CLASS must be a :class:`CourseOutlineChild` subclass. - """ - CHILD_CLASS = None - ADD_BUTTON_SELECTOR = '> .outline-content > .add-item a.button-new' - - def children(self, child_class=None): - """ - Returns all the children page objects of class child_class. - """ - if not child_class: - child_class = self.CHILD_CLASS - # pylint: disable=no-member - return self.q(css=self._bounded_selector(child_class.BODY_SELECTOR)).map( - lambda el: child_class(self.browser, el.get_attribute('data-locator'))).results - - def child_at(self, index, child_class=None): - """ - Returns the child at the specified index. - :type self: object - """ - if not child_class: - child_class = self.CHILD_CLASS - - return self.children(child_class)[index] - - -class CourseOutlineChild(PageObject, CourseOutlineItem): - """ - A page object that will be used as a child of :class:`CourseOutlineContainer`. - """ - url = None - BODY_SELECTOR = '.outline-item' - - def __init__(self, browser, locator): - super().__init__(browser) - self.locator = locator - - def is_browser_on_page(self): - return self.q(css=f'{self.BODY_SELECTOR}[data-locator="{self.locator}"]').present - - def _bounded_selector(self, selector): - """ - Return `selector`, but limited to this particular `CourseOutlineChild` context - """ - return '{}[data-locator="{}"] {}'.format( - self.BODY_SELECTOR, - self.locator, - selector - ) - - -class CourseOutlineUnit(CourseOutlineChild): - """ - PageObject that wraps a unit link on the Studio Course Outline page. - """ - url = None - BODY_SELECTOR = '.outline-unit' - NAME_SELECTOR = '.unit-title a' - - def go_to(self): - """ - Open the container page linked to by this unit link, and return - an initialized :class:`.ContainerPage` for that unit. - """ - return ContainerPage(self.browser, self.locator).visit() - - def is_browser_on_page(self): - return self.q(css=self.BODY_SELECTOR).present - - def children(self): - return self.q(css=self._bounded_selector(self.BODY_SELECTOR)).map( - lambda el: CourseOutlineUnit(self.browser, el.get_attribute('data-locator'))).results - - -class CourseOutlineSubsection(CourseOutlineContainer, CourseOutlineChild): - """ - :class`.PageObject` that wraps a subsection block on the Studio Course Outline page. - """ - url = None - - BODY_SELECTOR = '.outline-subsection' - NAME_SELECTOR = '.subsection-title' - NAME_FIELD_WRAPPER_SELECTOR = '.subsection-header .wrapper-xblock-field' - CHILD_CLASS = CourseOutlineUnit - - def unit(self, title): - """ - Return the :class:`.CourseOutlineUnit with the title `title`. - """ - return self.child(title) # lint-amnesty, pylint: disable=no-member - - def units(self): - """ - Returns the units in this subsection. - """ - return self.children() - - def unit_at(self, index): - """ - Returns the CourseOutlineUnit at the specified index. - """ - return self.child_at(index) - - def add_unit(self): - """ - Adds a unit to this subsection - """ - self.q(css=self._bounded_selector(self.ADD_BUTTON_SELECTOR)).click() - - -class CourseOutlineSection(CourseOutlineContainer, CourseOutlineChild): - """ - :class`.PageObject` that wraps a section block on the Studio Course Outline page. - """ - url = None - BODY_SELECTOR = '.outline-section' - NAME_SELECTOR = '.section-title' - NAME_FIELD_WRAPPER_SELECTOR = '.section-header .wrapper-xblock-field' - CHILD_CLASS = CourseOutlineSubsection - - def subsection(self, title): - """ - Return the :class:`.CourseOutlineSubsection` with the title `title`. - """ - return self.child(title) # lint-amnesty, pylint: disable=no-member - - def subsections(self): - """ - Returns a list of the CourseOutlineSubsections of this section - """ - return self.children() - - def subsection_at(self, index): - """ - Returns the CourseOutlineSubsection at the specified index. - """ - return self.child_at(index) - - def add_subsection(self): - """ - Adds a subsection to this section - """ - self.add_child() # lint-amnesty, pylint: disable=no-member - - -class ExpandCollapseLinkState: - """ - Represents the three states that the expand/collapse link can be in - """ - MISSING = 0 - COLLAPSE = 1 - EXPAND = 2 - - -class CourseOutlinePage(CoursePage, CourseOutlineContainer): - """ - Course Outline page in Studio. - """ - url_path = "course" - CHILD_CLASS = CourseOutlineSection - EXPAND_COLLAPSE_CSS = '.button-toggle-expand-collapse' - BOTTOM_ADD_SECTION_BUTTON = '.outline > .add-section .button-new' - - def is_browser_on_page(self): - return all([ - self.q(css='body.view-outline').present, - self.q(css='.content-primary').present, - self.q(css='div.ui-loading.is-hidden').present - ]) - - def section_at(self, index): - """ - Returns the :class:`.CourseOutlineSection` at the specified index. - """ - return self.child_at(index) - - def start_reindex(self): - """ - Starts course reindex by clicking reindex button - """ - self.reindex_button.click() # lint-amnesty, pylint: disable=no-member - - def open_subsection_settings_dialog(self, index=0): - """ - clicks on the settings button of subsection. - """ - self.q(css=".subsection-header-actions .configure-button").nth(index).click() - self.wait_for_element_presence('.course-outline-modal', 'Subsection settings modal is present.') - - def select_advanced_tab(self, desired_item='special_exam'): - """ - Select the advanced settings tab - """ - self.q(css=".settings-tab-button[data-tab='advanced']").first.click() - if desired_item == 'special_exam': - self.wait_for_element_presence('input.no_special_exam', 'Special exam settings fields not present.') - if desired_item == 'gated_content': - self.wait_for_element_visibility('#is_prereq', 'Gating settings fields are present.') - - -class CourseOutlineModal: - """ - Page object specifically for a modal window on the course outline page. - - Subsections are handled slightly differently in some regards, and should use SubsectionOutlineModal. - """ - MODAL_SELECTOR = ".wrapper-modal-window" - - def __init__(self, page): - self.page = page - - def _bounded_selector(self, selector): - """ - Returns `selector`, but limited to this particular `CourseOutlineModal` context. - """ - return " ".join([self.MODAL_SELECTOR, selector]) - - def is_shown(self): - """ - Return whether or not the modal defined by self.MODAL_SELECTOR is shown. - """ - return self.page.q(css=self.MODAL_SELECTOR).present - - def find_css(self, selector): - """ - Find the given css selector on the page. - """ - return self.page.q(css=self._bounded_selector(selector)) - - def click(self, selector, index=0): - """ - Perform a Click action on the given selector. - """ - self.find_css(selector).nth(index).click() - - def save(self): - """ - Click the save action button, and wait for the ajax call to return. - """ - self.click(".action-save") - self.page.wait_for_ajax() - - @property - def policy(self): - """ - Select the grading format with `value` in the drop-down list. - """ - element = self.find_css('#grading_type')[0] - return self.get_selected_option_text(element) - - @policy.setter - def policy(self, grading_label): - """ - Select the grading format with `value` in the drop-down list. - """ - element = self.find_css('#grading_type')[0] - select = Select(element) - select.select_by_visible_text(grading_label) - - EmptyPromise( - lambda: self.policy == grading_label, - "Grading label is updated.", - ).fulfill() - - def get_selected_option_text(self, element): - """ - Returns the text of the first selected option for the element. - """ - if element: - select = Select(element) - return select.first_selected_option.text - else: - return None - - -class SubsectionOutlineModal(CourseOutlineModal): - """ - Subclass to handle a few special cases with subsection modals. - """ - - @property - def is_explicitly_locked(self): - """ - Override - returns True if staff_only is set. - """ - return self.subsection_visibility == 'staff_only' - - @property - def subsection_visibility(self): - """ - Returns the current visibility setting for a subsection - """ - self.ensure_staff_lock_visible() # lint-amnesty, pylint: disable=no-member - return self.find_css('input[name=content-visibility]:checked').first.attrs('value')[0] - - @is_explicitly_locked.setter - def is_explicitly_locked(self, value): - """ - Override - sets visibility to staff_only if True, else 'visible'. - - For hide_after_due, use the set_subsection_visibility method directly. - """ - self.subsection_visibility = 'staff_only' if value else 'visible' - - @subsection_visibility.setter - def subsection_visibility(self, value): - """ - Sets the subsection visibility to the given value. - """ - self.ensure_staff_lock_visible() # lint-amnesty, pylint: disable=no-member - self.find_css('input[name=content-visibility][value=' + value + ']').click() - EmptyPromise(lambda: value == self.subsection_visibility, "Subsection visibility is updated").fulfill() - - @property - def is_staff_lock_visible(self): - """ - Override - Returns true if the staff lock option is visible. - """ - return self.find_css('input[name=content-visibility]').visible diff --git a/common/test/acceptance/pages/studio/pagination.py b/common/test/acceptance/pages/studio/pagination.py deleted file mode 100644 index e4a66a6832..0000000000 --- a/common/test/acceptance/pages/studio/pagination.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Mixin to include for Paginated container pages -""" - - -from selenium.webdriver.common.keys import Keys - - -class PaginatedMixin: - """ - Mixin class used for paginated page tests. - """ - def nav_disabled(self, position, arrows=('next', 'previous')): - """ - Verifies that pagination nav is disabled. Position can be 'top' or 'bottom'. - - `top` is the header, `bottom` is the footer. - - To specify a specific arrow, pass an iterable with a single element, 'next' or 'previous'. - """ - return all(self.q(css=f'nav.{position} * .{arrow}-page-link.is-disabled') for arrow in arrows) - - def move_back(self, position): - """ - Clicks one of the forward nav buttons. Position can be 'top' or 'bottom'. - """ - 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 * .next-page-link' % position)[0].click() - self.wait_until_ready() - - def go_to_page(self, number): - """ - Enter a number into the page number input field, and then try to navigate to it. - """ - page_input = self.q(css="#page-number-input")[0] - page_input.click() - page_input.send_keys(str(number)) - page_input.send_keys(Keys.RETURN) - self.wait_until_ready() - - def get_page_number(self): - """ - Returns the page number as the page represents it, in string form. - """ - return self.q(css="span.current-page")[0].get_attribute('innerHTML') - - def check_page_unchanged(self, first_block_name): - """ - Used to make sure that a page has not transitioned after a bogus number is given. - """ - if not self.xblocks[0].name == first_block_name: - return False - if not self.q(css='#page-number-input')[0].get_attribute('value') == '': - return False - return True diff --git a/common/test/acceptance/pages/studio/settings.py b/common/test/acceptance/pages/studio/settings.py deleted file mode 100644 index 4b29de9ebb..0000000000 --- a/common/test/acceptance/pages/studio/settings.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Course Schedule and Details Settings page. -""" - - -from bok_choy.javascript import requirejs - -from common.test.acceptance.pages.studio.course_page import CoursePage -from common.test.acceptance.pages.studio.users import wait_for_ajax_or_reload - - -@requirejs('js/factories/settings') -class SettingsPage(CoursePage): - """ - Course Schedule and Details Settings page. - """ - - url_path = "settings/details" - upload_image_browse_button_selector = 'form.upload-dialog input[type=file]' - upload_image_upload_button_selector = '.modal-actions li:nth-child(1) a' - upload_image_popup_window_selector = '.assetupload-modal' - - ################ - # Helpers - ################ - def is_browser_on_page(self): - wait_for_ajax_or_reload(self.browser) - return self.q(css='body.view-settings').visible diff --git a/common/test/acceptance/pages/studio/users.py b/common/test/acceptance/pages/studio/users.py deleted file mode 100644 index b81d226a52..0000000000 --- a/common/test/acceptance/pages/studio/users.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Page classes to test either the Course Team page or the Library Team page. -""" - -from bok_choy.page_object import PageObject -from bok_choy.promise import EmptyPromise - -from common.test.acceptance.tests.helpers import disable_animations - - -def wait_for_ajax_or_reload(browser): - """ - Wait for all ajax requests to finish, OR for the page to reload. - Normal wait_for_ajax() chokes on occasion if the pages reloads, - giving "WebDriverException: Message: u'jQuery is not defined'" - """ - def _is_ajax_finished(): - """ Wait for jQuery to finish all AJAX calls, if it is present. """ - return browser.execute_script("return typeof(jQuery) == 'undefined' || jQuery.active == 0") - - EmptyPromise(_is_ajax_finished, "Finished waiting for ajax requests.").fulfill() - - -class UsersPageMixin(PageObject): - """ Common functionality for course/library team pages """ - new_user_form_selector = '.form-create.create-user .user-email-input' - - def url(self): # lint-amnesty, pylint: disable=invalid-overridden-method - """ - URL to this page - override in subclass - """ - raise NotImplementedError - - def wait_until_ready(self): - """ - When the page first loads, there is a loading indicator and most - functionality is not yet available. This waits for that loading to - finish. - - This method is different from wait_until_no_loading_indicator because this expects - the loading indicator to still exist on the page; it is just hidden. - - It also disables animations for improved test reliability. - """ - - self.wait_for_element_invisibility( - '.ui-loading', - 'Wait for the page to complete its initial loading' - ) - disable_animations(self) diff --git a/common/test/acceptance/pages/studio/utils.py b/common/test/acceptance/pages/studio/utils.py deleted file mode 100644 index 5ed94f6d2a..0000000000 --- a/common/test/acceptance/pages/studio/utils.py +++ /dev/null @@ -1,133 +0,0 @@ -""" -Utility methods useful for Studio page tests. -""" - - -from bok_choy.javascript import js_defined -from selenium.webdriver.common.keys import Keys - -from common.test.acceptance.tests.helpers import click_and_wait_for_window - -NAV_HELP_NOT_SIGNED_IN_CSS = '.nav-item.nav-not-signedin-help a' -NAV_HELP_CSS = '.nav-item.nav-account-help a' -SIDE_BAR_HELP_AS_LIST_ITEM = '.bit li.action-item a' -SIDE_BAR_HELP_CSS = '.external-help a, .external-help-button' - - -@js_defined('window.jQuery') -def type_in_codemirror(page, index, text, find_prefix="$"): # lint-amnesty, pylint: disable=missing-function-docstring - script = """ - var cm = {find_prefix}('div.CodeMirror:eq({index})').get(0).CodeMirror; - CodeMirror.signal(cm, "focus", cm); - cm.setValue(arguments[0]); - CodeMirror.signal(cm, "blur", cm);""".format(index=index, find_prefix=find_prefix) - - page.browser.execute_script(script, str(text)) - - -@js_defined('window.jQuery') -def get_codemirror_value(page, index=0, find_prefix="$"): - return page.browser.execute_script( - "return {find_prefix}('div.CodeMirror:eq({index})').get(0).CodeMirror.getValue();".format( - index=index, find_prefix=find_prefix - ) - ) - - -def set_input_value(page, css, value): - """ - Sets the text field with the given label (display name) to the specified value. - """ - input_element = page.q(css=css).results[0] - # Click in the input to give it the focus - input_element.click() - # Select all, then input the value - input_element.send_keys(Keys.CONTROL + 'a') - input_element.send_keys(value) - # Return the input_element for chaining - return input_element - - -def set_input_value_and_save(page, css, value): - """ - Sets the text field with given label (display name) to the specified value, and presses Save. - """ - set_input_value(page, css, value).send_keys(Keys.ENTER) - page.wait_for_ajax() - - -def verify_ordering(test_class, page, expected_orderings): # pylint: disable=unused-argument - """ - Verifies the expected ordering of xblocks on the page. - """ - xblocks = page.xblocks - blocks_checked = set() - for expected_ordering in expected_orderings: - for xblock in xblocks: - parent = list(expected_ordering.keys())[0] - if xblock.name == parent: - blocks_checked.add(parent) - children = xblock.children - expected_length = len(expected_ordering.get(parent)) - assert expected_length == \ - len(children), f'Number of children incorrect for group {parent}.' \ - f' Expected {expected_length} but got {len(children)}.' - - for idx, expected in enumerate(expected_ordering.get(parent)): - assert expected == children[idx].name - blocks_checked.add(expected) - break - assert len(blocks_checked) == len(xblocks) - - -class HelpMixin: - """ - Mixin for testing Help links. - """ - def get_nav_help_element_and_click_help(self, signed_in=True): - """ - Click on the help, and also get the DOM help element. - - It operates on the help elements in the navigation bar. - - Arguments: - signed_in (bool): Indicates whether user is signed in or not. - - Returns: - WebElement: Help DOM element in the navigation bar. - """ - - element_css = None - if signed_in: - element_css = NAV_HELP_CSS - else: - element_css = NAV_HELP_NOT_SIGNED_IN_CSS - - help_element = self.q(css=element_css).results[0] - click_and_wait_for_window(self, help_element) - return help_element - - def get_side_bar_help_element_and_click_help(self, as_list_item=False, index=-1): - """ - Click on the help, and also get the DOM help element. - - It operates on the help elements in the side bar. - - Arguments: - as_list_item (bool): Indicates whether help element is - enclosed in a 'li' DOM element. - index (int): The index of element in case there are more than - one matching elements. - - Returns: - WebElement: Help DOM element in the side bar. - """ - element_css = None - if as_list_item: - element_css = SIDE_BAR_HELP_AS_LIST_ITEM - else: - element_css = SIDE_BAR_HELP_CSS - - help_element = self.q(css=element_css).results[index] - click_and_wait_for_window(self, help_element) - return help_element diff --git a/common/test/acceptance/pages/studio/video/__init__.py b/common/test/acceptance/pages/studio/video/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/test/acceptance/pages/studio/video/video.py b/common/test/acceptance/pages/studio/video/video.py deleted file mode 100644 index ed7b2030c6..0000000000 --- a/common/test/acceptance/pages/studio/video/video.py +++ /dev/null @@ -1,713 +0,0 @@ -""" -CMS Video -""" - - -import os -import os.path -import time - -import requests -from bok_choy.javascript import js_defined, wait_for_js -from bok_choy.promise import EmptyPromise, Promise -from selenium.webdriver.common.action_chains import ActionChains -from selenium.webdriver.common.keys import Keys - -from common.test.acceptance.pages.common.utils import sync_on_notification # lint-amnesty, pylint: disable=no-name-in-module -from common.test.acceptance.pages.lms.video.video import VideoPage -from common.test.acceptance.tests.helpers import YouTubeStubConfig - -CLASS_SELECTORS = { - 'video_container': '.video', - 'video_init': '.is-initialized', - 'video_xmodule': '.xmodule_VideoBlock', - 'video_spinner': '.video-wrapper .spinner', - 'video_controls': '.video-controls', - 'attach_asset': '.upload-dialog > input[type="file"]', - 'upload_dialog': '.wrapper-modal-window-assetupload', - 'xblock': '.add-xblock-component', - 'slider_range': '.slider-range', - 'error': '.transcripts-error-message', - 'url_inputs': '.videolist-settings-item input.input', - 'collapse_bar': '.videolist-extra-videos', - 'status': '.transcripts-message-status', - 'attach_transcript': '.file-chooser > input[type="file"]', - 'basic_metadata': '.basic_metadata_edit', -} - -BUTTON_SELECTORS = { - 'create_video': 'button[data-category="video"]', - 'handout_download': '.wrapper-handouts .btn-link', - 'handout_download_editor': '.wrapper-comp-setting.file-uploader .download-action', - 'upload_asset': '.upload-action', - 'asset_submit': '.action-upload', - 'handout_clear': '.wrapper-comp-setting.file-uploader .setting-clear', - 'translations_clear': '.metadata-video-translations .setting-clear', - 'translation_add': '.wrapper-translations-settings > a', - 'import': '.setting-import', - 'download_to_edit': '.setting-download', - 'disabled_download_to_edit': '.setting-download.is-disabled', - 'upload_new_timed_transcripts': '.setting-upload', - 'replace': '.setting-replace', - 'choose': '.setting-choose', - 'use_existing': '.setting-use-existing', - 'collapse_link': '.collapse-action.collapse-setting', -} - -DROP_DOWN_SELECTORS = { - 'transcript_language': '.wrapper-translations-settings .list-settings .list-settings-item select' -} - -DISPLAY_NAME = "Component Display Name" - -DEFAULT_SETTINGS = [ - # basic - [DISPLAY_NAME, 'Video', False], - ['Default Video URL', 'https://www.youtube.com/watch?v=3_yD_cEKoCk, , ', False], - ['Video ID', '', False], - - # advanced - [DISPLAY_NAME, 'Video', False], - ['Download Transcript Allowed', 'False', False], - ['Downloadable Transcript URL', '', False], - ['Show Transcript', 'True', False], - ['Transcript Languages', '', False], - ['Upload Handout', '', False], - ['Video Available on Web Only', 'False', False], - ['Video Download Allowed', 'False', False], - ['Video File URLs', '', False], - ['Video ID', '', False], - ['Video Start Time', '00:00:00', False], - ['Video Stop Time', '00:00:00', False], - ['YouTube ID', '3_yD_cEKoCk', False], - ['YouTube ID for .75x speed', '', False], - ['YouTube ID for 1.25x speed', '', False], - ['YouTube ID for 1.5x speed', '', False] -] - -# field names without clear button -FIELDS_WO_CLEAR = [ - 'Transcript Languages' -] - - -# We should wait 300 ms for event handler invocation + 200ms for safety. -DELAY = 0.5 - - -@js_defined('window.Video', 'window.jQuery', 'window.XModule', 'window.XBlock', - 'window.MathJax') -class VideoComponentPage(VideoPage): - """ - CMS Video Component Page - """ - - url = None - - @wait_for_js - def is_browser_on_page(self): - return ( - self.q(css='div{}'.format(CLASS_SELECTORS['video_xmodule'])).present or - self.q(css='div{}'.format(CLASS_SELECTORS['xblock'])).present - ) - - def get_element_selector(self, class_name, vertical=False): - return super().get_element_selector(class_name, vertical=vertical) - - def _wait_for(self, check_func, desc, result=False, timeout=30): - """ - Calls the method provided as an argument until the Promise satisfied or BrokenPromise - - Arguments: - check_func (callable): Promise function to be fulfilled. - desc (str): Description of the Promise, used in log messages. - result (bool): Indicates whether we need result from Promise or not - timeout (float): Maximum number of seconds to wait for the Promise to be satisfied before timing out. - - """ - if result: - return Promise(check_func, desc, timeout=timeout).fulfill() - else: - return EmptyPromise(check_func, desc, timeout=timeout).fulfill() - - def wait_for_video_component_render(self): - """ - Wait until video component rendered completely - """ - if not YouTubeStubConfig.get_configuration().get('youtube_api_blocked'): - self._wait_for(lambda: self.q(css=CLASS_SELECTORS['video_init']).present, 'Video Player Initialized') - self._wait_for(lambda: not self.q(css=CLASS_SELECTORS['video_spinner']).visible, - 'Video Buffering Completed') - self._wait_for(self.is_controls_visible, 'Player Controls are Visible') - - def wait_for_message(self, message_type, expected_message): - """ - Wait until the message of the requested type is as expected. - """ - self._wait_for(lambda: self.message(message_type) == expected_message, "Waiting for message update.") - - @wait_for_js - def is_controls_visible(self): - """ - Get current visibility sate of all video controls. - - Returns: - bool: True means video controls are visible for all videos, False means video controls are not visible - for one or more videos - - """ - return self.q(css=CLASS_SELECTORS['video_controls']).visible - - def click_button_subtitles(self): - """ - Click .setting-replace button after first hovering to it. - """ - element = self.q(css='.setting-replace')[0] - ActionChains(self.browser).move_to_element(element).click(element).perform() - - def click_button(self, button_name, index=0, require_notification=False): - """ - Click on a button as specified by `button_name` - - Arguments: - button_name (str): button name - index (int): query index - - """ - self.scroll_to_button(button_name, index) - self.q(css=BUTTON_SELECTORS[button_name]).nth(index).click() - if require_notification: - sync_on_notification(self) - self.wait_for_ajax() - - def scroll_to_button(self, button_name, index=0): - """ - Scroll to a button specified by `button_name` - - Arguments: - button_name (str): button name - index (int): query index - - """ - element = self.q(css=BUTTON_SELECTORS[button_name])[index] - self.browser.execute_script("arguments[0].scrollIntoView();", element) - - def get_drop_down_items(self, drop_down_name, index=0): - """ - Get the items from a drop down list specified by `drop_down_name` - - Arguments: - drop_down_name (str): name of the drop down list - index (int): query index - - """ - drop_downs = self.q(css=DROP_DOWN_SELECTORS[drop_down_name]) - return drop_downs[index].find_elements_by_tag_name("option") - - def is_language_disabled(self, lang_code): - """ - Determine whether or not a lanuage is disabled in a drop down - - Arguments: - lang_code (str): two letter language code - - """ - language_options = self.get_drop_down_items('transcript_language', index=1) - language = [l for l in language_options if l.get_attribute('value') == lang_code][0] - return language.get_attribute("disabled") - - @staticmethod - def file_path(filename): - """ - Construct file path to be uploaded to assets. - - Arguments: - filename (str): asset filename - - """ - return os.sep.join(os.path.abspath(__file__).split(os.sep)[:-5]) + '/data/uploads/' + filename - - def upload_handout(self, handout_filename): - """ - Upload a handout file to assets - - Arguments: - handout_filename (str): handout file name - - """ - self.upload_asset(handout_filename) - - def upload_asset(self, asset_filename, asset_type='handout', index=0): - """ - Upload a asset file to assets - - Arguments: - asset_filename (str): asset file name - asset_type (str): one of `handout`, `transcript` - index (int): query index - - """ - asset_file_path = self.file_path(asset_filename) - self.scroll_to_button('upload_asset') - self.click_button('upload_asset', index) - self.q(css=CLASS_SELECTORS['attach_asset']).results[0].send_keys(asset_file_path) - # Only srt format transcript files can be uploaded, If an error - # occurs due to incorrect transcript file we will return from here - if asset_type == 'transcript' and self.q(css='#upload_error').present: - return - self.click_button('asset_submit') - # confirm upload completion - self._wait_for(lambda: not self.q(css=CLASS_SELECTORS['upload_dialog']).present, 'Upload Completed') - - def clear_handout(self): - """ - Clear handout from settings - """ - self.click_button('handout_clear') - - def _get_handout(self, url): - """ - Download handout at `url` - """ - kwargs = {} - session_id = [{i['name']: i['value']} for i in self.browser.get_cookies() if i['name'] == 'sessionid'] - if session_id: - kwargs.update({ - 'cookies': session_id[0] - }) - - response = requests.get(url, **kwargs) - - return response.status_code < 400, response.headers - - def download_handout(self, mime_type, is_editor=False): - """ - Download handout with mime type specified by `mime_type` - - Arguments: - mime_type (str): mime type of handout file - - Returns: - tuple: Handout download result. - - """ - selector = BUTTON_SELECTORS['handout_download_editor'] if is_editor else BUTTON_SELECTORS['handout_download'] - handout_url = self.q(css=selector).attrs('href')[0] - result, headers = self._get_handout(handout_url) - - return result, headers['content-type'] == mime_type - - @property - def is_handout_button_visible(self): - """ - Check if handout download button is visible - """ - return self.q(css=BUTTON_SELECTORS['handout_download']).visible - - def create_video(self): - """ - Create a Video Component by clicking on Video button and wait for rendering completion. - """ - # Create video - self.click_button('create_video', require_notification=True) - self.wait_for_video_component_render() - - def xblocks(self): - """ - Tells the total number of video xblocks present on current unit page. - - Returns: - (int): total video xblocks - - """ - return len(self.q(css='.xblock-header').filter( - lambda el: 'xblock-header-video' in el.get_attribute('class')).results) - - def focus_caption_line(self, line_number): - """ - Focus a caption line as specified by `line_number` - - Arguments: - line_number (int): caption line number - - """ - caption_line_selector = f".subtitles li span[data-index='{line_number - 1}']" - self.q(css=caption_line_selector).results[0].send_keys(Keys.ENTER) - - def is_caption_line_focused(self, line_number): - """ - Check if a caption line focused - - Arguments: - line_number (int): caption line number - - """ - caption_line_selector = f".subtitles li span[data-index='{line_number - 1}']" - caption_container = self.q(css=caption_line_selector).results[0].find_element_by_xpath('..') - return 'focused' in caption_container.get_attribute('class').split() - - @property - def is_slider_range_visible(self): - """ - Return True if slider range is visible. - """ - return self.q(css=CLASS_SELECTORS['slider_range']).visible - - def verify_settings(self): - """ - Verify that video component has correct default settings. - """ - def _check_settings_length(): - """Check video settings""" - query = '.wrapper-comp-setting' - settings = self.q(css=query).results - if len(DEFAULT_SETTINGS) == len(settings): - return True, settings - return (False, None) - - settings = Promise(_check_settings_length, 'All video fields are present').fulfill() - - for counter, setting in enumerate(settings): - is_verified = self._verify_setting_entry( - setting, - DEFAULT_SETTINGS[counter][0], - DEFAULT_SETTINGS[counter][1] - ) - - if not is_verified: - return is_verified - - return True - - @staticmethod - def _verify_setting_entry(setting, field_name, field_value): - """ - Verify a `setting` entry. - - Arguments: - setting (WebElement): Selenium WebElement - field_name (str): Name of field - field_value (str): Value of field - - Returns: - bool: Does `setting` have correct value. - - """ - if field_name != setting.find_element_by_class_name('setting-label').get_attribute('innerHTML'): - return False - - # Get class attribute values - classes = setting.get_attribute('class').split() - list_type_classes = ['metadata-list-enum', 'metadata-dict', 'metadata-video-translations'] - is_list_type = any(list_type in classes for list_type in list_type_classes) - - if is_list_type: - current_value = ', '.join( - ele.get_attribute('value') for ele in setting.find_elements_by_class_name('list-settings-item')) - elif 'metadata-videolist-enum' in setting.get_attribute('class'): - current_value = ', '.join(item.find_element_by_tag_name('input').get_attribute('value') for item in - setting.find_elements_by_class_name('videolist-settings-item')) - else: - current_value = setting.find_element_by_class_name('setting-input').get_attribute('value') - - if field_value != current_value: - return False - - # Verify if clear button is active for expected video fields - if field_name not in FIELDS_WO_CLEAR and 'metadata-videolist-enum' not in setting.get_attribute('class'): - setting_clear_button = setting.find_elements_by_class_name('setting-clear')[0] - if 'active' not in setting_clear_button.get_attribute('class'): - return False - - return True - - def set_field_value(self, field_name, field_value, field_type='input'): - """ - Set settings input `field` with `value` - - Arguments: - field_name (str): Name of field - field_value (str): Name of value - field_type (str): `input`, `select` etc(more to be added later) - - """ - query = '.wrapper-comp-setting > label:nth-child(1)' - field_id = '' - - if field_type == 'input': - for index, _ in enumerate(self.q(css=query)): - if field_name in self.q(css=query).nth(index).text[0]: - field_id = self.q(css=query).nth(index).attrs('for')[0] - break - - self.q(css=f'#{field_id}').fill(field_value) - elif field_type == 'select': - self.q(css=f'select[name="{field_name}"] option[value="{field_value}"]').first.click() - - def verify_field_value(self, field_name, field_value): - """ - Get settings value of `field_name` - - Arguments: - field_name (str): Name of field - field_value (str): Name of value - - Returns: - bool: If `field_name` has `field_value` - - """ - _, setting = self._get_setting_entry(field_name) - return self._verify_setting_entry(setting, field_name, field_value) - - def _get_setting_entry(self, field_name): - """ - Get setting entry of `field_name` - - Arguments: - field_name (str): Name of field - - Returns: - setting (WebElement): Selenium WebElement - - """ - for index, setting in enumerate(self.q(css='.wrapper-comp-setting').results): - if setting.find_element_by_class_name('setting-label').get_attribute('innerHTML') == field_name: - return index, setting - - def translations_count(self): - """ - Get count of translations. - """ - return len(self.q(css='.wrapper-translations-settings .list-settings-item').results) - - def select_translation_language(self, language_code, index=0): - """ - Select translation language as specified by `language_code` - - Arguments: - language_code (str): - index (int): query index - - """ - translations_items = '.wrapper-translations-settings .list-settings-item' - language_selector = translations_items + f' select option[value="{language_code}"]' - self.q(css=language_selector).nth(index).click() - - def upload_translation(self, transcript_name, language_code): - """ - Upload a translation file. - - Arguments: - transcript_name (str): - language_code (str): - - """ - self.click_button('translation_add') - translations_count = self.translations_count() - self.select_translation_language(language_code, translations_count - 1) - self.upload_asset(transcript_name, asset_type='transcript', index=translations_count - 1) - - def replace_translation(self, old_lang_code, new_lang_code, transcript_name): - """ - Replace a translation. - - Arguments: - old_lang_code (str): - new_lang_code (str): - transcript_name (str): - - """ - language_codes = self.translations() - index = language_codes.index(old_lang_code) - self.select_translation_language(new_lang_code, index) - self.upload_asset(transcript_name, asset_type='transcript', index=index) - - def translations(self): - """ - Extract translations - - Returns: - list: list of translation language codes - - """ - translations_selector = '.metadata-video-translations .list-settings-item' - return self.q(css=translations_selector).attrs('data-original-lang') - - def download_translation(self, language_code, text_to_search): - """ - Download a translation having `language_code` and containing `text_to_search` - - Arguments: - language_code (str): language code - text_to_search (str): text to search in translation - - Returns: - bool: whether download was successful - - """ - mime_type = 'application/x-subrip' - lang_code = f'?language_code={language_code}' - link = [link for link in self.q(css='.download-action').attrs('href') if lang_code in link] - result, headers, content = self._get_transcript(link[0]) # lint-amnesty, pylint: disable=no-member - - return result is True and mime_type in headers['content-type'] and text_to_search in content.decode('utf-8') - - def remove_translation(self, language_code): - """ - Remove a translation having `language_code` - - Arguments: - language_code (str): language code - - """ - selector = '.metadata-video-translations .list-settings-item' - translation = self.q(css=selector).filter(lambda el: language_code == el.get_attribute('data-original-lang')) - translation[0].find_element_by_class_name('remove-action').click() - - @property - def upload_status_message(self): - """ - Get asset upload status message - """ - return self.q(css='#upload_error').text[0] - - def captions_lines(self): - """ - Extract partial caption lines. - - As all the captions lines are exactly same so only getting partial lines will work. - """ - self.wait_for_captions() # lint-amnesty, pylint: disable=no-member - selector = '.subtitles li:nth-child({})' - return ' '.join([self.q(css=selector.format(i)).text[0] for i in range(1, 6)]) - - def set_url_field(self, url, field_number): - """ - Set video url field in basic settings tab. - - Arguments: - url (str): video url - field_number (int): video url field number - - """ - if self.q(css=CLASS_SELECTORS['collapse_bar']).visible is False: - self.click_button('collapse_link') - - self.q(css=CLASS_SELECTORS['url_inputs']).nth(field_number - 1).fill(url) - time.sleep(DELAY) - self.wait_for_ajax() - - def message(self, message_type): - """ - Get video url field status/error message. - - Arguments: - message_type(str): type(status, error) of message - - Returns: - str: status/error message - - """ - if message_type == 'status': - self.wait_for_element_visibility(CLASS_SELECTORS[message_type], - f'{message_type.title()} message is Visible') - - return self.q(css=CLASS_SELECTORS[message_type]).text[0] - - def url_field_status(self, *field_numbers): - """ - Get video url field status(enable/disable). - - Arguments: - url (str): video url - field_numbers (tuple or None): field numbers to check status for, None means get status for all. - tuple items will be integers and must start from 1 - - Returns: - dict: field numbers as keys and field status(bool) as values, False means a field is disabled - - """ - if field_numbers: - index_list = [number - 1 for number in field_numbers] - else: - index_list = list(range(3)) # maximum three fields - - statuses = {} - for index in index_list: - status = 'is-disabled' not in self.q(css=CLASS_SELECTORS['url_inputs']).nth(index).attrs('class')[0] - statuses[index + 1] = status - - return statuses - - def clear_field(self, index): - """ - Clear a video url field at index specified by `index`. - """ - self.q(css=CLASS_SELECTORS['url_inputs']).nth(index - 1).fill('') - - # Trigger an 'input' event after filling the field with an empty value. - self.browser.execute_script( - "$('{}:eq({})').trigger('{}')".format(CLASS_SELECTORS['url_inputs'], index, 'input')) - - time.sleep(DELAY) - self.wait_for_ajax() - - def clear_fields(self): - """ - Clear video url fields. - """ - script = """ - $('{selector}') - .prop('disabled', false) - .removeClass('is-disabled') - .val('') - .trigger('input'); - """.format(selector=CLASS_SELECTORS['url_inputs']) - self.browser.execute_script(script) - time.sleep(DELAY) - self.wait_for_ajax() - - def revert_field(self, field_name): - """ - Revert a field. - """ - _, setting = self._get_setting_entry(field_name) - setting.find_element_by_class_name('setting-clear').click() - - def is_transcript_button_visible(self, button_name, index=0, button_text=None): - """ - Check if a transcript related button is visible. - - Arguments: - button_name (str): name of button - index (int): query index - button_text (str or None): text to match with text on a button, if None then don't match texts - - Returns: - bool: is button visible - - """ - is_visible = self.q(css=BUTTON_SELECTORS[button_name]).nth(index).visible - - is_text_matched = True - if button_text and button_text != self.q(css=BUTTON_SELECTORS[button_name]).nth(index).text[0]: - is_text_matched = False - - return is_visible and is_text_matched - - def upload_transcript(self, transcript_filename): - """ - Upload a Transcript - - Arguments: - transcript_filename (str): name of transcript file - - """ - # Show the Browse Button - self.browser.execute_script("$('form.file-chooser').show()") - asset_file_path = self.file_path(transcript_filename) - attach_css = CLASS_SELECTORS['attach_transcript'] - self.wait_for_element_visibility(attach_css, "The file chooser's input field is visible.") - self.q(css=attach_css).results[0].send_keys(asset_file_path) - # confirm upload completion - self._wait_for(lambda: not self.q(css=attach_css).visible, 'Upload Completed') diff --git a/common/test/acceptance/tests/__init__.py b/common/test/acceptance/tests/__init__.py deleted file mode 100644 index a04d1659e1..0000000000 --- a/common/test/acceptance/tests/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Contains the Acceptance tests. -""" - - -import logging - -# Silence noisy loggers -LOG_OVERRIDES = [ - ('requests.packages.urllib3.connectionpool', logging.ERROR), - ('django.db.backends', logging.ERROR), - ('stevedore.extension', logging.ERROR), -] - -for log_name, log_level in LOG_OVERRIDES: - logging.getLogger(log_name).setLevel(log_level) diff --git a/common/test/acceptance/tests/data/formula_problem.xml b/common/test/acceptance/tests/data/formula_problem.xml deleted file mode 100644 index 6e76914286..0000000000 --- a/common/test/acceptance/tests/data/formula_problem.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - -

    Some edX courses ask you to enter an algebraic expression as an answer. Try entering the following algebraic expression in the box below. It’s easier than it looks.

    -

    \(A \cdot x^2 + \sqrt{y}\) -

    -

    -The entry is case sensitive. The product must be indicated with an asterisk, and the exponentiation with a caret, so you would write -"A*x^2 + sqrt(y)".

    - - - - - - diff --git a/common/test/acceptance/tests/data/multiple_choice.xml b/common/test/acceptance/tests/data/multiple_choice.xml deleted file mode 100644 index 603047d7d0..0000000000 --- a/common/test/acceptance/tests/data/multiple_choice.xml +++ /dev/null @@ -1,28 +0,0 @@ - -

    Many edX courses have homework or exercises you need to complete. Notice the clock image to the left? That means this homework or exercise needs to be completed for you to pass the course. (This can be a bit confusing; the exercise may or may not have a due date prior to the end of the course.)

    -

    We’ve provided eight (8) examples of how a professor might ask you questions. While the multiple choice question types below are somewhat standard, explore the other question types in the sequence above, like the formula builder- try them all out.

    -

    As you go through the question types, notice how edX gives you immediate feedback on your responses - it really helps in the learning process.

    -

    What color is the open ocean on a sunny day?

    - - - - - - - a table - a desk - a chair - a bookshelf - - - - - - a piano - a tree - a guitar - a window - - -

    -
    diff --git a/common/test/acceptance/tests/data/poll_markdown.xml b/common/test/acceptance/tests/data/poll_markdown.xml deleted file mode 100644 index 17a2aca687..0000000000 --- a/common/test/acceptance/tests/data/poll_markdown.xml +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/common/test/acceptance/tests/discussion/__init__.py b/common/test/acceptance/tests/discussion/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/test/acceptance/tests/discussion/helpers.py b/common/test/acceptance/tests/discussion/helpers.py deleted file mode 100644 index 3c166b529c..0000000000 --- a/common/test/acceptance/tests/discussion/helpers.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -Helper functions and classes for discussion tests. -""" - - -import json -from uuid import uuid4 - - -from common.test.acceptance.fixtures import LMS_BASE_URL -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc -from common.test.acceptance.fixtures.discussion import ( - ForumsConfigMixin, - MultipleThreadFixture, - Thread, -) -from common.test.acceptance.pages.lms.discussion import DiscussionTabSingleThreadPage -from common.test.acceptance.tests.helpers import UniqueCourseTest - - -class BaseDiscussionMixin: - """ - A mixin containing methods common to discussion tests. - """ - - def setup_multiple_threads(self, thread_count, **thread_kwargs): - """ - Set up multiple threads on the page by passing 'thread_count'. - """ - self.thread_ids = [] - threads = [] - for i in range(thread_count): - thread_id = f"test_thread_{i}_{uuid4().hex}" - thread_body = "Dummy long text body." * 50 - threads.append( - Thread(id=thread_id, commentable_id=self.discussion_id, body=thread_body, **thread_kwargs), - ) - self.thread_ids.append(thread_id) - thread_fixture = MultipleThreadFixture(threads) - response = thread_fixture.push() - assert response.ok, 'Failed to push discussion content' - - -class CohortTestMixin: - """ - Mixin for tests of cohorted courses - """ - def setup_cohort_config(self, course_fixture, auto_cohort_groups=None): - """ - Sets up the course to use cohorting with the given list of auto_cohort_groups. - If auto_cohort_groups is None, no auto cohorts are set. - """ - course_fixture._update_xblock(course_fixture._course_location, { # lint-amnesty, pylint: disable=protected-access - "metadata": { - "cohort_config": { - "auto_cohort_groups": auto_cohort_groups or [], - "cohorted_discussions": [], - "cohorted": True, - }, - }, - }) - - def add_manual_cohort(self, course_fixture, cohort_name): - """ - Adds a cohort by name, returning its ID. - """ - url = LMS_BASE_URL + "/courses/" + course_fixture._course_key + '/cohorts/' # lint-amnesty, pylint: disable=protected-access - data = json.dumps({"name": cohort_name, 'assignment_type': 'manual'}) - response = course_fixture.session.post(url, data=data, headers=course_fixture.headers) - assert response.ok, 'Failed to create cohort' - return response.json()['id'] - - def add_user_to_cohort(self, course_fixture, username, cohort_id): - """ - Adds a user to the specified cohort. - """ - url = LMS_BASE_URL + "/courses/" + course_fixture._course_key + f"/cohorts/{cohort_id}/add" # lint-amnesty, pylint: disable=protected-access - data = {"users": username} - course_fixture.headers['Content-type'] = 'application/x-www-form-urlencoded' - response = course_fixture.session.post(url, data=data, headers=course_fixture.headers) - assert response.ok, 'Failed to add user to cohort' - - -class BaseDiscussionTestCase(UniqueCourseTest, ForumsConfigMixin): - """Base test case class for all discussions-related tests.""" - def setUp(self): - super().setUp() - - self.discussion_id = f"test_discussion_{uuid4().hex}" - self.course_fixture = CourseFixture(**self.course_info) - self.course_fixture.add_children( - XBlockFixtureDesc("chapter", "Test Section").add_children( - XBlockFixtureDesc("sequential", "Test Subsection").add_children( - XBlockFixtureDesc("vertical", "Test Unit").add_children( - XBlockFixtureDesc( - "discussion", - "Test Discussion", - metadata={"discussion_id": self.discussion_id} - ) - ) - ) - ) - ) - self.course_fixture.add_advanced_settings( - {'discussion_topics': {'value': {'General': {'id': 'course'}}}} - ) - self.course_fixture.install() - - self.enable_forums() - - def create_single_thread_page(self, thread_id): - """ - Sets up a `DiscussionTabSingleThreadPage` for a given - `thread_id`. - """ - return DiscussionTabSingleThreadPage(self.browser, self.course_id, self.discussion_id, thread_id) diff --git a/common/test/acceptance/tests/discussion/test_cohort_management.py b/common/test/acceptance/tests/discussion/test_cohort_management.py deleted file mode 100644 index 9efc20490e..0000000000 --- a/common/test/acceptance/tests/discussion/test_cohort_management.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -End-to-end tests related to the cohort management on the LMS Instructor Dashboard -""" - - -import uuid - -from common.test.acceptance.fixtures.course import CourseFixture -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage -from common.test.acceptance.tests.discussion.helpers import CohortTestMixin -from common.test.acceptance.tests.helpers import EventsTestMixin, UniqueCourseTest -from openedx.core.lib.tests import attr - - -@attr(shard=8) -class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin): - """ - Tests for cohort management on the LMS Instructor Dashboard - """ - - def setUp(self): - """ - Set up a cohorted course - """ - super().setUp() - - # create course with cohorts - self.manual_cohort_name = "ManualCohort1" - self.auto_cohort_name = "AutoCohort1" - self.course_fixture = CourseFixture(**self.course_info).install() - self.setup_cohort_config(self.course_fixture, auto_cohort_groups=[self.auto_cohort_name]) - self.manual_cohort_id = self.add_manual_cohort(self.course_fixture, self.manual_cohort_name) - - # create a non-instructor who will be registered for the course and in the manual cohort. - self.student_name, self.student_email = self._generate_unique_user_data() - self.student_id = AutoAuthPage( - self.browser, username=self.student_name, email=self.student_email, - course_id=self.course_id, staff=False - ).visit().get_user_id() - self.add_user_to_cohort(self.course_fixture, self.student_name, self.manual_cohort_id) - - # create a second student user - self.other_student_name, self.other_student_email = self._generate_unique_user_data() - self.other_student_id = AutoAuthPage( - self.browser, username=self.other_student_name, email=self.other_student_email, - course_id=self.course_id, staff=False - ).visit().get_user_id() - - # login as an instructor - self.instructor_name, self.instructor_email = self._generate_unique_user_data() - self.instructor_id = AutoAuthPage( - self.browser, username=self.instructor_name, email=self.instructor_email, - course_id=self.course_id, staff=True - ).visit().get_user_id() - - # go to the membership page on the instructor dashboard - self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) - self.instructor_dashboard_page.visit() - self.cohort_management_page = self.instructor_dashboard_page.select_cohort_management() - - def _generate_unique_user_data(self): - """ - Produce unique username and e-mail. - """ - unique_username = 'user' + str(uuid.uuid4().hex)[:12] - unique_email = unique_username + "@example.com" - return unique_username, unique_email - - @attr('a11y') - def test_cohorts_management_a11y(self): - """ - Run accessibility audit for cohort management. - """ - self.cohort_management_page.a11y_audit.config.set_rules({ - "ignore": [ - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - self.cohort_management_page.a11y_audit.check_for_accessibility_errors() diff --git a/common/test/acceptance/tests/discussion/test_discussion.py b/common/test/acceptance/tests/discussion/test_discussion.py deleted file mode 100644 index b5e4675e56..0000000000 --- a/common/test/acceptance/tests/discussion/test_discussion.py +++ /dev/null @@ -1,349 +0,0 @@ -""" -Tests for discussion pages -""" - - -from uuid import uuid4 - -import pytest - -from common.test.acceptance.fixtures.course import CourseFixture # lint-amnesty, pylint: disable=unused-import -from common.test.acceptance.fixtures.discussion import ( - Comment, - Response, - SingleThreadViewFixture, - Thread, - -) -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.discussion import ( - DiscussionTabHomePage, - DiscussionTabSingleThreadPage, - -) -from common.test.acceptance.tests.discussion.helpers import BaseDiscussionMixin, BaseDiscussionTestCase -from common.test.acceptance.tests.helpers import UniqueCourseTest -from openedx.core.lib.tests import attr - -THREAD_CONTENT_WITH_LATEX = """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt # lint-amnesty, pylint: disable=line-too-long - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit sse cillum dolore eu fugiat nulla pariatur. - \n\n----------\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit sse cillum dolore eu fugiat nulla pariatur. (b).\n\n - **(a)** $H_1(e^{j\\omega}) = \\sum_{n=-\\infty}^{\\infty}h_1[n]e^{-j\\omega n} = - \\sum_{n=-\\infty} ^{\\infty}h[n]e^{-j\\omega n}+\\delta_2e^{-j\\omega n_0}$ - $= H(e^{j\\omega})+\\delta_2e^{-j\\omega n_0}=A_e (e^{j\\omega}) e^{-j\\omega n_0} - +\\delta_2e^{-j\\omega n_0}=e^{-j\\omega n_0} (A_e(e^{j\\omega})+\\delta_2) - $H_3(e^{j\\omega})=A_e(e^{j\\omega})+\\delta_2$. Dummy $A_e(e^{j\\omega})$ dummy post $. - $A_e(e^{j\\omega}) \\ge -\\delta_2$, it follows that $H_3(e^{j\\omega})$ is real and - $H_3(e^{j\\omega})\\ge 0$.\n\n**(b)** Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit sse cillum dolore eu fugiat nulla pariatur.\n\n - **Case 1:** If $re^{j\\theta}$ is a Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit sse cillum dolore eu fugiat nulla pariatur. - \n\n**Case 3:** Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit sse cillum dolore eu fugiat nulla pariatur. - Lorem $H_3(e^{j\\omega}) = P(cos\\omega)(cos\\omega - cos\\theta)^k$, - Lorem Lorem Lorem Lorem Lorem Lorem $P(cos\\omega)$ has no - $(cos\\omega - cos\\theta)$ factor. - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit sse cillum dolore eu fugiat nulla pariatur. - $P(cos\\theta) \\neq 0$. Since $P(cos\\omega)$ this is a dummy data post $\\omega$, - dummy $\\delta > 0$ such that for all $\\omega$ dummy $|\\omega - \\theta| - < \\delta$, $P(cos\\omega)$ Lorem ipsum dolor sit amet, consectetur adipiscing elit, - sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim - veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit sse cillum dolore - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit sse cillum dolore eu fugiat nulla pariatur. - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit sse cillum dolore eu fugiat nulla pariatur. - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit sse cillum dolore eu fugiat nulla pariatur. - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit sse cillum dolore eu fugiat nulla pariatur. - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit sse cillum dolore eu fugiat nulla pariatur. - """ - - -@attr(shard=2) -class DiscussionHomePageTest(BaseDiscussionTestCase): - """ - Tests for the discussion home page. - """ - - SEARCHED_USERNAME = "gizmo" - - def setUp(self): - super().setUp() - AutoAuthPage(self.browser, course_id=self.course_id).visit() - self.page = DiscussionTabHomePage(self.browser, self.course_id) - self.page.visit() - - @attr('a11y') - def test_page_accessibility(self): - self.page.a11y_audit.config.set_rules({ - "ignore": [ - 'section', # TODO: AC-491 - 'aria-required-children', # TODO: AC-534 - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region' # TODO: AC-932 - ] - }) - self.page.a11y_audit.check_for_accessibility_errors() - - -class DiscussionTabMultipleThreadTest(BaseDiscussionTestCase, BaseDiscussionMixin): - """ - Tests for the discussion page with multiple threads - """ - def setUp(self): - super().setUp() - AutoAuthPage(self.browser, course_id=self.course_id).visit() - self.thread_count = 2 - self.thread_ids = [] - self.setup_multiple_threads(thread_count=self.thread_count) - - self.thread_page_1 = DiscussionTabSingleThreadPage( - self.browser, - self.course_id, - self.discussion_id, - self.thread_ids[0] - ) - self.thread_page_2 = DiscussionTabSingleThreadPage( - self.browser, - self.course_id, - self.discussion_id, - self.thread_ids[1] - ) - self.thread_page_1.visit() - - @attr('a11y') - def test_page_accessibility(self): - self.thread_page_1.a11y_audit.config.set_rules({ - "ignore": [ - 'section', # TODO: AC-491 - 'aria-required-children', # TODO: AC-534 - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - - self.thread_page_1.a11y_audit.check_for_accessibility_errors() - - self.thread_page_2.a11y_audit.config.set_rules({ - "ignore": [ - 'section', # TODO: AC-491 - 'aria-required-children', # TODO: AC-534 - 'region' # TODO: AC-932 - ] - }) - - self.thread_page_2.a11y_audit.check_for_accessibility_errors() - - -class DiscussionOpenClosedThreadTest(BaseDiscussionTestCase): - """ - Tests for checking the display of attributes on open and closed threads - """ - - def setUp(self): - super().setUp() - - self.thread_id = f"test_thread_{uuid4().hex}" - - def setup_user(self, roles=[]): # lint-amnesty, pylint: disable=dangerous-default-value - roles_str = ','.join(roles) - self.user_id = AutoAuthPage(self.browser, course_id=self.course_id, roles=roles_str).visit().get_user_id() # lint-amnesty, pylint: disable=attribute-defined-outside-init - - def setup_view(self, **thread_kwargs): # lint-amnesty, pylint: disable=missing-function-docstring - thread_kwargs.update({'commentable_id': self.discussion_id}) - view = SingleThreadViewFixture( - Thread(id=self.thread_id, **thread_kwargs) - ) - view.addResponse(Response(id="response1")) - view.push() - - def setup_openclosed_thread_page(self, closed=False): # lint-amnesty, pylint: disable=missing-function-docstring - self.setup_user(roles=['Moderator']) - if closed: - self.setup_view(closed=True) - else: - self.setup_view() - page = self.create_single_thread_page(self.thread_id) - page.visit() - page.close_open_thread() - return page - - @attr('a11y') - def test_page_accessibility(self): - page = self.setup_openclosed_thread_page() - page.a11y_audit.config.set_rules({ - 'ignore': [ - 'section', # TODO: AC-491 - 'aria-required-children', # TODO: AC-534 - 'color-contrast', # Commented out for now because they reproducibly fail on Jenkins but not locally - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - page.a11y_audit.check_for_accessibility_errors() - - page = self.setup_openclosed_thread_page(True) - page.a11y_audit.config.set_rules({ - 'ignore': [ - 'section', # TODO: AC-491 - 'aria-required-children', # TODO: AC-534 - 'color-contrast', # Commented out for now because they reproducibly fail on Jenkins but not locally - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - page.a11y_audit.check_for_accessibility_errors() - - -class DiscussionResponseEditTest(BaseDiscussionTestCase): - """ - Tests for editing responses displayed beneath thread in the single thread view. - """ - def setup_user(self, roles=[]): # lint-amnesty, pylint: disable=dangerous-default-value - roles_str = ','.join(roles) - self.user_id = AutoAuthPage(self.browser, course_id=self.course_id, roles=roles_str).visit().get_user_id() # lint-amnesty, pylint: disable=attribute-defined-outside-init - - def setup_view(self): # lint-amnesty, pylint: disable=missing-function-docstring - view = SingleThreadViewFixture(Thread(id="response_edit_test_thread", commentable_id=self.discussion_id)) - view.addResponse( - Response(id="response_other_author", user_id="other", thread_id="response_edit_test_thread"), - ) - view.addResponse( - Response(id="response_self_author", user_id=self.user_id, thread_id="response_edit_test_thread"), - ) - view.push() - - @attr('a11y') - def test_page_accessibility(self): - self.setup_user() - self.setup_view() - page = self.create_single_thread_page("response_edit_test_thread") - page.a11y_audit.config.set_rules({ - 'ignore': [ - 'section', # TODO: AC-491 - 'aria-required-children', # TODO: AC-534 - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - page.visit() - page.a11y_audit.check_for_accessibility_errors() - - -class DiscussionCommentEditTest(BaseDiscussionTestCase): - """ - Tests for editing comments displayed beneath responses in the single thread view. - """ - def setup_user(self, roles=[]): # lint-amnesty, pylint: disable=dangerous-default-value - roles_str = ','.join(roles) - self.user_id = AutoAuthPage(self.browser, course_id=self.course_id, roles=roles_str).visit().get_user_id() # lint-amnesty, pylint: disable=attribute-defined-outside-init - - def setup_view(self): # lint-amnesty, pylint: disable=missing-function-docstring - view = SingleThreadViewFixture(Thread(id="comment_edit_test_thread", commentable_id=self.discussion_id)) - view.addResponse( - Response(id="response1"), - [Comment(id="comment_other_author", user_id="other"), Comment(id="comment_self_author", user_id=self.user_id)]) # lint-amnesty, pylint: disable=line-too-long - view.push() - - @attr('a11y') - def test_page_accessibility(self): - self.setup_user() - self.setup_view() - page = self.create_single_thread_page("comment_edit_test_thread") - page.visit() - page.a11y_audit.config.set_rules({ - 'ignore': [ - 'section', # TODO: AC-491 - 'aria-required-children', # TODO: AC-534 - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - page.a11y_audit.check_for_accessibility_errors() - - @attr('a11y') - @pytest.mark.skip(reason='This test is too flaky to run at all. TNL-6215') - def test_inline_a11y(self): - """ - Tests Inline Discussion for accessibility issues. - """ - self.setup_multiple_threads(thread_count=3) - - # First test the a11y of the expanded list of threads - self.discussion_page.expand_discussion() - self.discussion_page.a11y_audit.config.set_rules({ - 'ignore': [ - 'section' - ] - }) - self.discussion_page.a11y_audit.check_for_accessibility_errors() - - # Now show the first thread and test the a11y again - self.discussion_page.show_thread(self.thread_ids[0]) - self.discussion_page.a11y_audit.check_for_accessibility_errors() - - # Finally show the new post form and test its a11y - self.discussion_page.click_new_post_button() - self.discussion_page.a11y_audit.check_for_accessibility_errors() - - -class DiscussionSearchAlertTest(UniqueCourseTest): - """ - Tests for spawning and dismissing alerts related to user search actions and their results. - """ - - SEARCHED_USERNAME = "gizmo" - - def setUp(self): - super().setUp() - CourseFixture(**self.course_info).install() - # first auto auth call sets up a user that we will search for in some tests - self.searched_user_id = AutoAuthPage( - self.browser, - username=self.SEARCHED_USERNAME, - course_id=self.course_id - ).visit().get_user_id() - # this auto auth call creates the actual session user - AutoAuthPage(self.browser, course_id=self.course_id).visit() - self.page = DiscussionTabHomePage(self.browser, self.course_id) - self.page.visit() - - @attr('a11y') - def test_page_accessibility(self): - self.page.a11y_audit.config.set_rules({ - 'ignore': [ - 'section', # TODO: AC-491 - 'aria-required-children', # TODO: AC-534 - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - self.page.a11y_audit.check_for_accessibility_errors() diff --git a/common/test/acceptance/tests/helpers.py b/common/test/acceptance/tests/helpers.py deleted file mode 100644 index bda79cb0ff..0000000000 --- a/common/test/acceptance/tests/helpers.py +++ /dev/null @@ -1,538 +0,0 @@ -""" -Test helper functions and base classes. -""" - - -import functools -import json -import os -import sys -from datetime import datetime -from unittest import SkipTest, TestCase - -import requests -from bok_choy.javascript import js_defined -from bok_choy.page_object import XSS_INJECTION -from bok_choy.promise import EmptyPromise, Promise -from bok_choy.web_app_test import WebAppTest -from opaque_keys.edx.locator import CourseLocator -from path import Path as path -from pymongo import MongoClient # lint-amnesty, pylint: disable=unused-import -from selenium.common.exceptions import StaleElementReferenceException -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support.select import Select -from selenium.webdriver.support.ui import WebDriverWait - -from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory -from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from xmodule.partitions.partitions import UserPartition # lint-amnesty, pylint: disable=wrong-import-order - -MAX_EVENTS_IN_FAILURE_OUTPUT = 20 - - -def skip_if_browser(browser): - """ - Method decorator that skips a test if browser is `browser` - - Args: - browser (str): name of internet browser - - Returns: - Decorated function - - """ - def decorator(test_function): - """ - The decorator to be applied to the test function. - """ - @functools.wraps(test_function) - def wrapper(self, *args, **kwargs): - if self.browser.name == browser: - raise SkipTest(f'Skipping as this test will not work with {browser}') - test_function(self, *args, **kwargs) - return wrapper - return decorator - - -def is_youtube_available(): - """ - Check if the required youtube urls are available. - - If a URL in `youtube_api_urls` is not reachable then subsequent URLs will not be checked. - - Returns: - bool: - - """ - # TODO: Design and implement a better solution that is reliable and repeatable, - # reflects how the application works in production, and limits the third-party - # network traffic (e.g. repeatedly retrieving the js from youtube from the browser). - - youtube_api_urls = { - 'main': 'https://www.youtube.com/', - 'player': 'https://www.youtube.com/iframe_api', - # For transcripts, you need to check an actual video, so we will - # just specify our default video and see if that one is available. - 'transcript': 'http://video.google.com/timedtext?lang=en&v=3_yD_cEKoCk', - } - - for url in youtube_api_urls.values(): - try: - response = requests.get(url, allow_redirects=False) - except requests.exceptions.ConnectionError: - return False - - if response.status_code >= 300: - return False - - return True - - -def is_focused_on_element(browser, selector): - """ - Check if the focus is on the element that matches the selector. - """ - return browser.execute_script(f"return $('{selector}').is(':focus')") - - -def load_data_str(rel_path): - """ - Load a file from the "data" directory as a string. - `rel_path` is the path relative to the data directory. - """ - full_path = path(__file__).abspath().dirname() / "data" / rel_path - with open(full_path) as data_file: - return data_file.read() - - -def remove_file(filename): - """ - Remove a file if it exists - """ - if os.path.exists(filename): - os.remove(filename) - - -def disable_animations(page): - """ - Disable jQuery and CSS3 animations. - """ - disable_jquery_animations(page) - disable_css_animations(page) - - -def enable_animations(page): - """ - Enable jQuery and CSS3 animations. - """ - enable_jquery_animations(page) - enable_css_animations(page) - - -@js_defined('window.jQuery') -def disable_jquery_animations(page): - """ - Disable jQuery animations. - """ - page.browser.execute_script("jQuery.fx.off = true;") - - -@js_defined('window.jQuery') -def enable_jquery_animations(page): - """ - Enable jQuery animations. - """ - page.browser.execute_script("jQuery.fx.off = false;") - - -def disable_css_animations(page): - """ - Disable CSS3 animations, transitions, transforms. - """ - page.browser.execute_script(""" - var id = 'no-transitions'; - - // if styles were already added, just do nothing. - if (document.getElementById(id)) { - return; - } - - var css = [ - '* {', - '-webkit-transition: none !important;', - '-moz-transition: none !important;', - '-o-transition: none !important;', - '-ms-transition: none !important;', - 'transition: none !important;', - '-webkit-transition-property: none !important;', - '-moz-transition-property: none !important;', - '-o-transition-property: none !important;', - '-ms-transition-property: none !important;', - 'transition-property: none !important;', - '-webkit-transform: none !important;', - '-moz-transform: none !important;', - '-o-transform: none !important;', - '-ms-transform: none !important;', - 'transform: none !important;', - '-webkit-animation: none !important;', - '-moz-animation: none !important;', - '-o-animation: none !important;', - '-ms-animation: none !important;', - 'animation: none !important;', - '}' - ].join(''), - head = document.head || document.getElementsByTagName('head')[0], - styles = document.createElement('style'); - - styles.id = id; - styles.type = 'text/css'; - if (styles.styleSheet){ - styles.styleSheet.cssText = css; - } else { - styles.appendChild(document.createTextNode(css)); - } - - head.appendChild(styles); - """) - - -def enable_css_animations(page): - """ - Enable CSS3 animations, transitions, transforms. - """ - page.browser.execute_script(""" - var styles = document.getElementById('no-transitions'), - head = document.head || document.getElementsByTagName('head')[0]; - - head.removeChild(styles) - """) - - -def select_option_by_text(select_browser_query, option_text, focus_out=False): - """ - Chooses an option within a select by text (helper method for Select's select_by_visible_text method). - - Wrap this in a Promise to prevent a StaleElementReferenceException - from being raised while the DOM is still being rewritten - """ - def select_option(query, value): - """ Get the first select element that matches the query and select the desired value. """ - try: - select = Select(query.first.results[0]) - select.select_by_visible_text(value) - if focus_out: - query.first.results[0].send_keys(Keys.TAB) - return True - except StaleElementReferenceException: - return False - - msg = f'Selected option {option_text}' - EmptyPromise(lambda: select_option(select_browser_query, option_text), msg).fulfill() - - -def get_selected_option_text(select_browser_query): - """ - Returns the text value for the first selected option within a select. - - Wrap this in a Promise to prevent a StaleElementReferenceException - from being raised while the DOM is still being rewritten - """ - def get_option(query): - """ Get the first select element that matches the query and return its value. """ - try: - select = Select(query.first.results[0]) - return (True, select.first_selected_option.text) - except StaleElementReferenceException: - return (False, None) - - text = Promise(lambda: get_option(select_browser_query), 'Retrieved selected option text').fulfill() - return text - - -def get_options(select_browser_query): - """ - Returns all the options for the given select. - """ - return Select(select_browser_query.first.results[0]).options - - -def generate_course_key(org, number, run): - """ - Makes a CourseLocator from org, number and run - """ - default_store = os.environ.get('DEFAULT_STORE', 'draft') - return CourseLocator(org, number, run, deprecated=(default_store == 'draft')) - - -def select_option_by_value(browser_query, value, focus_out=False): - """ - Selects a html select element by matching value attribute - """ - select = Select(browser_query.first.results[0]) - select.select_by_value(value) - - def options_selected(): - """ - Returns True if all options in select element where value attribute - matches `value`. if any option is not selected then returns False - and select it. if value is not an option choice then it returns False. - """ - all_options_selected = True - has_option = False - for opt in select.options: - if opt.get_attribute('value') == value: - has_option = True - if not opt.is_selected(): - all_options_selected = False - opt.click() - if all_options_selected and not has_option: - all_options_selected = False - if focus_out: - browser_query.first.results[0].send_keys(Keys.TAB) - return all_options_selected - - # Make sure specified option is actually selected - EmptyPromise(options_selected, "Option is selected").fulfill() - - -def create_multiple_choice_xml(correct_choice=2, num_choices=4): - """ - Return the Multiple Choice Problem XML, given the name of the problem. - """ - # all choices are incorrect except for correct_choice - choices = [False for _ in range(num_choices)] - choices[correct_choice] = True - - choice_names = [f'choice_{index}' for index in range(num_choices)] - question_text = f'The correct answer is Choice {correct_choice}' - - return MultipleChoiceResponseXMLFactory().build_xml( - question_text=question_text, - choices=choices, - choice_names=choice_names, - ) - - -def create_multiple_choice_problem(problem_name): - """ - Return the Multiple Choice Problem Descriptor, given the name of the problem. - """ - xml_data = create_multiple_choice_xml() - return XBlockFixtureDesc( - 'problem', - problem_name, - data=xml_data, - metadata={'rerandomize': 'always'} - ) - - -def auto_auth(browser, username, email, staff, course_id, **kwargs): - """ - Logout and login with given credentials. - """ - AutoAuthPage(browser, username=username, email=email, course_id=course_id, staff=staff, **kwargs).visit() - - -class EventsTestMixin(TestCase): - """ - Helpers and setup for running tests that evaluate events emitted - """ - def setUp(self): - super().setUp() - mongo_host = 'edx.devstack.mongo' if 'BOK_CHOY_HOSTNAME' in os.environ else 'localhost' - self.event_collection = MongoClient(mongo_host)["test"]["events"] - self.start_time = datetime.now() - - -class AcceptanceTest(WebAppTest): - """ - The base class of all acceptance tests. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Use long messages so that failures show actual and expected values - self.longMessage = True # pylint: disable=invalid-name - - def tearDown(self): - self._save_console_log() - super().tearDown() - - def _save_console_log(self): - """ - Retrieve any JS errors caught by our error handler in the browser - and save them to a log file. This is a workaround for Firefox not - supporting the Selenium log capture API yet; for details, see - https://github.com/mozilla/geckodriver/issues/284 - """ - browser_name = os.environ.get('SELENIUM_BROWSER', 'firefox') - if browser_name != 'firefox': - return - result = sys.exc_info() - exception_type = result[0] - - # Do not save for skipped tests. - if exception_type is SkipTest: - return - - # If the test failed, save the browser console log. - # The exception info will either be an assertion error (on failure) - # or an actual exception (on error) - if result != (None, None, None): - logs = self.browser.execute_script("return window.localStorage.getItem('console_log_capture');") - if not logs: - return - logs = json.loads(logs) - - log_dir = os.environ.get('SELENIUM_DRIVER_LOG_DIR') - if log_dir and not os.path.exists(log_dir): - os.makedirs(log_dir) - - log_path = os.path.join(log_dir, f'{self.id()}_browser.log') - with open(log_path, 'w') as browser_log: - for (message, url, line_no, col_no, stack) in logs: - browser_log.write("{}:{}:{}: {}\n {}\n".format( - url, - line_no, - col_no, - message, - (stack or "").replace('\n', '\n ') - )) - - -class UniqueCourseTest(AcceptanceTest): - """ - Test that provides a unique course ID. - """ - - def setUp(self): - super().setUp() - - self.course_info = { - 'org': 'test_org', - 'number': self.unique_id, - 'run': 'test_run', - 'display_name': 'Test Course' + XSS_INJECTION + self.unique_id - } - - @property - def course_id(self): - """ - Returns the serialized course_key for the test - """ - # TODO - is there a better way to make this agnostic to the underlying default module store? - default_store = os.environ.get('DEFAULT_STORE', 'draft') - course_key = CourseLocator( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - deprecated=(default_store == 'draft') - ) - return str(course_key) - - -class YouTubeConfigError(Exception): - """ - Error occurred while configuring YouTube Stub Server. - """ - pass # lint-amnesty, pylint: disable=unnecessary-pass - - -class YouTubeStubConfig: - """ - Configure YouTube Stub Server. - """ - - YOUTUBE_HOSTNAME = os.environ.get('BOK_CHOY_HOSTNAME', '127.0.0.1') - PORT = 9080 - URL = f'http://{YOUTUBE_HOSTNAME}:{PORT}/' - - @classmethod - def configure(cls, config): - """ - Allow callers to configure the stub server using the /set_config URL. - - Arguments: - config (dict): Configuration dictionary. - - Raises: - YouTubeConfigError - - """ - youtube_stub_config_url = cls.URL + 'set_config' - - config_data = {param: json.dumps(value) for param, value in config.items()} - response = requests.put(youtube_stub_config_url, data=config_data) - - if not response.ok: - raise YouTubeConfigError( - 'YouTube Server Configuration Failed. URL {}, Configuration Data: {}, Status was {}'.format( - youtube_stub_config_url, config, response.status_code)) - - @classmethod - def reset(cls): - """ - Reset YouTube Stub Server Configurations using the /del_config URL. - - Raises: - YouTubeConfigError - - """ - youtube_stub_config_url = cls.URL + 'del_config' - - response = requests.delete(youtube_stub_config_url) - - if not response.ok: - raise YouTubeConfigError( - 'YouTube Server Configuration Failed. URL: {} Status was {}'.format( - youtube_stub_config_url, response.status_code)) - - @classmethod - def get_configuration(cls): - """ - Allow callers to get current stub server configuration. - - Returns: - dict - - """ - youtube_stub_config_url = cls.URL + 'get_config' - - response = requests.get(youtube_stub_config_url) - - if response.ok: - return json.loads(response.content.decode('utf-8')) - else: - return {} - - -def click_and_wait_for_window(page, element): - """ - To avoid a race condition, click an element that launces a new window, and - wait for that window to launch. - To check this, make sure the number of window_handles increases by one. - - Arguments: - page (PageObject): Page object to perform method on - element (WebElement): Clickable element that triggers the new window to open - """ - num_windows = len(page.browser.window_handles) - element.click() - WebDriverWait(page.browser, 10).until( - lambda driver: len(driver.window_handles) > num_windows - ) - - -def create_user_partition_json(partition_id, name, description, groups, scheme="random"): - """ - Helper method to create user partition JSON. If scheme is not supplied, "random" is used. - """ - # All that is persisted about a scheme is its name. - class MockScheme: - name = scheme - - return UserPartition( - partition_id, name, description, groups, MockScheme() - ).to_json() diff --git a/common/test/acceptance/tests/lms/__init__.py b/common/test/acceptance/tests/lms/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/test/acceptance/tests/lms/test_account_settings.py b/common/test/acceptance/tests/lms/test_account_settings.py deleted file mode 100644 index 39fbc96ac7..0000000000 --- a/common/test/acceptance/tests/lms/test_account_settings.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -End-to-end tests for the Account Settings page. -""" - - -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.account_settings import AccountSettingsPage -from common.test.acceptance.tests.helpers import AcceptanceTest, EventsTestMixin - - -class AccountSettingsTestMixin(EventsTestMixin, AcceptanceTest): - """ - Mixin with helper methods to test the account settings page. - """ - - CHANGE_INITIATED_EVENT_NAME = "edx.user.settings.change_initiated" - USER_SETTINGS_CHANGED_EVENT_NAME = 'edx.user.settings.changed' - ACCOUNT_SETTINGS_REFERER = "/account/settings" - - shard = 23 - - def visit_account_settings_page(self, gdpr=False): - """ - Visit the account settings page for the current user, and store the page instance - as self.account_settings_page. - """ - self.account_settings_page = AccountSettingsPage(self.browser) - self.account_settings_page.visit() - self.account_settings_page.wait_for_ajax() - # TODO: LEARNER-4422 - delete when we clean up flags - if gdpr: - self.account_settings_page.browser.get(self.browser.current_url + "?course_experience.gdpr=1") - self.account_settings_page.wait_for_page() - - def log_in_as_unique_user(self, email=None, full_name=None, password=None): - """ - Create a unique user and return the account's username and id. - """ - username = f"test_{self.unique_id[0:6]}" - auto_auth_page = AutoAuthPage( - self.browser, - username=username, - email=email, - full_name=full_name, - password=password - ).visit() - user_id = auto_auth_page.get_user_id() - return username, user_id - - -class AccountSettingsA11yTest(AccountSettingsTestMixin, AcceptanceTest): - """ - Class to test account settings accessibility. - """ - a11y = True - - def test_account_settings_a11y(self): - """ - Test the accessibility of the account settings page. - """ - self.log_in_as_unique_user() - self.visit_account_settings_page() - self.account_settings_page.a11y_audit.config.set_rules({ - "ignore": [ - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - self.account_settings_page.a11y_audit.check_for_accessibility_errors() diff --git a/common/test/acceptance/tests/lms/test_learner_profile.py b/common/test/acceptance/tests/lms/test_learner_profile.py deleted file mode 100644 index b72047b4ac..0000000000 --- a/common/test/acceptance/tests/lms/test_learner_profile.py +++ /dev/null @@ -1,172 +0,0 @@ -""" -End-to-end tests for Student's Profile Page. -""" - - -from datetime import datetime - -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.common.logout import LogoutPage -from common.test.acceptance.pages.lms.learner_profile import LearnerProfilePage -from common.test.acceptance.tests.helpers import AcceptanceTest, EventsTestMixin - - -class LearnerProfileTestMixin(EventsTestMixin): - """ - Mixin with helper methods for testing learner profile pages. - """ - - PRIVACY_PUBLIC = 'all_users' - PRIVACY_PRIVATE = 'private' - - PUBLIC_PROFILE_FIELDS = ['username', 'country', 'language_proficiencies', 'bio'] - PRIVATE_PROFILE_FIELDS = ['username'] - - PUBLIC_PROFILE_EDITABLE_FIELDS = ['country', 'language_proficiencies', 'bio'] - - USER_SETTINGS_CHANGED_EVENT_NAME = "edx.user.settings.changed" - - def log_in_as_unique_user(self): - """ - Create a unique user and return the account's username and id. - """ - username = f"test_{self.unique_id[0:6]}" - auto_auth_page = AutoAuthPage(self.browser, username=username).visit() - user_id = auto_auth_page.get_user_id() - return username, user_id - - def set_public_profile_fields_data(self, profile_page): - """ - Fill in the public profile fields of a user. - """ - # These value_for_dropdown_field method calls used to include - # focus_out = True, but a change in selenium is focusing out of the - # drop down after selection without any more action needed. - profile_page.value_for_dropdown_field('language_proficiencies', 'English') - profile_page.value_for_dropdown_field('country', 'United Arab Emirates') - profile_page.set_value_for_textarea_field('bio', 'Nothing Special') - # Waits here for text to appear/save on bio field - profile_page.wait_for_ajax() - - def visit_profile_page(self, username, privacy=None): - """ - Visit a user's profile page and if a privacy is specified and - is different from the displayed value, then set the privacy to that value. - """ - profile_page = LearnerProfilePage(self.browser, username) - - # Change the privacy if requested by loading the page and - # changing the drop down - if privacy is not None: - profile_page.visit() - - # Change the privacy setting if it is not the desired one already - profile_page.privacy = privacy - - # Verify the current setting is as expected - if privacy == self.PRIVACY_PUBLIC: - assert profile_page.privacy == 'all_users' - else: - assert profile_page.privacy == 'private' - - if privacy == self.PRIVACY_PUBLIC: - self.set_public_profile_fields_data(profile_page) - - # Reset event tracking so that the tests only see events from - # loading the profile page. - self.start_time = datetime.now() - - # Load the page - profile_page.visit() - - return profile_page - - def initialize_different_user(self, privacy=None, birth_year=None): - """ - Initialize the profile page for a different test user - """ - username, user_id = self.log_in_as_unique_user() - - # Set the privacy for the new user - if privacy is None: - privacy = self.PRIVACY_PUBLIC - self.visit_profile_page(username, privacy=privacy) - - # Set the user's year of birth - if birth_year: - self.set_birth_year(birth_year) - - # Log the user out - LogoutPage(self.browser).visit() - - return username, user_id - - -class LearnerProfileA11yTest(LearnerProfileTestMixin, AcceptanceTest): - """ - Class to test learner profile accessibility. - """ - a11y = True - - def test_editable_learner_profile_a11y(self): - """ - Test the accessibility of the editable version of the profile page - (user viewing her own public profile). - """ - username, _ = self.log_in_as_unique_user() - - profile_page = self.visit_profile_page(username) - profile_page.a11y_audit.config.set_rules({ - "ignore": [ - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - profile_page.a11y_audit.check_for_accessibility_errors() - - profile_page.make_field_editable('language_proficiencies') - profile_page.a11y_audit.check_for_accessibility_errors() - - profile_page.make_field_editable('bio') - profile_page.a11y_audit.check_for_accessibility_errors() - - def test_read_only_learner_profile_a11y(self): - """ - Test the accessibility of the read-only version of a public profile page - (user viewing someone else's profile page). - """ - # initialize_different_user should cause country, language, and bio to be filled out (since - # privacy is public). It doesn't appear that this is happening, although the method - # works in regular bokchoy tests. Perhaps a problem with phantomjs? So this test is currently - # only looking at a read-only profile page with a username. - different_username, _ = self.initialize_different_user(privacy=self.PRIVACY_PUBLIC) - self.log_in_as_unique_user() - - profile_page = self.visit_profile_page(different_username) - profile_page.a11y_audit.config.set_rules({ - "ignore": [ - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - profile_page.a11y_audit.check_for_accessibility_errors() - - def test_badges_accessibility(self): - """ - Test the accessibility of the badge listings and sharing modal. - """ - username = 'testcert' - - AutoAuthPage(self.browser, username=username).visit() - profile_page = self.visit_profile_page(username) - profile_page.a11y_audit.config.set_rules({ - "ignore": [ - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - 'color-contrast' # AC-938 - ] - }) - profile_page.display_accomplishments() - profile_page.a11y_audit.check_for_accessibility_errors() - profile_page.badges[0].display_modal() - profile_page.a11y_audit.check_for_accessibility_errors() diff --git a/common/test/acceptance/tests/lms/test_lms.py b/common/test/acceptance/tests/lms/test_lms.py deleted file mode 100644 index 438e973ea2..0000000000 --- a/common/test/acceptance/tests/lms/test_lms.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -End-to-end tests for the LMS. -""" - -import pytest - -from common.test.acceptance.fixtures.course import CourseFixture -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.course_home import CourseHomePage -from common.test.acceptance.pages.lms.course_wiki import ( - CourseWikiChildrenPage, - CourseWikiEditPage, - CourseWikiHistoryPage, - CourseWikiPage -) -from common.test.acceptance.pages.lms.tab_nav import TabNavPage -from common.test.acceptance.tests.helpers import ( - UniqueCourseTest, -) -from openedx.core.lib.tests import attr - - -@attr('a11y') -class CourseWikiA11yTest(UniqueCourseTest): - """ - Tests that verify the course wiki. - """ - - def setUp(self): - """ - Initialize pages and install a course fixture. - """ - super().setUp() - - # self.course_info['number'] must be shorter since we are accessing the wiki. See TNL-1751 - self.course_info['number'] = self.unique_id[0:6] - - self.course_wiki_page = CourseWikiPage(self.browser, self.course_id) - self.course_home_page = CourseHomePage(self.browser, self.course_id) - self.course_wiki_edit_page = CourseWikiEditPage(self.browser, self.course_id, self.course_info) - self.tab_nav = TabNavPage(self.browser) - - CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], self.course_info['display_name'] - ).install() - - # Auto-auth register for the course - AutoAuthPage(self.browser, course_id=self.course_id).visit() - - # Access course wiki page - self.course_home_page.visit() - self.tab_nav.go_to_tab('Wiki') - - def _open_editor(self): - self.course_wiki_page.open_editor() - self.course_wiki_edit_page.wait_for_page() - - @pytest.mark.skip(reason='This test fails when using the new coursehome MFE.') - def test_view(self): - """ - Verify the basic accessibility of the wiki page as initially displayed. - """ - self.course_wiki_page.a11y_audit.config.set_rules({ - "ignore": [ - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - self.course_wiki_page.a11y_audit.check_for_accessibility_errors() - - @pytest.mark.skip(reason='This test fails when using the new coursehome MFE.') - def test_edit(self): - """ - Verify the basic accessibility of edit wiki page. - """ - self._open_editor() - self.course_wiki_edit_page.a11y_audit.config.set_rules({ - "ignore": [ - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - self.course_wiki_edit_page.a11y_audit.check_for_accessibility_errors() - - @pytest.mark.skip(reason='This test fails when using the new coursehome MFE.') - def test_changes(self): - """ - Verify the basic accessibility of changes wiki page. - """ - self.course_wiki_page.show_history() - history_page = CourseWikiHistoryPage(self.browser, self.course_id, self.course_info) - history_page.a11y_audit.config.set_rules({ - "ignore": [ - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - history_page.wait_for_page() - history_page.a11y_audit.check_for_accessibility_errors() - - @pytest.mark.skip(reason='This test fails when using the new coursehome MFE.') - def test_children(self): - """ - Verify the basic accessibility of changes wiki page. - """ - self.course_wiki_page.show_children() - children_page = CourseWikiChildrenPage(self.browser, self.course_id, self.course_info) - children_page.a11y_audit.config.set_rules({ - "ignore": [ - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - children_page.wait_for_page() - children_page.a11y_audit.check_for_accessibility_errors() diff --git a/common/test/acceptance/tests/lms/test_lms_course_home.py b/common/test/acceptance/tests/lms/test_lms_course_home.py deleted file mode 100644 index a340c840a5..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_course_home.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -End-to-end tests for the LMS that utilize the course home page and course outline. -""" - -import pytest - -from openedx.core.lib.tests import attr - -from ...fixtures.course import CourseFixture, XBlockFixtureDesc -from ...pages.lms.course_home import CourseHomePage -from ...pages.lms.courseware import CoursewarePage -from ..helpers import UniqueCourseTest, auto_auth, load_data_str - - -class CourseHomeBaseTest(UniqueCourseTest): - """ - Provides base setup for course home tests. - """ - USERNAME = "STUDENT_TESTER" - EMAIL = "student101@example.com" - - def setUp(self): - """ - Initialize pages and install a course fixture. - """ - super().setUp() - - self.course_home_page = CourseHomePage(self.browser, self.course_id) - self.courseware_page = CoursewarePage(self.browser, self.course_id) - - # Install a course with sections and problems - course_fix = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ) - - course_fix.add_children( - XBlockFixtureDesc('static_tab', 'Test Static Tab', data=r"static tab data with mathjax \(E=mc^2\)"), - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('problem', 'Test Problem 1', data=load_data_str('multiple_choice.xml')), - XBlockFixtureDesc('problem', 'Test Problem 2', data=load_data_str('formula_problem.xml')), - XBlockFixtureDesc('html', 'Test HTML'), - ) - ), - XBlockFixtureDesc('chapter', 'Test Section 2').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection 2'), - XBlockFixtureDesc('sequential', 'Test Subsection 3').add_children( - XBlockFixtureDesc('problem', 'Test Problem A', data=load_data_str('multiple_choice.xml')) - ), - ) - ).install() - - # Auto-auth register for the course. - auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id) - - -@attr('a11y') -class CourseHomeA11yTest(CourseHomeBaseTest): - """ - Tests the accessibility of the course home page - """ - - @pytest.mark.skip(reason='This test fails when using the new coursehome MFE.') - def test_course_home_a11y(self): - """ - Test the accessibility of the course home page with course outline. - """ - course_home_page = CourseHomePage(self.browser, self.course_id) - course_home_page.visit() - course_home_page.a11y_audit.config.set_rules({ - "ignore": [ - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - 'landmark-no-duplicate-banner', # TODO: AC-934 - ] - }) - course_home_page.a11y_audit.check_for_accessibility_errors() diff --git a/common/test/acceptance/tests/lms/test_lms_dashboard.py b/common/test/acceptance/tests/lms/test_lms_dashboard.py deleted file mode 100644 index 53d1eafc5c..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_dashboard.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -End-to-end tests for the main LMS Dashboard (aka, Student Dashboard). -""" -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.dashboard import DashboardPage -from common.test.acceptance.tests.helpers import UniqueCourseTest, generate_course_key - -DEFAULT_SHORT_DATE_FORMAT = '{dt:%b} {dt.day}, {dt.year}' -TEST_DATE_FORMAT = '{dt:%b} {dt.day}, {dt.year} {dt.hour:02}:{dt.minute:02}' - - -class BaseLmsDashboardTestMultiple(UniqueCourseTest): - """ Base test suite for the LMS Student Dashboard with Multiple Courses""" - - def setUp(self): - """ - Initializes the components (page objects, courses, users) for this test suite - """ - # Some parameters are provided by the parent setUp() routine, such as the following: - # self.course_id, self.course_info, self.unique_id - super().setUp() - - # Load page objects for use by the tests - self.dashboard_page = DashboardPage(self.browser) - - # Configure some aspects of the test course and install the settings into the course - self.courses = { - 'A': { - 'org': 'test_org', - 'number': self.unique_id, - 'run': 'test_run_A', - 'display_name': 'Test Course A', - 'enrollment_mode': 'audit', - 'cert_name_long': 'Certificate of Audit Achievement' - }, - 'B': { - 'org': 'test_org', - 'number': self.unique_id, - 'run': 'test_run_B', - 'display_name': 'Test Course B', - 'enrollment_mode': 'verified', - 'cert_name_long': 'Certificate of Verified Achievement' - }, - 'C': { - 'org': 'test_org', - 'number': self.unique_id, - 'run': 'test_run_C', - 'display_name': 'Test Course C', - 'enrollment_mode': 'credit', - 'cert_name_long': 'Certificate of Credit Achievement' - } - } - - self.username = f"test_{self.unique_id[0:6]}" - self.email = f"{self.username}@example.com" - - self.course_keys = {} - self.course_fixtures = {} - - for key, value in self.courses.items(): - course_key = generate_course_key( - value['org'], - value['number'], - value['run'], - ) - - course_fixture = CourseFixture( - value['org'], - value['number'], - value['run'], - value['display_name'], - ) - - course_fixture.add_advanced_settings({ - "social_sharing_url": {"value": "http://custom/course/url"}, - "cert_name_long": {"value": value['cert_name_long']} - }) - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section 1').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection 1,1').add_children( - XBlockFixtureDesc('problem', 'Test Problem 1', data='problem 1 dummy body'), - XBlockFixtureDesc('html', 'html 1', data="html 1 dummy body"), - XBlockFixtureDesc('problem', 'Test Problem 2', data="problem 2 dummy body"), - XBlockFixtureDesc('html', 'html 2', data="html 2 dummy body"), - ), - XBlockFixtureDesc('sequential', 'Test Subsection 1,2').add_children( - XBlockFixtureDesc('problem', 'Test Problem 3', data='problem 3 dummy body'), - ), - XBlockFixtureDesc( - 'sequential', 'Test HIDDEN Subsection', metadata={'visible_to_staff_only': True} - ).add_children( - XBlockFixtureDesc('problem', 'Test HIDDEN Problem', data='hidden problem'), - ), - ) - ).install() - - self.course_keys[key] = course_key - self.course_fixtures[key] = course_fixture - - # Create the test user, register them for the course, and authenticate - AutoAuthPage( - self.browser, - username=self.username, - email=self.email, - course_id=course_key, - enrollment_mode=value['enrollment_mode'] - ).visit() - - # Navigate the authenticated, enrolled user to the dashboard page and get testing! - self.dashboard_page.visit() - - -class LmsDashboardA11yTest(BaseLmsDashboardTestMultiple): - """ - Class to test lms student dashboard accessibility. - """ - a11y = True - - def test_dashboard_course_listings_a11y(self): - """ - Test the accessibility of the course listings - """ - self.dashboard_page.a11y_audit.config.set_rules({ - "ignore": [ - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'button-name', # TODO: AC-935 - 'landmark-no-duplicate-banner', # TODO: AC-934 - 'landmark-complementary-is-top-level', # TODO: AC-939 - 'region' # TODO: AC-932 - ] - }) - course_listings = self.dashboard_page.get_courses() - assert len(course_listings) == 3 - self.dashboard_page.a11y_audit.check_for_accessibility_errors() diff --git a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py deleted file mode 100644 index 878fe0b456..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py +++ /dev/null @@ -1,244 +0,0 @@ -""" -End-to-end tests for the LMS Instructor Dashboard. -""" - - -import ddt - -from common.test.acceptance.fixtures.certificates import CertificateConfigFixture -from common.test.acceptance.fixtures.course import CourseFixture -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.dashboard import DashboardPage -from common.test.acceptance.pages.lms.instructor_dashboard import ( - InstructorDashboardPage, -) -from common.test.acceptance.tests.helpers import ( - EventsTestMixin, - UniqueCourseTest, - disable_animations, -) -from openedx.core.lib.tests import attr - - -class BaseInstructorDashboardTest(EventsTestMixin, UniqueCourseTest): - """ - Mixin class for testing the instructor dashboard. - """ - def log_in_as_instructor(self, global_staff=True, course_access_roles=None): - """ - Login with an instructor account. - - Args: - course_access_roles (str[]): List of course access roles that should be assigned to the user. - - Returns - username (str) - user_id (int) - """ - course_access_roles = course_access_roles or [] - auto_auth_page = AutoAuthPage( - self.browser, course_id=self.course_id, staff=global_staff, course_access_roles=course_access_roles - ) - auto_auth_page.visit() - user_info = auto_auth_page.user_info - return user_info['username'], user_info['user_id'], user_info['email'], user_info['password'] - - def visit_instructor_dashboard(self): - """ - Visits the instructor dashboard. - """ - instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) - instructor_dashboard_page.visit() - return instructor_dashboard_page - - -@attr('a11y') -class LMSInstructorDashboardA11yTest(BaseInstructorDashboardTest): - """ - Instructor dashboard base accessibility test. - """ - def setUp(self): - super().setUp() - self.course_fixture = CourseFixture(**self.course_info).install() - self.log_in_as_instructor() - self.instructor_dashboard_page = self.visit_instructor_dashboard() - - def test_instructor_dashboard_a11y(self): - self.instructor_dashboard_page.a11y_audit.config.set_rules({ - "ignore": [ - 'aria-valid-attr', # TODO: LEARNER-6611 & LEARNER-6865 - 'region', # TODO: AC-932 - ] - }) - self.instructor_dashboard_page.a11y_audit.check_for_accessibility_errors() - - -@ddt.ddt -class BulkEmailTest(BaseInstructorDashboardTest): - """ - End-to-end tests for bulk emailing from instructor dash. - """ - shard = 23 - - def setUp(self): - super().setUp() - self.course_fixture = CourseFixture(**self.course_info).install() - self.log_in_as_instructor() - instructor_dashboard_page = self.visit_instructor_dashboard() - self.send_email_page = instructor_dashboard_page.select_bulk_email() - - @attr('a11y') - def test_bulk_email_a11y(self): - """ - Bulk email accessibility tests - """ - self.send_email_page.a11y_audit.config.set_scope([ - '#section-send-email' - ]) - self.send_email_page.a11y_audit.config.set_rules({ - "ignore": [ - 'button-name', # TODO: TNL-5830 - 'aria-allowed-role', # TODO: AC-936 - 'color-contrast', # TODO: AC-938 - 'listitem' # TODO: AC-937 - ] - }) - self.send_email_page.a11y_audit.check_for_accessibility_errors() - - -@attr(shard=20) -class AutoEnrollmentWithCSVTest(BaseInstructorDashboardTest): - """ - End-to-end tests for Auto-Registration and enrollment functionality via CSV file. - """ - - def setUp(self): - super().setUp() - self.course_fixture = CourseFixture(**self.course_info).install() - self.log_in_as_instructor() - instructor_dashboard_page = self.visit_instructor_dashboard() - self.auto_enroll_section = instructor_dashboard_page.select_membership().select_auto_enroll_section() - # Initialize the page objects - self.dashboard_page = DashboardPage(self.browser) - - @attr('a11y') - def test_auto_enroll_csv_a11y(self): - """ - Auto-enrollment with CSV accessibility tests - """ - self.auto_enroll_section.a11y_audit.config.set_scope([ - '#membership-list-widget-tpl' - ]) - self.auto_enroll_section.a11y_audit.check_for_accessibility_errors() - - -@attr(shard=10) -@ddt.ddt -class CertificatesTest(BaseInstructorDashboardTest): - """ - Tests for Certificates functionality on instructor dashboard. - """ - - def setUp(self): - super().setUp() - self.test_certificate_config = { - 'id': 1, - 'name': 'Certificate name', - 'description': 'Certificate description', - 'course_title': 'Course title override', - 'signatories': [], - 'version': 1, - 'is_active': True - } - CourseFixture(**self.course_info).install() - self.cert_fixture = CertificateConfigFixture(self.course_id, self.test_certificate_config) - self.cert_fixture.install() - self.user_name, self.user_id, __, __ = self.log_in_as_instructor() - self.instructor_dashboard_page = self.visit_instructor_dashboard() - self.certificates_section = self.instructor_dashboard_page.select_certificates() - disable_animations(self.certificates_section) - - @attr('a11y') - def test_certificates_a11y(self): - """ - Certificates page accessibility tests - """ - self.certificates_section.a11y_audit.config.set_rules({ - "ignore": [ - 'aria-hidden-focus' # TODO: AC-938 - ] - }) - self.certificates_section.a11y_audit.config.set_scope([ - '.certificates-wrapper' - ]) - self.certificates_section.a11y_audit.check_for_accessibility_errors() - - -@attr(shard=20) -class CertificateInvalidationTest(BaseInstructorDashboardTest): - """ - Tests for Certificates functionality on instructor dashboard. - """ - - @classmethod - def setUpClass(cls): - super().setUpClass() - - # Create course fixture once each test run - CourseFixture( - org='test_org', - number='335535897951379478207964576572017930000', - run='test_run', - display_name='Test Course 335535897951379478207964576572017930000', - ).install() - - def setUp(self): - super().setUp() - # set same course number as we have in fixture json - self.course_info['number'] = "335535897951379478207964576572017930000" - - # we have created a user with this id in fixture, and created a generated certificate for it. - self.student_id = "99" - self.student_name = "testcert" - self.student_email = "cert@example.com" - - # Enroll above test user in the course - AutoAuthPage( - self.browser, - username=self.student_name, - email=self.student_email, - course_id=self.course_id, - ).visit() - - self.test_certificate_config = { - 'id': 1, - 'name': 'Certificate name', - 'description': 'Certificate description', - 'course_title': 'Course title override', - 'signatories': [], - 'version': 1, - 'is_active': True - } - - self.cert_fixture = CertificateConfigFixture(self.course_id, self.test_certificate_config) - self.cert_fixture.install() - self.user_name, self.user_id, __, __ = self.log_in_as_instructor() - self.instructor_dashboard_page = self.visit_instructor_dashboard() - self.certificates_section = self.instructor_dashboard_page.select_certificates() - - disable_animations(self.certificates_section) - - @attr('a11y') - def test_invalidate_certificates_a11y(self): - """ - Certificate invalidation accessibility tests - """ - self.certificates_section.a11y_audit.config.set_rules({ - "ignore": [ - 'aria-hidden-focus' # TODO: AC-938 - ] - }) - self.certificates_section.a11y_audit.config.set_scope([ - '.certificates-wrapper' - ]) - self.certificates_section.a11y_audit.check_for_accessibility_errors() diff --git a/common/test/acceptance/tests/lms/test_lms_problems.py b/common/test/acceptance/tests/lms/test_lms_problems.py deleted file mode 100644 index 342db5946c..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_problems.py +++ /dev/null @@ -1,205 +0,0 @@ -""" -Bok choy acceptance tests for problems in the LMS -""" - - -from textwrap import dedent - -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.courseware import CoursewarePage -from common.test.acceptance.pages.lms.problem import ProblemPage -from common.test.acceptance.tests.helpers import UniqueCourseTest -from openedx.core.lib.tests import attr - - -class ProblemsTest(UniqueCourseTest): - """ - Base class for tests of problems in the LMS. - """ - - def setUp(self): - super().setUp() - - self.username = f"test_student_{self.unique_id[0:8]}" - self.email = f"{self.username}@example.com" - self.password = "keep it secret; keep it safe." - - self.xqueue_grade_response = None - - self.courseware_page = CoursewarePage(self.browser, self.course_id) - - # Install a course with a hierarchy and problems - course_fixture = CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], self.course_info['display_name'] - ) - - problem = self.get_problem() - sequential = self.get_sequential() - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - sequential.add_children(problem) - ) - ).install() - - # Auto-auth register for the course. - AutoAuthPage( - self.browser, - username=self.username, - email=self.email, - password=self.password, - course_id=self.course_id, - staff=True - ).visit() - - def get_problem(self): - """ Subclasses should override this to complete the fixture """ - raise NotImplementedError() - - def get_sequential(self): - """ Subclasses can override this to add a sequential with metadata """ - return XBlockFixtureDesc('sequential', 'Test Subsection') - - -class CAPAProblemA11yBaseTestMixin: - """Base TestCase Class to verify CAPA problem accessibility.""" - - def test_a11y(self): - """ - Verifies that there are no accessibility issues for a particular problem type - """ - self.courseware_page.visit() - problem_page = ProblemPage(self.browser) - - # Set the scope to the problem question - problem_page.a11y_audit.config.set_scope( - include=['.wrapper-problem-response'] - ) - - # Run the accessibility audit. - problem_page.a11y_audit.check_for_accessibility_errors() - - -@attr('a11y') -class CAPAProblemChoiceA11yTest(CAPAProblemA11yBaseTestMixin, ProblemsTest): - """TestCase Class to verify accessibility for checkboxes and multiplechoice CAPA problems.""" - - def get_problem(self): - """ - Problem structure. - """ - xml = dedent(""" - - - - description 2 text 1 - description 2 text 2 - - True - False - - - - - description 2 text 1 - description 2 text 2 - - Alpha A hint - Beta - - - - """) - return XBlockFixtureDesc('problem', 'Problem A11Y TEST', data=xml) - - -@attr('a11y') -class ProblemTextInputA11yTest(CAPAProblemA11yBaseTestMixin, ProblemsTest): - """TestCase Class to verify TextInput problem accessibility.""" - - def get_problem(self): - """ - TextInput problem XML. - """ - xml = dedent(""" - - - - Appear weak when you are strong, and strong when you are weak. - In the midst of chaos, there is also opportunity. - - - - - The supreme art of war is to subdue the enemy without fighting. - Great results, can be achieved with small forces. - - - """) - return XBlockFixtureDesc('problem', 'TEXTINPUT PROBLEM', data=xml) - - -@attr('a11y') -class CAPAProblemDropDownA11yTest(CAPAProblemA11yBaseTestMixin, ProblemsTest): - """TestCase Class to verify accessibility for dropdowns(optioninput) CAPA problems.""" - - def get_problem(self): - """ - Problem structure. - """ - xml = dedent(""" - - -

    You can use this template as a guide to the simple editor markdown and OLX markup to use for - dropdown problems. Edit this component to replace this template with your own assessment.

    - - Choose wisely - - - - - -
    -
    - """) - return XBlockFixtureDesc('problem', 'Problem A11Y TEST', data=xml) - - -@attr('a11y') -class ProblemNumericalInputA11yTest(CAPAProblemA11yBaseTestMixin, ProblemsTest): - """Tests NumericalInput accessibility.""" - - def get_problem(self): - """NumericalInput problem XML.""" - xml = dedent(""" - - - - Use scientific notation to answer. - - - """) - return XBlockFixtureDesc('problem', 'NUMERICALINPUT PROBLEM', data=xml) - - -@attr('a11y') -class ProblemMathExpressionInputA11yTest(CAPAProblemA11yBaseTestMixin, ProblemsTest): - """Tests MathExpressionInput accessibility.""" - - def get_problem(self): - """MathExpressionInput problem XML.""" - xml = dedent(r""" - - - - - - Enter the equation - - - - """) - return XBlockFixtureDesc('problem', 'MATHEXPRESSIONINPUT PROBLEM', data=xml) diff --git a/common/test/acceptance/tests/lms/test_lms_user_preview.py b/common/test/acceptance/tests/lms/test_lms_user_preview.py deleted file mode 100644 index eafe12a269..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_user_preview.py +++ /dev/null @@ -1,153 +0,0 @@ -""" -Tests the "preview" selector in the LMS that allows changing between Staff, Learner, and Content Groups. -""" - - -from textwrap import dedent - -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.courseware import CoursewarePage -from common.test.acceptance.pages.lms.staff_view import StaffCoursewarePage -from common.test.acceptance.tests.helpers import UniqueCourseTest, create_user_partition_json -from openedx.core.lib.tests import attr -from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID, MINIMUM_STATIC_PARTITION_ID, Group # lint-amnesty, pylint: disable=wrong-import-order - - -@attr(shard=20) -class StaffViewTest(UniqueCourseTest): - """ - Tests that verify the staff view. - """ - USERNAME = "STAFF_TESTER" - EMAIL = "johndoe@example.com" - - def setUp(self): - super().setUp() - - self.courseware_page = CoursewarePage(self.browser, self.course_id) - - # Install a course with sections/problems, tabs, updates, and handouts - self.course_fixture = CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], self.course_info['display_name'] - ) - - self.populate_course_fixture(self.course_fixture) - - self.course_fixture.install() - - # Auto-auth register for the course. - # Do this as global staff so that you will see the Staff View - AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, - course_id=self.course_id, staff=True).visit() - - def _goto_staff_page(self): - """ - Open staff page with assertion - """ - self.courseware_page.visit() - staff_page = StaffCoursewarePage(self.browser, self.course_id) - assert staff_page.staff_view_mode == 'Staff' - return staff_page - - -@attr(shard=20) -class CourseWithContentGroupsTest(StaffViewTest): - """ - Verifies that changing the "View this course as" selector works properly for content groups. - """ - - def setUp(self): - super().setUp() - # pylint: disable=protected-access - self.course_fixture._update_xblock(self.course_fixture._course_location, { - "metadata": { - "user_partitions": [ - create_user_partition_json( - MINIMUM_STATIC_PARTITION_ID, - 'Configuration alpha,beta', - 'Content Group Partition', - [ - Group(MINIMUM_STATIC_PARTITION_ID + 1, 'alpha'), - Group(MINIMUM_STATIC_PARTITION_ID + 2, 'beta') - ], - scheme="cohort" - ) - ], - }, - }) - - def populate_course_fixture(self, course_fixture): - """ - Populates test course with chapter, sequential, and 3 problems. - One problem is visible to all, one problem is visible only to Group "alpha", and - one problem is visible only to Group "beta". - """ - problem_data = dedent(""" - - - - - Yes - - - - """) - - self.alpha_text = "VISIBLE TO ALPHA" # lint-amnesty, pylint: disable=attribute-defined-outside-init - self.beta_text = "VISIBLE TO BETA" # lint-amnesty, pylint: disable=attribute-defined-outside-init - self.audit_text = "VISIBLE TO AUDIT" # lint-amnesty, pylint: disable=attribute-defined-outside-init - self.everyone_text = "VISIBLE TO EVERYONE" # lint-amnesty, pylint: disable=attribute-defined-outside-init - - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc( - 'problem', - self.alpha_text, - data=problem_data, - metadata={"group_access": {MINIMUM_STATIC_PARTITION_ID: [MINIMUM_STATIC_PARTITION_ID + 1]}} - ), - XBlockFixtureDesc( - 'problem', - self.beta_text, - data=problem_data, - metadata={"group_access": {MINIMUM_STATIC_PARTITION_ID: [MINIMUM_STATIC_PARTITION_ID + 2]}} - ), - XBlockFixtureDesc( - 'problem', - self.audit_text, - data=problem_data, - # Below 1 is the hardcoded group ID for "Audit" - metadata={"group_access": {ENROLLMENT_TRACK_PARTITION_ID: [1]}} - ), - XBlockFixtureDesc( - 'problem', - self.everyone_text, - data=problem_data - ) - ) - ) - ) - ) - - @attr('a11y') - def test_course_page(self): - """ - Run accessibility audit for course staff pages. - """ - course_page = self._goto_staff_page() - course_page.a11y_audit.config.set_rules({ - 'ignore': [ - 'aria-allowed-attr', # TODO: AC-559 - 'aria-roles', # TODO: AC-559, - 'aria-valid-attr', # TODO: AC-559 - 'color-contrast', # TODO: AC-559 - 'link-href', # TODO: AC-559 - 'section', # TODO: AC-559 - 'region', # TODO: AC-932 - ] - }) - course_page.a11y_audit.check_for_accessibility_errors() diff --git a/common/test/acceptance/tests/lms/test_problem_types.py b/common/test/acceptance/tests/lms/test_problem_types.py deleted file mode 100644 index 0171577b15..0000000000 --- a/common/test/acceptance/tests/lms/test_problem_types.py +++ /dev/null @@ -1,1109 +0,0 @@ -""" -Bok choy acceptance and a11y tests for problem types in the LMS -""" - - -import random -import textwrap -from abc import ABCMeta, abstractmethod - -import ddt -from bok_choy.promise import BrokenPromise - -from capa.tests.response_xml_factory import ( - AnnotationResponseXMLFactory, - ChoiceResponseXMLFactory, - ChoiceTextResponseXMLFactory, - CodeResponseXMLFactory, - CustomResponseXMLFactory, - FormulaResponseXMLFactory, - JSInputXMLFactory, - MultipleChoiceResponseXMLFactory, - NumericalResponseXMLFactory, - OptionResponseXMLFactory, - StringResponseXMLFactory, - SymbolicResponseXMLFactory -) -from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.lms.problem import ProblemPage -from common.test.acceptance.tests.helpers import EventsTestMixin, select_option_by_text -from common.test.acceptance.tests.lms.test_lms_problems import ProblemsTest -from openedx.core.lib.tests import attr - - -class ProblemTypeTestBaseMeta(ABCMeta): - """ - MetaClass for ProblemTypeTestBase to ensure that the required attributes - are defined in the inheriting classes. - """ - def __call__(cls, *args, **kwargs): - obj = type.__call__(cls, *args, **kwargs) - - required_attrs = [ - 'problem_name', - 'problem_type', - 'factory', - 'factory_kwargs', - 'status_indicators', - ] - - for required_attr in required_attrs: - msg = ('{} is a required attribute for {}').format( - required_attr, str(cls) - ) - - try: - if obj.__getattribute__(required_attr) is None: - raise NotImplementedError(msg) - except AttributeError: - raise NotImplementedError(msg) # lint-amnesty, pylint: disable=raise-missing-from - - return obj - - -class ProblemTypeTestBase(ProblemsTest, EventsTestMixin, metaclass=ProblemTypeTestBaseMeta): - """ - Base class for testing assesment problem types in bok choy. - - This inherits from ProblemsTest, which has capabilities for testing problem - features that are not problem type specific (checking, hinting, etc.). - - The following attributes must be explicitly defined when inheriting from - this class: - problem_name (str) - problem_type (str) - factory (ResponseXMLFactory subclass instance) - - Additionally, the default values for factory_kwargs and status_indicators - may need to be overridden for some problem types. - """ - - problem_name = None - problem_type = None - problem_points = 1 - factory = None - factory_kwargs = {} - status_indicators = { - 'correct': ['span.correct'], - 'incorrect': ['span.incorrect'], - 'unanswered': ['span.unanswered'], - 'submitted': ['span.submitted'], - 'unsubmitted': ['.unsubmitted'] - } - - def setUp(self): - """ - Visits courseware_page and defines self.problem_page. - """ - super().setUp() - self.courseware_page.visit() - self.problem_page = ProblemPage(self.browser) - - def get_sequential(self): - """ Allow any class in the inheritance chain to customize subsection metadata.""" - return XBlockFixtureDesc('sequential', 'Test Subsection', metadata=getattr(self, 'sequential_metadata', {})) - - def get_problem(self): - """ - Creates a {problem_type} problem - """ - # Generate the problem XML using capa.tests.response_xml_factory - return XBlockFixtureDesc( - 'problem', - self.problem_name, - data=self.factory.build_xml(**self.factory_kwargs), - metadata={'rerandomize': 'always', 'show_reset_button': True} - ) - - def wait_for_status(self, status): - """ - Waits for the expected status indicator. - - Args: - status: one of ("correct", "incorrect", "unanswered", "submitted") - """ - msg = f"Wait for status to be {status}" - selector = ', '.join(self.status_indicators[status]) - self.problem_page.wait_for_element_visibility(selector, msg) - - def problem_status(self, status): - """ - Returns the status of problem - Args: - status(string): status of the problem which is to be checked - - Returns: - True: If provided status is present on the page - False: If provided status is not present on the page - """ - selector = ', '.join(self.status_indicators[status]) - try: - self.problem_page.wait_for_element_visibility(selector, 'Status not present', timeout=10) - return True - except BrokenPromise: - return False - - @abstractmethod - def answer_problem(self, correctness): - """ - Args: - `correct` (bool): Inputs correct answer if True, else inputs - incorrect answer. - """ - raise NotImplementedError() - - -class ProblemTypeA11yTestMixin: - """ - Shared a11y tests for all problem types. - """ - @attr('a11y') - def test_problem_type_a11y(self): - """ - Run accessibility audit for the problem type. - """ - self.problem_page.wait_for( - lambda: self.problem_page.problem_name == self.problem_name, - "Make sure the correct problem is on the page" - ) - - # Set the scope to the problem container - self.problem_page.a11y_audit.config.set_scope( - include=['div#seq_content'] - ) - - # Run the accessibility audit. - self.problem_page.a11y_audit.check_for_accessibility_errors() - - -class AnnotationProblemTypeBase(ProblemTypeTestBase): - """ - ProblemTypeTestBase specialization for Annotation Problem Type - """ - problem_name = 'ANNOTATION TEST PROBLEM' - problem_type = 'annotationresponse' - problem_points = 2 - - factory = AnnotationResponseXMLFactory() - partially_correct = True - - can_submit_blank = True - can_update_save_notification = False - factory_kwargs = { - 'title': 'Annotation Problem', - 'text': 'The text being annotated', - 'comment': 'What do you think the about this text?', - 'comment_prompt': 'Type your answer below.', - 'tag_prompt': 'Which of these items most applies to the text?', - 'options': [ - ('dog', 'correct'), - ('cat', 'incorrect'), - ('fish', 'partially-correct'), - ] - } - - status_indicators = { - 'correct': ['span.correct'], - 'incorrect': ['span.incorrect'], - 'partially-correct': ['span.partially-correct'], - 'unanswered': ['span.unanswered'], - 'submitted': ['span.submitted'], - } - - def setUp(self, *args, **kwargs): - """ - Additional setup for AnnotationProblemTypeBase - """ - super().setUp(*args, **kwargs) - - self.problem_page.a11y_audit.config.set_rules({ - "ignore": [ - 'label', # TODO: AC-491 - 'label-title-only', # TODO: AC-493 - ] - }) - - def answer_problem(self, correctness): - """ - Answer annotation problem. - """ - if correctness == 'correct': - choice = 0 - elif correctness == 'partially-correct': - choice = 2 - else: - choice = 1 - answer = 'Student comment' - - self.problem_page.q(css='div.problem textarea.comment').fill(answer) - self.problem_page.q( - css='div.problem span.tag' - ).nth(choice).click() - - -class AnnotationProblemTypeTest(AnnotationProblemTypeBase, ProblemTypeA11yTestMixin): - """ - Standard tests for the Annotation Problem Type - """ - shard = 20 - pass # lint-amnesty, pylint: disable=unnecessary-pass - - -class CheckboxProblemTypeBase(ProblemTypeTestBase): - """ - ProblemTypeTestBase specialization Checkbox Problem Type - """ - problem_name = 'CHECKBOX TEST PROBLEM' - problem_type = 'checkbox' - partially_correct = True - - factory = ChoiceResponseXMLFactory() - - factory_kwargs = { - 'question_text': 'The correct answer is Choice 0 and Choice 2, Choice 1 and Choice 3 together are incorrect.', - 'choice_type': 'checkbox', - 'credit_type': 'edc', - 'choices': [True, False, True, False], - 'choice_names': ['Choice 0', 'Choice 1', 'Choice 2', 'Choice 3'], - 'explanation_text': 'This is explanation text' - } - - def answer_problem(self, correctness): - """ - Answer checkbox problem. - """ - if correctness == 'correct': - self.problem_page.click_choice("choice_0") - self.problem_page.click_choice("choice_2") - elif correctness == 'partially-correct': - self.problem_page.click_choice("choice_2") - else: - self.problem_page.click_choice("choice_1") - self.problem_page.click_choice("choice_3") - - -@ddt.ddt -class CheckboxProblemTypeTest(CheckboxProblemTypeBase, ProblemTypeA11yTestMixin): - """ - Standard tests for the Checkbox Problem Type - """ - shard = 18 - - -class CheckboxProblemTypeTestNonRandomized(CheckboxProblemTypeBase, ProblemTypeA11yTestMixin): - """ - Tests for the non-randomized checkbox problem - """ - - def get_problem(self): - """ - Creates a {problem_type} problem - """ - # Generate the problem XML using capa.tests.response_xml_factory - return XBlockFixtureDesc( - 'problem', - self.problem_name, - data=self.factory.build_xml(**self.factory_kwargs), - metadata={'rerandomize': 'never', 'show_reset_button': True} - ) - - -@ddt.ddt -class MultipleChoiceProblemTypeBase(ProblemTypeTestBase): - """ - ProblemTypeTestBase specialization Multiple Choice Problem Type - """ - problem_name = 'MULTIPLE CHOICE TEST PROBLEM' - problem_type = 'multiple choice' - - factory = MultipleChoiceResponseXMLFactory() - - partially_correct = False - - factory_kwargs = { - 'question_text': 'The correct answer is Choice 2', - 'choices': [False, False, True, False], - 'choice_names': ['choice_0', 'choice_1', 'choice_2', 'choice_3'], - } - status_indicators = { - 'correct': ['label.choicegroup_correct'], - 'incorrect': ['label.choicegroup_incorrect', 'span.incorrect'], - 'unanswered': ['span.unanswered'], - 'submitted': ['label.choicegroup_submitted', 'span.submitted'], - } - - def problem_status(self, status): - """ - Returns the status of problem - Args: - status(string): status of the problem which is to be checked - - Returns: - True: If provided status is present on the page - False: If provided status is not present on the page - """ - selector = ', '.join(self.status_indicators[status]) - try: - self.problem_page.wait_for_element_visibility(selector, 'Status not present', timeout=10) - return True - except BrokenPromise: - return False - - def answer_problem(self, correctness): - """ - Answer multiple choice problem. - """ - if correctness == 'incorrect': - self.problem_page.click_choice("choice_choice_1") - else: - self.problem_page.click_choice("choice_choice_2") - - -@ddt.ddt -class MultipleChoiceProblemTypeTest(MultipleChoiceProblemTypeBase, ProblemTypeA11yTestMixin): - """ - Standard tests for the Multiple Choice Problem Type - """ - shard = 24 - - -@ddt.ddt -class MultipleChoiceProblemTypeTestNonRandomized(MultipleChoiceProblemTypeBase, ProblemTypeA11yTestMixin): - """ - Tests for non-randomized multiple choice problem - """ - shard = 24 - - def get_problem(self): - """ - Creates a {problem_type} problem - """ - # Generate the problem XML using capa.tests.response_xml_factory - return XBlockFixtureDesc( - 'problem', - self.problem_name, - data=self.factory.build_xml(**self.factory_kwargs), - metadata={'rerandomize': 'never', 'show_reset_button': True, 'max_attempts': 3} - ) - - -class RadioProblemTypeBase(ProblemTypeTestBase): - """ - ProblemTypeTestBase specialization for Radio Problem Type - """ - problem_name = 'RADIO TEST PROBLEM' - problem_type = 'radio' - - partially_correct = False - - factory = ChoiceResponseXMLFactory() - - factory_kwargs = { - 'question_text': 'The correct answer is Choice 2', - 'choice_type': 'radio', - 'choices': [False, False, True, False], - 'choice_names': ['Choice 0', 'Choice 1', 'Choice 2', 'Choice 3'], - } - status_indicators = { - 'correct': ['label.choicegroup_correct'], - 'incorrect': ['label.choicegroup_incorrect', 'span.incorrect'], - 'unanswered': ['span.unanswered'], - 'submitted': ['label.choicegroup_submitted', 'span.submitted'], - } - - def problem_status(self, status): - """ - Returns the status of problem - Args: - status(string): status of the problem which is to be checked - - Returns: - True: If provided status is present on the page - False: If provided status is not present on the page - """ - selector = ', '.join(self.status_indicators[status]) - try: - self.problem_page.wait_for_element_visibility(selector, 'Status not present', timeout=10) - return True - except BrokenPromise: - return False - - def answer_problem(self, correctness): - """ - Answer radio problem. - """ - if correctness == 'correct': - self.problem_page.click_choice("choice_2") - else: - self.problem_page.click_choice("choice_1") - - -@ddt.ddt -class RadioProblemTypeTest(RadioProblemTypeBase, ProblemTypeA11yTestMixin): - """ - Standard tests for the Multiple Radio Problem Type - """ - shard = 24 - pass # lint-amnesty, pylint: disable=unnecessary-pass - - -class RadioProblemTypeTestNonRandomized(RadioProblemTypeBase, ProblemTypeA11yTestMixin): - """ - Tests for non-randomized radio problem - """ - shard = 8 - - def get_problem(self): - """ - Creates a {problem_type} problem - """ - # Generate the problem XML using capa.tests.response_xml_factory - return XBlockFixtureDesc( - 'problem', - self.problem_name, - data=self.factory.build_xml(**self.factory_kwargs), - metadata={'rerandomize': 'never', 'show_reset_button': True} - ) - - -class DropDownProblemTypeBase(ProblemTypeTestBase): - """ - ProblemTypeTestBase specialization for Drop Down Problem Type - """ - problem_name = 'DROP DOWN TEST PROBLEM' - problem_type = 'drop down' - - partially_correct = False - - factory = OptionResponseXMLFactory() - - factory_kwargs = { - 'question_text': 'The correct answer is Option 2', - 'options': ['Option 1', 'Option 2', 'Option 3', 'Option 4'], - 'correct_option': 'Option 2' - } - - def answer_problem(self, correctness): - """ - Answer drop down problem. - """ - answer = 'Option 2' if correctness == 'correct' else 'Option 3' - selector_element = self.problem_page.q( - css='.problem .option-input select') - select_option_by_text(selector_element, answer) - - -@ddt.ddt -class DropdownProblemTypeTest(DropDownProblemTypeBase, ProblemTypeA11yTestMixin): - """ - Standard tests for the Dropdown Problem Type - """ - shard = 8 - pass # lint-amnesty, pylint: disable=unnecessary-pass - - -@ddt.ddt -class DropDownProblemTypeTestNonRandomized(DropDownProblemTypeBase, ProblemTypeA11yTestMixin): - """ - Tests for non-randomized Dropdown problem - """ - shard = 8 - - def get_problem(self): - """ - Creates a {problem_type} problem - """ - # Generate the problem XML using capa.tests.response_xml_factory - return XBlockFixtureDesc( - 'problem', - self.problem_name, - data=self.factory.build_xml(**self.factory_kwargs), - metadata={'rerandomize': 'never', 'show_reset_button': True} - ) - - -class StringProblemTypeBase(ProblemTypeTestBase): - """ - ProblemTypeTestBase specialization for String Problem Type - """ - problem_name = 'STRING TEST PROBLEM' - problem_type = 'string' - - partially_correct = False - - factory = StringResponseXMLFactory() - - factory_kwargs = { - 'question_text': 'The answer is "correct string"', - 'case_sensitive': False, - 'answer': 'correct string', - } - - status_indicators = { - 'correct': ['div.correct'], - 'incorrect': ['div.incorrect'], - 'unanswered': ['div.unanswered', 'div.unsubmitted'], - 'submitted': ['span.submitted'], - } - - def problem_status(self, status): - """ - Returns the status of problem - Args: - status(string): status of the problem which is to be checked - - Returns: - True: If provided status is present on the page - False: If provided status is not present on the page - """ - selector = ', '.join(self.status_indicators[status]) - try: - self.problem_page.wait_for_element_visibility(selector, 'Status not present', timeout=10) - return True - except BrokenPromise: - return False - - def answer_problem(self, correctness): - """ - Answer string problem. - """ - textvalue = 'correct string' if correctness == 'correct' else 'incorrect string' - self.problem_page.fill_answer(textvalue) # lint-amnesty, pylint: disable=no-member - - -class StringProblemTypeTest(StringProblemTypeBase, ProblemTypeA11yTestMixin): - """ - Standard tests for the String Problem Type - """ - shard = 8 - pass # lint-amnesty, pylint: disable=unnecessary-pass - - -class NumericalProblemTypeBase(ProblemTypeTestBase): - """ - ProblemTypeTestBase specialization for Numerical Problem Type - """ - problem_name = 'NUMERICAL TEST PROBLEM' - problem_type = 'numerical' - partially_correct = False - - factory = NumericalResponseXMLFactory() - - factory_kwargs = { - 'question_text': 'The answer is pi + 1', - 'answer': '4.14159', - 'tolerance': '0.00001', - 'math_display': True, - } - - status_indicators = { - 'correct': ['div.correct'], - 'incorrect': ['div.incorrect'], - 'unanswered': ['div.unanswered', 'div.unsubmitted'], - 'submitted': ['div.submitted'], - 'unsubmitted': ['div.unsubmitted'] - } - - def problem_status(self, status): - """ - Returns the status of problem - Args: - status(string): status of the problem which is to be checked - - Returns: - True: If provided status is present on the page - False: If provided status is not present on the page - """ - selector = ', '.join(self.status_indicators[status]) - try: - self.problem_page.wait_for_element_visibility(selector, 'Status not present', timeout=10) - return True - except BrokenPromise: - return False - - def answer_problem(self, correctness): - """ - Answer numerical problem. - """ - textvalue = '' - if correctness == 'correct': - textvalue = "pi + 1" - elif correctness == 'error': - textvalue = 'notNum' - else: - textvalue = str(random.randint(-2, 2)) - self.problem_page.fill_answer(textvalue) # lint-amnesty, pylint: disable=no-member - - -@ddt.ddt -class NumericalProblemTypeTest(NumericalProblemTypeBase, ProblemTypeA11yTestMixin): - """ - Standard tests for the Numerical Problem Type - """ - shard = 12 - - -@ddt.ddt -class NumericalProblemTypeTestNonRandomized(NumericalProblemTypeBase, ProblemTypeA11yTestMixin): - """ - Tests for non-randomized Numerical problem - """ - shard = 12 - - def get_problem(self): - """ - Creates a {problem_type} problem - """ - # Generate the problem XML using capa.tests.response_xml_factory - return XBlockFixtureDesc( - 'problem', - self.problem_name, - data=self.factory.build_xml(**self.factory_kwargs), - metadata={'rerandomize': 'never', 'show_reset_button': True} - ) - - -@ddt.ddt -class FormulaProblemTypeBase(ProblemTypeTestBase): - """ - ProblemTypeTestBase specialization for Formula Problem Type - """ - problem_name = 'FORMULA TEST PROBLEM' - problem_type = 'formula' - partially_correct = False - - factory = FormulaResponseXMLFactory() - - factory_kwargs = { - 'question_text': 'The solution is [mathjax]x^2+2x+y[/mathjax]', - 'sample_dict': {'x': (-100, 100), 'y': (-100, 100)}, - 'num_samples': 10, - 'tolerance': 0.00001, - 'math_display': True, - 'answer': 'x^2+2*x+y', - } - - status_indicators = { - 'correct': ['div.correct'], - 'incorrect': ['div.incorrect'], - 'unanswered': ['div.unanswered', 'div.unsubmitted'], - 'submitted': ['div.submitted'], - } - - def problem_status(self, status): - """ - Returns the status of problem - Args: - status(string): status of the problem which is to be checked - - Returns: - True: If provided status is present on the page - False: If provided status is not present on the page - """ - selector = ', '.join(self.status_indicators[status]) - try: - self.problem_page.wait_for_element_visibility(selector, 'Status not present', timeout=10) - return True - except BrokenPromise: - return False - - def answer_problem(self, correctness): - """ - Answer formula problem. - """ - textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' - self.problem_page.fill_answer(textvalue) # lint-amnesty, pylint: disable=no-member - - -@ddt.ddt -class ScriptProblemTypeBase(ProblemTypeTestBase): - """ - ProblemTypeTestBase specialization for Script Problem Type - """ - problem_name = 'SCRIPT TEST PROBLEM' - problem_type = 'script' - problem_points = 2 - partially_correct = False - - factory = CustomResponseXMLFactory() - - factory_kwargs = { - 'cfn': 'test_add_to_ten', - 'expect': '10', - 'num_inputs': 2, - 'question_text': 'Enter two integers that sum to 10.', - 'input_element_label': 'Enter an integer', - 'script': textwrap.dedent(""" - def test_add_to_ten(expect,ans): - try: - a1=int(ans[0]) - a2=int(ans[1]) - except ValueError: - a1=0 - a2=0 - return (a1+a2)==int(expect) - """), - } - status_indicators = { - 'correct': ['div.correct'], - 'incorrect': ['div.incorrect'], - 'unanswered': ['div.unanswered', 'div.unsubmitted'], - 'submitted': ['div.submitted'], - } - - def problem_status(self, status): - """ - Returns the status of problem - Args: - status(string): status of the problem which is to be checked - - Returns: - True: If provided status is present on the page - """ - selector = ', '.join(self.status_indicators[status]) - try: - self.problem_page.wait_for_element_visibility(selector, 'Status is present', timeout=10) - return True - except BrokenPromise: - return False - - def answer_problem(self, correctness): - """ - Answer script problem. - """ - # Correct answer is any two integers that sum to 10 - first_addend = random.randint(-100, 100) - second_addend = 10 - first_addend - - # If we want an incorrect answer, then change - # the second addend so they no longer sum to 10 - if not correctness == 'correct': - second_addend += random.randint(1, 10) - - self.problem_page.fill_answer(first_addend, input_num=0) # lint-amnesty, pylint: disable=no-member - self.problem_page.fill_answer(second_addend, input_num=1) # lint-amnesty, pylint: disable=no-member - - -@ddt.ddt -class ScriptProblemTypeTest(ScriptProblemTypeBase, ProblemTypeA11yTestMixin): - """ - Standard tests for the Script Problem Type - """ - shard = 20 - pass # lint-amnesty, pylint: disable=unnecessary-pass - - -class ScriptProblemTypeTestNonRandomized(ScriptProblemTypeBase, ProblemTypeA11yTestMixin): - """ - Tests for non-randomized Script problem - """ - shard = 8 - - def get_problem(self): - """ - Creates a {problem_type} problem - """ - # Generate the problem XML using capa.tests.response_xml_factory - return XBlockFixtureDesc( - 'problem', - self.problem_name, - data=self.factory.build_xml(**self.factory_kwargs), - metadata={'rerandomize': 'never', 'show_reset_button': True} - ) - - -class JSInputTypeTest(ProblemTypeTestBase, ProblemTypeA11yTestMixin): - """ - TestCase Class for jsinput (custom JavaScript) problem type. - Right now the only test point that is executed is the a11y test. - This is because the factory simply creates an empty iframe. - """ - problem_name = 'JSINPUT PROBLEM' - problem_type = 'customresponse' - - factory = JSInputXMLFactory() - - factory_kwargs = { - 'question_text': 'IFrame shows below (but has no content)' - } - - def answer_problem(self, correctness): - """ - Problem is not set up to work (displays an empty iframe), but this method must - be extended because the parent class has marked it as abstract. - """ - raise NotImplementedError() - - -class CodeProblemTypeBase(ProblemTypeTestBase): - """ - ProblemTypeTestBase specialization for Code Problem Type - """ - problem_name = 'CODE TEST PROBLEM' - problem_type = 'code' - partially_correct = False - can_update_save_notification = False - factory = CodeResponseXMLFactory() - - factory_kwargs = { - 'question_text': 'Submit code to an external grader', - 'initial_display': 'print "Hello world!"', - 'grader_payload': '{"grader": "ps1/Spring2013/test_grader.py"}', - } - - status_indicators = { - 'correct': ['.grader-status .correct ~ .debug'], - 'incorrect': ['.grader-status .incorrect ~ .debug'], - 'unanswered': ['.grader-status .unanswered ~ .debug'], - 'submitted': ['.grader-status .submitted ~ .debug'], - } - - def answer_problem(self, correctness): - """ - Answer code problem. - """ - # The fake xqueue server is configured to respond - # correct / incorrect no matter what we submit. - # Furthermore, since the inline code response uses - # JavaScript to make the code display nicely, it's difficult - # to programatically input text - # (there's not