From a91b1e7bcfcfdcb685403f5df3f5b4b5c357f62d Mon Sep 17 00:00:00 2001 From: salman2013 Date: Fri, 10 Nov 2023 12:39:57 +0500 Subject: [PATCH 1/5] chore: remove bok-choy usage --- a11y_tests/pages.py | 25 - ...test_course_enrollment_demographics_axs.py | 42 -- acceptance_tests/mixins.py | 14 - acceptance_tests/pages.py | 351 -------------- acceptance_tests/test_auth.py | 30 -- acceptance_tests/test_course_detail.py | 140 ------ acceptance_tests/test_course_engagement.py | 273 ----------- acceptance_tests/test_course_enrollment.py | 211 --------- .../test_course_enrollment_demographics.py | 221 --------- acceptance_tests/test_course_index.py | 335 -------------- acceptance_tests/test_course_performance.py | 438 ------------------ acceptance_tests/test_error_pages.py | 38 -- acceptance_tests/test_landing.py | 81 ---- requirements/base.txt | 10 +- requirements/doc.txt | 10 +- requirements/local.txt | 10 - requirements/test.in | 1 - requirements/test.txt | 56 ++- 18 files changed, 49 insertions(+), 2237 deletions(-) delete mode 100644 a11y_tests/test_course_enrollment_demographics_axs.py delete mode 100644 acceptance_tests/pages.py delete mode 100644 acceptance_tests/test_auth.py delete mode 100644 acceptance_tests/test_course_detail.py delete mode 100644 acceptance_tests/test_course_engagement.py delete mode 100644 acceptance_tests/test_course_enrollment.py delete mode 100644 acceptance_tests/test_course_enrollment_demographics.py delete mode 100644 acceptance_tests/test_course_index.py delete mode 100644 acceptance_tests/test_course_performance.py delete mode 100644 acceptance_tests/test_error_pages.py delete mode 100644 acceptance_tests/test_landing.py diff --git a/a11y_tests/pages.py b/a11y_tests/pages.py index a923d58bd..ff392c2c7 100644 --- a/a11y_tests/pages.py +++ b/a11y_tests/pages.py @@ -3,34 +3,9 @@ """ -from bok_choy.page_object import PageObject - from acceptance_tests import DASHBOARD_SERVER_URL, TEST_COURSE_ID -class CoursePage(PageObject): - basic_auth_username = None - basic_auth_password = None - - def __init__(self, browser, course_id=None): - # Create the path - self.course_id = course_id or TEST_COURSE_ID - path = f'courses/{self.course_id}' - - self.server_url = DASHBOARD_SERVER_URL - self.page_url = f'{self.server_url}/{path}' - - # Call the constructor and setup the URL - super().__init__(browser) - - def is_browser_on_page(self): - return self.browser.current_url == self.page_url - - @property - def url(self): - return self.page_url - - class CourseEnrollmentDemographicsPage(CoursePage): demographic = None diff --git a/a11y_tests/test_course_enrollment_demographics_axs.py b/a11y_tests/test_course_enrollment_demographics_axs.py deleted file mode 100644 index ada860ca5..000000000 --- a/a11y_tests/test_course_enrollment_demographics_axs.py +++ /dev/null @@ -1,42 +0,0 @@ -from bok_choy.promise import EmptyPromise -from bok_choy.web_app_test import WebAppTest - -from a11y_tests.mixins import CoursePageTestsMixin -from a11y_tests.pages import CourseEnrollmentDemographicsAgePage - -_multiprocess_can_split_ = True - - -class CourseEnrollmentDemographicsAgeTests(CoursePageTestsMixin, WebAppTest): - """ - A test for the accessibility of the CourseEnrollmentDemographicsAgePage. - """ - - def setUp(self): - super().setUp() - self.page = CourseEnrollmentDemographicsAgePage(self.browser) - - def test_a11y(self): - # Log in and navigate to page - self.login() - self.page.visit() - - self.page.a11y_audit.config.set_rules({ - "ignore": [ - 'color-contrast', # TODO: AN-6010, AN-6011 - 'skip-link', # TODO: AN-6185 - 'link-href', # TODO: AN-6186 - 'icon-aria-hidden', # TODO: AN-6187 - 'page-has-heading-one', - 'aria-hidden-focus', - ], - }) - - # Wait for the datatable to finish loading - ready_promise = EmptyPromise( - lambda: 'Loading' not in self.page.q(css='div.section-data-table').text, - "Page finished loading" - ).fulfill() - - # Check the page for accessibility errors - report = self.page.a11y_audit.check_for_accessibility_errors() diff --git a/acceptance_tests/mixins.py b/acceptance_tests/mixins.py index 2a0007522..1895630e9 100644 --- a/acceptance_tests/mixins.py +++ b/acceptance_tests/mixins.py @@ -3,7 +3,6 @@ from unittest import skip from analyticsclient.client import Client -from bok_choy.promise import EmptyPromise from selenium.webdriver.common.keys import Keys from acceptance_tests import ( @@ -89,19 +88,6 @@ def assertValidFeedbackLink(self, selector): element = self.page.q(css=selector) self.assertEqual(element.text[0], DASHBOARD_FEEDBACK_EMAIL) - def fulfill_loading_promise(self, css_selector): - """ - Ensure the info contained by `css_selector` is loaded via AJAX. - - Arguments - css_selector (string) -- CSS selector of the parent element that will contain the loading message. - """ - - EmptyPromise( - lambda: 'Loading...' not in self.page.q(css=css_selector + ' .loading-container').text, - "Loading finished." - ).fulfill() - def assertTable(self, table_selector, columns, download_selector=None): # Ensure the table is loaded via AJAX self.fulfill_loading_promise(table_selector) diff --git a/acceptance_tests/pages.py b/acceptance_tests/pages.py deleted file mode 100644 index 13e94be73..000000000 --- a/acceptance_tests/pages.py +++ /dev/null @@ -1,351 +0,0 @@ -""" -Tests for course analytics pages -""" - -from bok_choy.page_object import PageObject -from bok_choy.promise import EmptyPromise - -from acceptance_tests import ( - BASIC_AUTH_PASSWORD, - BASIC_AUTH_USERNAME, - DASHBOARD_SERVER_URL, - LMS_HOSTNAME, - LMS_SSL_ENABLED, - TEST_ASSIGNMENT_ID, - TEST_ASSIGNMENT_TYPE, - TEST_COURSE_ID, - TEST_GRADED_PROBLEM_ID, - TEST_GRADED_PROBLEM_PART_ID, - TEST_UNGRADED_PROBLEM_ID, - TEST_UNGRADED_PROBLEM_PART_ID, - TEST_UNGRADED_SECTION_ID, - TEST_UNGRADED_SUBSECTION_ID, - TEST_VIDEO_ID, - TEST_VIDEO_SECTION_ID, - TEST_VIDEO_SUBSECTION_ID, -) - - -class DashboardPage(PageObject): # pylint: disable=abstract-method - path = None - basic_auth_username = None - basic_auth_password = None - - @property - def url(self): - return self.page_url - - def __init__(self, browser, path=None): - super().__init__(browser) - path = path or self.path - self.server_url = DASHBOARD_SERVER_URL - self.page_url = f'{self.server_url}/{path}' - - -class LandingPage(DashboardPage): - path = '' - - def is_browser_on_page(self): - return self.browser.current_url == self.page_url - - -class CoursePage(DashboardPage): - def __init__(self, browser, course_id=None): - # Create the path - self.course_id = course_id or TEST_COURSE_ID - path = f'courses/{self.course_id}' - - # Call the constructor and setup the URL - super().__init__(browser, path) - - def is_browser_on_page(self): - return self.browser.current_url == self.page_url - - -class CourseHomePage(CoursePage): - def __init__(self, browser, course_id=None): - super().__init__(browser, course_id) - self.page_url += '/' - - def is_browser_on_page(self): - return super().is_browser_on_page() and self.browser.title.startswith('Course Home') - - -class CourseEnrollmentActivityPage(CoursePage): - def __init__(self, browser, course_id=None): - super().__init__(browser, course_id) - self.page_url += '/enrollment/activity/' - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - 'Enrollment Activity' in self.browser.title - - -class LMSLoginPage(PageObject): - @property - def url(self): - protocol = 'https' if LMS_SSL_ENABLED else 'http' - - if BASIC_AUTH_USERNAME and BASIC_AUTH_PASSWORD: - return f'{protocol}://{BASIC_AUTH_USERNAME}:{BASIC_AUTH_PASSWORD}@{LMS_HOSTNAME}/login' - - return f'{protocol}://{LMS_HOSTNAME}/login' - - def is_browser_on_page(self): - return self.browser.title.startswith('Log into') - - def _is_browser_on_lms_dashboard(self): - return lambda: self.browser.title.startswith('Dashboard') - - def login(self, username, password): - self.q(css='input#email').fill(username) - self.q(css='input#password').fill(password) - self.q(css='button#submit').click() - - # Wait for LMS to redirect to the dashboard - EmptyPromise(self._is_browser_on_lms_dashboard(), "LMS login redirected to dashboard").fulfill() - - -class LoginPage(DashboardPage): - path = 'login' - - def is_browser_on_page(self): - return True - - -class CourseEnrollmentDemographicsPage(CoursePage): - demographic = None - - def __init__(self, browser, course_id=None): - super().__init__(browser, course_id) - self.page_url += f'/enrollment/demographics/{self.demographic}/' - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - f'Enrollment Demographics by {self.demographic.title()}' in self.browser.title - - -class CourseEnrollmentDemographicsAgePage(CourseEnrollmentDemographicsPage): - demographic = 'age' - - -class CourseEnrollmentDemographicsGenderPage(CourseEnrollmentDemographicsPage): - demographic = 'gender' - - -class CourseEnrollmentDemographicsEducationPage(CourseEnrollmentDemographicsPage): - demographic = 'education' - - -class CourseEnrollmentGeographyPage(CoursePage): - def __init__(self, browser, course_id=None): - super().__init__(browser, course_id) - self.page_url += '/enrollment/geography/' - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - 'Enrollment Geography' in self.browser.title - - -class CourseEngagementContentPage(CoursePage): - def __init__(self, browser, course_id=None): - super().__init__(browser, course_id) - self.page_url += '/engagement/content/' - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - 'Engagement Content' in self.browser.title - - -class CourseEngagementVideosContentPage(CoursePage): - def __init__(self, browser, course_id=None): - super().__init__(browser, course_id) - self.page_url += '/engagement/videos/' - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - 'Engagement Videos' in self.browser.title - - -class CourseEngagementVideoSectionPage(CoursePage): - def __init__(self, browser, course_id=None, section_id=None): - super().__init__(browser, course_id) - self.section_id = section_id or TEST_VIDEO_SECTION_ID - self.page_url += f'/engagement/videos/sections/{self.section_id}/' - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - 'Engagement Videos' in self.browser.title - - -class CourseEngagementVideoSubsectionPage(CoursePage): - def __init__(self, browser, course_id=None, section_id=None, subsection_id=None): - super().__init__(browser, course_id) - self.section_id = section_id or TEST_VIDEO_SECTION_ID - self.subsection_id = subsection_id or TEST_VIDEO_SUBSECTION_ID - self.page_url += '/engagement/videos/sections/{}/subsections/{}/'.format( - self.section_id, self.subsection_id) - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - 'Engagement Videos' in self.browser.title - - -class CourseEngagementVideoTimelinePage(CoursePage): - def __init__(self, browser, course_id=None, section_id=None, subsection_id=None, video_id=None): - super().__init__(browser, course_id) - self.section_id = section_id or TEST_VIDEO_SECTION_ID - self.subsection_id = subsection_id or TEST_VIDEO_SUBSECTION_ID - self.video_id = video_id or TEST_VIDEO_ID - self.page_url += '/engagement/videos/sections/{}/subsections/{}/modules/{}/timeline/'.format( - self.section_id, self.subsection_id, self.video_id) - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - 'Engagement Videos' in self.browser.title - - -class CourseIndexPage(DashboardPage): - path = 'courses/' - - def is_browser_on_page(self): - return self.browser.title.startswith('Courses') - - -class CoursePerformanceUngradedContentPage(CoursePage): - def __init__(self, browser, course_id=None): - super().__init__(browser, course_id) - self.page_url += '/performance/ungraded_content/' - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - 'Ungraded Problems' in self.browser.title - - -class CoursePerformanceUngradedSectionPage(CoursePage): - def __init__(self, browser, course_id=None, section_id=None): - super().__init__(browser, course_id) - self.section_id = section_id or TEST_UNGRADED_SECTION_ID - self.page_url += f'/performance/ungraded_content/sections/{self.section_id}/' - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - 'Ungraded Problems' in self.browser.title - - -class CoursePerformanceUngradedSubsectionPage(CoursePage): - def __init__(self, browser, course_id=None, section_id=None, subsection_id=None): - super().__init__(browser, course_id) - self.section_id = section_id or TEST_UNGRADED_SECTION_ID - self.subsection_id = subsection_id or TEST_UNGRADED_SUBSECTION_ID - self.page_url += '/performance/ungraded_content/sections/{}/subsections/{}/'.format( - self.section_id, self.subsection_id) - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - 'Ungraded Problems' in self.browser.title - - -class CoursePerformanceUngradedAnswerDistributionPage(CoursePage): - def __init__(self, browser, course_id=None, section_id=None, subsection_id=None, problem_id=None, part_id=None): - super().__init__(browser, course_id) - self.section_id = section_id or TEST_UNGRADED_SECTION_ID - self.subsection_id = subsection_id or TEST_UNGRADED_SUBSECTION_ID - self.problem_id = problem_id or TEST_UNGRADED_PROBLEM_ID - self.part_id = part_id or TEST_UNGRADED_PROBLEM_PART_ID - self.page_url += '/performance/ungraded_content/sections/{}/subsections/{}/problems/{}/' \ - 'parts/{}/answer_distribution/'.format(self.section_id, self.subsection_id, - self.problem_id, self.part_id) - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - self.browser.title.startswith('Performance: Problem Submissions') - - -class CoursePerformanceGradedContentPage(CoursePage): - def __init__(self, browser, course_id=None): - super().__init__(browser, course_id) - self.page_url += '/performance/graded_content/' - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - 'Graded Content' in self.browser.title - - -class CoursePerformanceGradedContentByTypePage(CoursePage): - def __init__(self, browser, course_id=None, assignment_type=None): - super().__init__(browser, course_id) - self.assignment_type = assignment_type or TEST_ASSIGNMENT_TYPE - self.page_url += f'/performance/graded_content/{self.assignment_type}/' - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - self.assignment_type in self.browser.title - - -class CoursePerformanceAssignmentPage(CoursePage): - def __init__(self, browser, course_id=None, assignment_id=None): - super().__init__(browser, course_id) - self.assignment_id = assignment_id or TEST_ASSIGNMENT_ID - self.page_url += f'/performance/graded_content/assignments/{self.assignment_id}/' - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - 'Graded Content' in self.browser.title - - -class CoursePerformanceAnswerDistributionPage(CoursePage): - def __init__(self, browser, course_id=None, assignment_id=None, problem_id=None, part_id=None): - super().__init__(browser, course_id) - self.assignment_id = assignment_id or TEST_ASSIGNMENT_ID - self.problem_id = problem_id or TEST_GRADED_PROBLEM_ID - self.part_id = part_id or TEST_GRADED_PROBLEM_PART_ID - self.page_url += '/performance/graded_content/assignments/{}/problems/{}/parts/{}/answer_distribution/'.format( - self.assignment_id, self.problem_id, self.part_id) - - def is_browser_on_page(self): - return super().is_browser_on_page() and \ - self.browser.title.startswith('Performance: Problem Submissions') - - -class CourseLearnersPage(CoursePage): - def __init__(self, browser, course_id=None): - super().__init__(browser, course_id) - self.page_url += '/learners/' - - def is_browser_on_page(self): - return super().is_browser_on_page() \ - and self.browser.title.startswith('Learners') - - -class ErrorPage(DashboardPage): - error_code = None - error_title = None - - def __init__(self, browser): - self.path = self.path or f'{self.error_code}/' - super().__init__(browser) - - def is_browser_on_page(self): - element = self.q(css='.error-title') - return element.present and element.text[0] == self.error_title - - -class ServerErrorPage(ErrorPage): - error_code = 500 - error_title = 'An Error Occurred' - - -class NotFoundErrorPage(ErrorPage): - error_code = 404 - error_title = 'Page Not Found' - - -class AccessDeniedErrorPage(ErrorPage): - error_code = 403 - error_title = 'Access Denied' - - -class ServiceUnavailableErrorPage(ErrorPage): - error_code = 503 - error_title = "We're having trouble loading this page. Please try again in a minute." diff --git a/acceptance_tests/test_auth.py b/acceptance_tests/test_auth.py deleted file mode 100644 index 90c163fbb..000000000 --- a/acceptance_tests/test_auth.py +++ /dev/null @@ -1,30 +0,0 @@ -from unittest import skipUnless - -from bok_choy.web_app_test import WebAppTest - -from acceptance_tests import ENABLE_OAUTH_TESTS -from acceptance_tests.mixins import LoginMixin -from acceptance_tests.pages import LoginPage - - -@skipUnless(ENABLE_OAUTH_TESTS, 'OAuth tests are not enabled.') -class OAuth2FlowTests(LoginMixin, WebAppTest): - def setUp(self): - """ - Instantiate the page objects. - """ - super().setUp() - - self.insights_login_page = LoginPage(self.browser) - - def test_login(self): - self.login_with_lms() - - # Visit login URL and get redirected - self.insights_login_page.visit() - - # User should arrive at course index page (or access denied page, if no permissions) - # Splitting this out into two separate tests would require two separate sets of credentials. That is - # feasible, but somewhat time-consuming. For now, we will rely on unit tests to validate the permissions and - # ensure both cases below are met. - self.assertTrue(self.browser.title.startswith('Courses') or self.browser.title.startswith('Access Denied')) diff --git a/acceptance_tests/test_course_detail.py b/acceptance_tests/test_course_detail.py deleted file mode 100644 index 38a8ff930..000000000 --- a/acceptance_tests/test_course_detail.py +++ /dev/null @@ -1,140 +0,0 @@ -from bok_choy.web_app_test import WebAppTest - -from acceptance_tests import ENABLE_COURSE_API -from acceptance_tests.mixins import CoursePageTestsMixin -from acceptance_tests.pages import CourseHomePage - -_multiprocess_can_split_ = True - - -# pylint: disable=abstract-method -class CourseHomeTests(CoursePageTestsMixin, WebAppTest): - def setUp(self): - super().setUp() - self.page = CourseHomePage(self.browser) - self.course = self.analytics_api_client.courses(self.page.course_id) - - def test_page(self): - super().test_page() - self._test_table() - - def _test_data_update_message(self): - # The course homepage does not display any data. - pass - - def _view_to_href(self, view): - """ - Generates a URL path from the specified view name. - """ - return '/' + view.replace('_', '/') \ - .replace('courses:', f'courses/{self.page.course_id}/') \ - .replace(':', '/') + '/' - - def _test_table(self): - table_items = [ - { - 'name': 'Enrollment', - 'icon': 'fa-child', - 'heading': 'Who are my learners?', - 'items': [ - { - 'title': 'How many learners are in my course?', - 'view': 'courses:enrollment:activity', - 'breadcrumbs': ['Activity'] - }, - { - 'title': 'How old are my learners?', - 'view': 'courses:enrollment:demographics_age', - 'breadcrumbs': ['Demographics', 'Age'] - }, - { - 'title': 'What level of education do my learners have?', - 'view': 'courses:enrollment:demographics_education', - 'breadcrumbs': ['Demographics', 'Education'] - }, - { - 'title': 'What is the learner gender breakdown?', - 'view': 'courses:enrollment:demographics_gender', - 'breadcrumbs': ['Demographics', 'Gender'] - }, - { - 'title': 'Where are my learners?', - 'view': 'courses:enrollment:geography', - 'breadcrumbs': ['Geography'] - }, - ], - } - ] - - engagement = { - 'name': 'Engagement', - 'icon': 'fa-bar-chart', - 'heading': 'What are learners doing in my course?', - 'items': [ - { - 'title': 'How many learners are interacting with my course?', - 'view': 'courses:engagement:content', - 'breadcrumbs': ['Content'] - } - ] - } - if ENABLE_COURSE_API: - engagement['items'].append({ - 'title': 'How did learners interact with course videos?', - 'view': 'courses:engagement:videos', - 'breadcrumbs': ['Videos'] - }) - table_items.append(engagement) - - if ENABLE_COURSE_API: - table_items.append({ - 'name': 'Performance', - 'icon': 'fa-check-square-o', - 'heading': 'How are learners doing on course assignments?', - 'items': [ - { - 'title': 'How are learners doing on graded course assignments?', - 'view': 'courses:performance:graded_content', - 'breadcrumbs': ['Graded Content'] - } - ] - }) - - table_outer = self.page.browser.find_element_by_css_selector('.course-home-table-outer') - - headings = table_outer.find_elements_by_css_selector('header .heading') - table_elements = table_outer.find_elements_by_css_selector('.course-home-table') - - for i, item in enumerate(table_items): - # Check the headings - self.assertEqual(headings[i].text, item['heading']) - - table = table_elements[i] - - # Check the name and icon - name = table.find_element_by_css_selector('.name') - self.assertEqual(name.text, item['name']) - - # If this element doesn't exist an exception will be thrown - name.find_element_by_css_selector('span.ico.fa.%s' % item['icon']) - - # Retrieve the individual table rows - rows = table.find_elements_by_css_selector('.item') - for j, row in enumerate(item['items']): - if j <= 0: - # First element is the name (checked above) - continue - - element = rows[j + 1] - - # Check the title and link - title = element.find_element_by_css_selector('.title') - self.assertEqual(title.text, row['title']) - expected = self._view_to_href(row['view']) - actual = title.find_element_by_css_selector('a').get_attribute('href') - self.assertTrue(actual.endswith(expected), f'{actual} should end with {expected}') - - # Check the breadcrumbs - breadcrumbs = element.find_element_by_css_selector('.breadcrumbs') - breadcrumbs.find_element_by_css_selector('span.ico.fa.%s' % item['icon']) - self.assertEqual(breadcrumbs.text, ' '.join(row['breadcrumbs'])) diff --git a/acceptance_tests/test_course_engagement.py b/acceptance_tests/test_course_engagement.py deleted file mode 100644 index 1084b1c09..000000000 --- a/acceptance_tests/test_course_engagement.py +++ /dev/null @@ -1,273 +0,0 @@ -import datetime -from unittest import skipUnless - -from analyticsclient.constants import activity_types as at -from bok_choy.web_app_test import WebAppTest -from opaque_keys.edx.keys import UsageKey - -from acceptance_tests import ( - ENABLE_COURSE_API, - ENABLE_FORUM_POSTS, - ENABLE_VIDEO_PREVIEW, -) -from acceptance_tests.mixins import CoursePageTestsMixin -from acceptance_tests.pages import ( - CourseEngagementContentPage, - CourseEngagementVideosContentPage, - CourseEngagementVideoSectionPage, - CourseEngagementVideoSubsectionPage, - CourseEngagementVideoTimelinePage, -) - -_multiprocess_can_split_ = True - - -class CourseEngagementPageTestsMixin(CoursePageTestsMixin): - help_path = 'engagement/Engagement_Content.html' - chart_selector = None - - def test_page(self): - super().test_page() - self._test_chart() - self._test_table() - - def _test_chart(self): - # Ensure the graph is loaded via AJAX - self.fulfill_loading_promise(self.chart_selector) - self.assertElementHasContent(self.chart_selector) - - def _test_table(self): - raise NotImplementedError - - -class CourseEngagementContentTests(CourseEngagementPageTestsMixin, WebAppTest): - """ - Tests for the Engagement content page. - """ - - chart_selector = '#engagement-trend-view' - - def setUp(self): - """ - Instantiate the page object. - """ - super().setUp() - self.page = CourseEngagementContentPage(self.browser) - self.course = self.analytics_api_client.courses(self.page.course_id) - - def test_page(self): - super().test_page() - self._test_engagement_metrics() - - def _get_data_update_message(self): - recent_activity = self.course.activity()[0] - last_updated = datetime.datetime.strptime(recent_activity['created'], self.api_datetime_format) - return 'Course engagement data was last updated %(update_date)s at %(update_time)s UTC.' % \ - self.format_last_updated_date_and_time(last_updated) - - def _test_engagement_metrics(self): - """ Verify the metrics tiles display the correct information. """ - end_date = datetime.datetime.utcnow().strftime(self.analytics_api_client.DATETIME_FORMAT) - recent_activity = self.course.activity(end_date=end_date)[-1] - - # Verify the activity values - activity_types = [at.ANY, at.ATTEMPTED_PROBLEM, at.PLAYED_VIDEO] - expected_tooltips = { - at.ANY: 'Learners who visited at least one page in the course content.', - at.ATTEMPTED_PROBLEM: 'Learners who submitted an answer for a standard problem. ' - 'Not all problem types are included.', - at.PLAYED_VIDEO: 'Learners who played one or more videos.' - } - for activity_type in activity_types: - data_selector = f'data-activity-type={activity_type}' - self.assertSummaryPointValueEquals(data_selector, self.format_number(recent_activity[activity_type])) - self.assertSummaryTooltipEquals(data_selector, expected_tooltips[activity_type]) - - def _test_table(self): - """ Verify the activity table is rendered with the correct information. """ - date_time_format = self.analytics_api_client.DATETIME_FORMAT - - end_date = datetime.datetime.utcnow() - end_date_string = end_date.strftime(self.analytics_api_client.DATETIME_FORMAT) - - trend_activity = self.course.activity(start_date=None, end_date=end_date_string) - trend_activity = sorted(trend_activity, reverse=True, key=lambda item: item['interval_end']) - - table_selector = 'div[data-role=engagement-table] table' - - headings = ['Week Ending', 'Active Learners', 'Watched a Video', 'Tried a Problem'] - if ENABLE_FORUM_POSTS: - headings.append('Participated in Discussions') - headings.append('Percent of Current Learners') - - self.assertTableColumnHeadingsEqual(table_selector, headings) - - rows = self.page.browser.find_elements_by_css_selector('%s tbody tr' % table_selector) - self.assertGreater(len(rows), 0) - - for i, row in enumerate(rows): - columns = row.find_elements_by_css_selector('td') - weekly_activity = trend_activity[i] - expected_date = self.format_time_as_dashboard( - (datetime.datetime.strptime(weekly_activity['interval_end'], date_time_format)) - datetime.timedelta( - days=1)) - expected_date = self.date_strip_leading_zeroes(expected_date) - expected = [expected_date, - self.format_number(weekly_activity[at.ANY]), - self.format_number(weekly_activity[at.PLAYED_VIDEO]), - self.format_number(weekly_activity[at.ATTEMPTED_PROBLEM])] - actual = [columns[0].text, columns[1].text, columns[2].text, columns[3].text] - self.assertListEqual(actual, expected) - - for j in range(1, 4): - self.assertIn('text-right', columns[j].get_attribute('class')) - - # Verify CSV button has an href attribute - selector = "a[data-role=engagement-trend-csv]" - self.assertValidHref(selector) - - -class CourseEngagementVideoMixin(CourseEngagementPageTestsMixin): - help_path = 'engagement/Engagement_Video.html' - chart_selector = '#chart-view' - expected_table_heading = 'Video Views' - expected_heading = None - expected_tooltip = None - expected_table_columns = None - - def test_page(self): - super().test_page() - self._test_heading_question() - - def _get_data_update_message(self): - last_updated = datetime.datetime.min - - videos = self.course.videos() - for video in videos: - last_updated = max(last_updated, datetime.datetime.strptime(video['created'], self.api_datetime_format)) - - updated_date_and_time = self.format_last_updated_date_and_time(last_updated) - return ('Video data was last updated {} at {} UTC.').format( - updated_date_and_time['update_date'], updated_date_and_time['update_time']) - - def _test_table(self): - element = self.page.q(css='.section-data-table-title') - self.assertIn(self.expected_table_heading, element[0].text) - self.assertTableColumnHeadingsEqual('div[data-role="data-table"]', self.expected_table_columns) - - def _test_chart(self): - super()._test_chart() - container_selector = '.analytics-chart-container' - element = self.page.q(css=container_selector + ' i') - self.assertEqual(element[0].get_attribute('data-original-title'), self.expected_tooltip) - - def _test_heading_question(self): - element = self.page.q(css='.section-heading') - self.assertEqual(element.text[0], self.expected_heading) - - -@skipUnless(ENABLE_COURSE_API, 'Course API must be enabled to test the video pages.') -class CourseEngagementVideoContentTests(CourseEngagementVideoMixin, WebAppTest): - expected_heading = 'How did learners interact with course videos?' - expected_tooltip = 'Each bar shows the average number of complete and incomplete views for videos in that ' \ - 'section. Click on bars with low totals or a high incomplete rate to drill down and ' \ - 'understand why.' - expected_table_columns = ['Order', 'Section Name', 'Videos', 'Average Complete Views', - 'Average Incomplete Views', 'Completion Percentage'] - - def setUp(self): - super().setUp() - self.page = CourseEngagementVideosContentPage(self.browser) - self.course = self.analytics_api_client.courses(self.page.course_id) - - -@skipUnless(ENABLE_COURSE_API, 'Course API must be enabled to test the video pages.') -class CourseEngagementVideoSectionTests(CourseEngagementVideoMixin, WebAppTest): - expected_heading = 'How did learners interact with videos in this section?' - expected_tooltip = 'Each bar shows the average number of complete and incomplete views for videos in that ' \ - 'subsection. Click on bars with low totals or a high incomplete rate to drill down and ' \ - 'understand why.' - expected_table_columns = ['Order', 'Subsection Name', 'Videos', 'Average Complete Views', - 'Average Incomplete Views', 'Completion Percentage'] - - def setUp(self): - super().setUp() - self.page = CourseEngagementVideoSectionPage(self.browser) - self.course = self.analytics_api_client.courses(self.page.course_id) - - -@skipUnless(ENABLE_COURSE_API, 'Course API must be enabled to test the video pages.') -class CourseEngagementVideoSubsectionTests(CourseEngagementVideoMixin, WebAppTest): - expected_heading = 'How did learners interact with videos in this subsection?' - expected_tooltip = 'Each bar shows the counts of complete and incomplete views for that video. ' \ - 'Click to understand where learners drop off and which parts they replay.' - expected_table_columns = ['Order', 'Video Name', 'Complete Views', 'Incomplete Views', - 'Completion Percentage'] - - def setUp(self): - super().setUp() - self.page = CourseEngagementVideoSubsectionPage(self.browser) - self.course = self.analytics_api_client.courses(self.page.course_id) - - -@skipUnless(ENABLE_COURSE_API, 'Course API must be enabled to test the video pages.') -class CourseEngagementVideoTimelineTests(CourseEngagementVideoMixin, WebAppTest): - expected_heading = 'What were the viewing patterns for this video?' - expected_tooltip = 'The number of learners who watched each segment of the video, and the ' \ - 'number of replays for each segment.' - expected_table_columns = ['Time', 'Unique Viewers', 'Replays'] - expected_table_heading = 'Total Video Views' - - def setUp(self): - super().setUp() - self.page = CourseEngagementVideoTimelinePage(self.browser) - self.course = self.analytics_api_client.courses(self.page.course_id) - - def test_page(self): - super().test_page() - self._test_metrics() - if ENABLE_VIDEO_PREVIEW: - self._test_video_preview() - - def _test_video_preview(self): - preview_selector = '#module-preview' - self.assertFalse(self.page.q(css=preview_selector).visible) - - toggle_element = self.page.q(css='.collapsible-toggle-text') - self.assertTrue(toggle_element.present) - self.assertEqual(toggle_element.text[0], 'Expand Preview') - toggle_element.click() - self.assertEqual(toggle_element.text[0], 'Collapse Preview') - self.page.wait_for_element_visibility(preview_selector, 'Video preview is visible') - - self.fulfill_loading_promise('.module-loading') - self.assertElementHasContent(preview_selector) - - def _test_metrics(self): - module_id = UsageKey.from_string(self.page.video_id).html_id() - video = [video for video in self.course.videos() if video['encoded_module_id'] == module_id][0] - - expected_metrics = [ - { - 'tooltip': 'Estimated percentage of learners who watched the entire video.', - 'data_type': 'watched-percent', - 'metric_value': self.build_display_percentage( - video['users_at_end'], max(video['users_at_start'], video['users_at_end']), - zero_percent_default='0%') - }, - { - 'tooltip': 'Learners who started watching the video.', - 'data_type': 'started-video', - 'metric_value': self.format_number(video['users_at_start']) - }, - { - 'tooltip': 'Learners who watched the video to the end.', - 'data_type': 'finished-video', - 'metric_value': self.format_number(video['users_at_end']) - } - ] - - for expected in expected_metrics: - data_selector = 'data-type={}'.format(expected['data_type']) - self.assertSummaryPointValueEquals(data_selector, expected['metric_value']) - self.assertSummaryTooltipEquals(data_selector, expected['tooltip']) diff --git a/acceptance_tests/test_course_enrollment.py b/acceptance_tests/test_course_enrollment.py deleted file mode 100644 index 744fc6fec..000000000 --- a/acceptance_tests/test_course_enrollment.py +++ /dev/null @@ -1,211 +0,0 @@ -import datetime -from collections import OrderedDict - -from analyticsclient.constants import ( - UNKNOWN_COUNTRY_CODE, - demographics, - enrollment_modes, -) -from bok_choy.web_app_test import WebAppTest - -from acceptance_tests.mixins import CoursePageTestsMixin -from acceptance_tests.pages import ( - CourseEnrollmentActivityPage, - CourseEnrollmentGeographyPage, -) - -_multiprocess_can_split_ = True - - -class CourseEnrollmentActivityTests(CoursePageTestsMixin, WebAppTest): - help_path = 'enrollment/Enrollment_Activity.html' - - def setUp(self): - super().setUp() - self.page = CourseEnrollmentActivityPage(self.browser) - self.course = self.analytics_api_client.courses(self.page.course_id) - - def get_enrollment_data(self): - """ - Returns all historical enrollment data for enrollment count collection. - """ - end_date = datetime.datetime.utcnow() - end_date_string = end_date.strftime(self.analytics_api_client.DATETIME_FORMAT) - return self.course.enrollment('mode', start_date=None, end_date=end_date_string) - - def test_page(self): - super().test_page() - self._test_enrollment_metrics_and_graph() - self._test_enrollment_trend_table() - - def _get_data_update_message(self): - current_enrollment = self.get_enrollment_data()[-1] - last_updated = datetime.datetime.strptime(current_enrollment['created'], self.api_datetime_format) - return 'Enrollment activity data was last updated %(update_date)s at %(update_time)s UTC.' % \ - self.format_last_updated_date_and_time(last_updated) - - def _get_valid_enrollment_modes(self, trends): - valid_modes = set() - invalid_modes = set(enrollment_modes.ALL) - - for datum in trends: - for candidate in list(invalid_modes): - if datum.get(candidate, 0) > 0: - invalid_modes.remove(candidate) - valid_modes.add(candidate) - - if len(invalid_modes) <= 0: - break - - return valid_modes - - def _test_enrollment_metrics_and_graph(self): - """ Verify the graph loads and that the metric tiles display the correct information. """ - - enrollment_data = self.get_enrollment_data() - enrollment = enrollment_data[-1]['count'] - - # Verify the current enrollment metric tile. - tooltip = 'Learners currently enrolled in the course.' - self.assertMetricTileValid('current_enrollment', enrollment, tooltip) - - # Verify the total enrollment change metric tile. - i = 7 - enrollment = enrollment - enrollment_data[-(i + 1)]['count'] - tooltip = 'Net difference in current enrollment in the last week.' - self.assertMetricTileValid('enrollment_change_last_%s_days' % i, enrollment, tooltip) - - valid_modes = self._get_valid_enrollment_modes(enrollment_data) - - if enrollment_modes.VERIFIED in valid_modes: - # Verify the verified enrollment metric tile. - verified_enrollment = enrollment_data[-1][enrollment_modes.VERIFIED] - tooltip = 'Number of currently enrolled learners pursuing a verified certificate of achievement.' - self.assertMetricTileValid('verified_enrollment', verified_enrollment, tooltip) - - # Verify *something* rendered where the graph should be. We cannot easily verify what rendered - self.assertElementHasContent("[data-section=enrollment-basics] #enrollment-trend-view") - - def _test_enrollment_trend_table(self): - """ Verify the information rendered in the table is correct. """ - - enrollment_data = sorted(self.get_enrollment_data(), reverse=True, key=lambda item: item['date']) - - table_selector = 'div[data-role=enrollment-table] table' - headings = ['Date', 'Current Enrollment'] - - valid_modes = self._get_valid_enrollment_modes(enrollment_data) - display_names = OrderedDict([ - (enrollment_modes.HONOR, 'Honor'), - (enrollment_modes.AUDIT, 'Audit'), - (enrollment_modes.VERIFIED, 'Verified'), - (enrollment_modes.PROFESSIONAL, 'Professional'), - (enrollment_modes.CREDIT, 'Verified with Credit') - ]) - for mode, display_name in display_names.items(): - if mode in valid_modes: - headings.append(display_name) - - self.assertTableColumnHeadingsEqual(table_selector, headings) - - rows = self.page.browser.find_elements_by_css_selector('%s tbody tr' % table_selector) - self.assertGreater(len(rows), 0) - - for i, row in enumerate(rows): - columns = row.find_elements_by_css_selector('td') - enrollment = enrollment_data[i] - expected_date = datetime.datetime.strptime(enrollment['date'], self.api_date_format).strftime("%B %d, %Y") - expected_date = self.date_strip_leading_zeroes(expected_date) - - expected = [expected_date, self.format_number(enrollment['count'])] - actual = [columns[0].text, columns[1].text] - self.assertListEqual(actual, expected) - self.assertIn('text-right', columns[1].get_attribute('class')) - - # Verify CSV button has an href attribute - selector = "a[data-role=enrollment-trend-csv]" - self.assertValidHref(selector) - - -class CourseEnrollmentGeographyTests(CoursePageTestsMixin, WebAppTest): - help_path = 'enrollment/Enrollment_Geography.html' - - def setUp(self): - super().setUp() - self.page = CourseEnrollmentGeographyPage(self.browser) - self.course = self.analytics_api_client.courses(self.page.course_id) - self.enrollment_data = sorted(self.course.enrollment(demographics.LOCATION), - key=lambda item: item['count'], reverse=True) - - def test_page(self): - super().test_page() - self._test_enrollment_country_map() - self._test_enrollment_country_table() - self._test_metrics() - - def _get_data_update_message(self): - current_enrollment = self.course.enrollment(demographics.LOCATION)[0] - last_updated = datetime.datetime.strptime(current_enrollment['created'], self.api_datetime_format) - return 'Geographic learner data was last updated %(update_date)s at %(update_time)s UTC.' % \ - self.format_last_updated_date_and_time(last_updated) - - def _test_enrollment_country_map(self): - """ Verify the geolocation map is loaded. """ - - map_selector = "div[data-view=world-map]" - - # Ensure the map is loaded via AJAX - self.fulfill_loading_promise(map_selector) - - # make sure the map section is present - element = self.page.q(css=map_selector) - self.assertTrue(element.present) - - # make sure that the map is present - element = self.page.q(css=map_selector + " svg[class=datamap]") - self.assertTrue(element.present) - - # make sure the legend is present - element = self.page.q(css=map_selector + " svg[class=datamaps-legend]") - self.assertTrue(element.present) - - def _test_enrollment_country_table(self): - """ Verify the geolocation enrollment table is loaded. """ - - table_section_selector = "div[data-role=enrollment-location-table]" - self.assertTable(table_section_selector, ['Country or Region', 'Percent', 'Current Enrollment'], - 'a[data-role=enrollment-location-csv]') - - rows = self.page.browser.find_elements_by_css_selector(f'{table_section_selector} tbody tr') - sum_count = float(sum([datum['count'] for datum in self.enrollment_data])) - - for i, row in enumerate(rows): - columns = row.find_elements_by_css_selector('td') - enrollment = self.enrollment_data[i] - - expected_percent_display = self.build_display_percentage(enrollment['count'], sum_count) - - country_name = enrollment['country']['name'] - if country_name == UNKNOWN_COUNTRY_CODE: - country_name = 'Unknown Country' - # FIXME: because django-countries is different between dashboard and api - elif country_name == 'United States': - country_name = 'United States of America' - - expected = [country_name, expected_percent_display, self.format_number(enrollment['count'])] - actual = [columns[0].text, columns[1].text, columns[2].text] - self.assertListEqual(actual, expected) - self.assertIn('text-right', columns[1].get_attribute('class')) - - def _test_metrics(self): - """ Verify the metrics tiles display the correct information. """ - - enrollment_data = [datum for datum in self.enrollment_data if datum['country']['name'] != 'UNKNOWN'] - self.assertSummaryPointValueEquals('data-stat-type=num-countries', str(len(enrollment_data))) - - for i in range(3): - country = enrollment_data[i]['country']['name'] - # FIXME: because django-countries is different between dashboard and api - if country == 'United States': - country = 'United Sta...' - self.assertSummaryPointValueEquals('data-stat-type=top-country-{}'.format(i + 1), country) diff --git a/acceptance_tests/test_course_enrollment_demographics.py b/acceptance_tests/test_course_enrollment_demographics.py deleted file mode 100644 index 21a69870a..000000000 --- a/acceptance_tests/test_course_enrollment_demographics.py +++ /dev/null @@ -1,221 +0,0 @@ -import datetime - -from analyticsclient.constants import demographics -from analyticsclient.constants import education_levels as EDUCATION_LEVEL -from analyticsclient.constants import genders as GENDER -from bok_choy.web_app_test import WebAppTest - -from acceptance_tests.mixins import CourseDemographicsPageTestsMixin -from acceptance_tests.pages import ( - CourseEnrollmentDemographicsAgePage, - CourseEnrollmentDemographicsEducationPage, - CourseEnrollmentDemographicsGenderPage, -) - -_multiprocess_can_split_ = True - - -class CourseEnrollmentDemographicsAgeTests(CourseDemographicsPageTestsMixin, WebAppTest): - help_path = 'enrollment/Demographics_Age.html' - - demographic_type = demographics.BIRTH_YEAR - table_columns = ['Age', 'Number of Learners', 'Percent of Total'] - - def setUp(self): - super().setUp() - self.page = CourseEnrollmentDemographicsAgePage(self.browser) - self.course = self.analytics_api_client.courses(self.page.course_id) - - self.demographic_data = sorted(self.course.enrollment(self.demographic_type), - key=lambda item: item['count'], reverse=True) - - # Remove items with no birth year - self.demographic_data_without_none = [datum for datum in self.demographic_data if datum['birth_year']] - - def test_page(self): - super().test_page() - self._test_metrics() - - def _calculate_median_age(self, current_year): - demographic_data = self.demographic_data_without_none - - total_enrollment = sum([datum['count'] for datum in demographic_data]) - half_enrollments = total_enrollment * 0.5 - count_enrollments = 0 - - data = sorted(demographic_data, key=lambda item: item['birth_year'], reverse=False) - - for index, datum in enumerate(data): - age = current_year - datum['birth_year'] - count_enrollments += datum['count'] - - if count_enrollments > half_enrollments: - return age - - if count_enrollments == half_enrollments: - if total_enrollment % 2 == 0: - next_age = current_year - data[index + 1]['birth_year'] - return (next_age + age) * 0.5 - return age - - return None - - def _count_ages(self, current_year, min_age, max_age): - """ - Returns the number of enrollments between min_age (inclusive) and - max_age (exclusive). - """ - filtered_ages = self.demographic_data_without_none - - if min_age: - filtered_ages = ([datum for datum in filtered_ages - if (current_year - datum['birth_year']) >= min_age]) - if max_age: - filtered_ages = ([datum for datum in filtered_ages - if (current_year - datum['birth_year']) < max_age]) - - return sum([datum['count'] for datum in filtered_ages]) - - def _test_metrics(self): - current_year = datetime.date.today().year - total = float(sum([datum['count'] for datum in self.demographic_data_without_none])) - age_metrics = [ - { - 'stat_type': 'median_age', - 'value': self._calculate_median_age(current_year) - }, - { - 'stat_type': 'enrollment_age_under_25', - 'value': self.build_display_percentage(self._count_ages(current_year, None, 26), total) - }, - { - 'stat_type': 'enrollment_age_between_26_40', - 'value': self.build_display_percentage(self._count_ages(current_year, 26, 41), total) - }, - { - 'stat_type': 'enrollment_age_over_40', - 'value': self.build_display_percentage(self._count_ages(current_year, 41, None), total) - } - ] - - for metric in age_metrics: - selector = 'data-stat-type={}'.format(metric['stat_type']) - self.assertSummaryPointValueEquals(selector, str(metric['value'])) - - def _test_table_row(self, datum, column, sum_count): - expected_percent_display = self.build_display_percentage(datum['count'], sum_count) - # it's difficult to test the actual age in the case of a tie, so leave out (unit tests should catch this) - expected = [self.format_number(datum['count']), expected_percent_display] - actual = [column[1].text, column[2].text] - self.assertListEqual(actual, expected) - self.assertIn('text-right', column[1].get_attribute('class')) - self.assertIn('text-right', column[2].get_attribute('class')) - - -class CourseEnrollmentDemographicsGenderTests(CourseDemographicsPageTestsMixin, WebAppTest): - help_path = 'enrollment/Demographics_Gender.html' - - demographic_type = demographics.GENDER - table_columns = ['Date', 'Current Enrollment', 'Female', 'Male', 'Other', 'Not Reported'] - - def setUp(self): - super().setUp() - self.page = CourseEnrollmentDemographicsGenderPage(self.browser) - self.course = self.analytics_api_client.courses(self.page.course_id) - - end_date = datetime.datetime.utcnow() - end_date_string = end_date.strftime(self.analytics_api_client.DATETIME_FORMAT) - response = self.course.enrollment(self.demographic_type, end_date=end_date_string) - self.demographic_data = sorted(response, key=lambda x: datetime.datetime.strptime(x['date'], '%Y-%m-%d'), - reverse=True) - - def _test_table_row(self, datum, column, sum_count): - genders = [GENDER.FEMALE, GENDER.MALE, GENDER.OTHER, GENDER.UNKNOWN] - expected_date = datetime.datetime.strptime(datum['date'], self.api_date_format).strftime("%B %d, %Y") - expected_date = self.date_strip_leading_zeroes(expected_date) - gender_total = sum([value for key, value in datum.items() if value and key in genders]) - - expected = [expected_date, self.format_number(gender_total)] - for gender in genders: - expected.append(self.format_number(datum.get(gender, 0) or 0)) - - actual = [] - for i in range(6): - actual.append(column[i].text) - - self.assertListEqual(actual, expected) - - for i in range(1, 6): - self.assertIn('text-right', column[i].get_attribute('class')) - - -class CourseEnrollmentDemographicsEducationTests(CourseDemographicsPageTestsMixin, WebAppTest): - EDUCATION_NAMES = { - EDUCATION_LEVEL.NONE: 'None', - EDUCATION_LEVEL.OTHER: 'Other', - EDUCATION_LEVEL.PRIMARY: 'Primary', - EDUCATION_LEVEL.JUNIOR_SECONDARY: 'Middle', - EDUCATION_LEVEL.SECONDARY: 'Secondary', - EDUCATION_LEVEL.ASSOCIATES: 'Associate', - EDUCATION_LEVEL.BACHELORS: "Bachelor's", - EDUCATION_LEVEL.MASTERS: "Master's", - EDUCATION_LEVEL.DOCTORATE: 'Doctorate', - None: 'Unknown' - } - - help_path = 'enrollment/Demographics_Education.html' - - demographic_type = demographics.EDUCATION - table_columns = ['Educational Background', 'Number of Learners'] - - def setUp(self): - super().setUp() - self.page = CourseEnrollmentDemographicsEducationPage(self.browser) - self.course = self.analytics_api_client.courses(self.page.course_id) - self.demographic_data = sorted(self.course.enrollment(self.demographic_type), - key=lambda item: item['count'], reverse=True) - - def test_page(self): - super().test_page() - self._test_metrics() - - def _test_metrics(self): - # The total should not include users who did not provide an education level - total = sum([datum['count'] for datum in self.demographic_data if datum['education_level']]) - - education_groups = [ - { - 'levels': ['primary', 'junior_secondary', 'secondary'], - 'stat_type': 'education_high_school_or_less_enrollment', - 'tooltip': 'The percentage of learners who selected Secondary/high school, Junior secondary/junior ' - 'high/middle school, or Elementary/primary school as their highest level of ' - 'education completed.' - }, - { - 'levels': ['associates', 'bachelors'], - 'stat_type': 'education_college_enrollment', - 'tooltip': "The percentage of learners who selected Bachelor's degree or Associate degree as their " - "highest level of education completed." - }, - { - 'levels': ['masters', 'doctorate'], - 'stat_type': 'education_advanced_enrollment', - 'tooltip': "The percentage of learners who selected Doctorate or Master's or professional degree as " - "their highest level of education completed." - } - ] - - for group in education_groups: - selector = 'data-stat-type={}'.format(group['stat_type']) - filtered_group = ([education for education in self.demographic_data - if education['education_level'] in group['levels']]) - group_total = float(sum([datum['count'] for datum in filtered_group])) - expected_percent_display = self.build_display_percentage(group_total, total) - self.assertSummaryPointValueEquals(selector, expected_percent_display) - self.assertSummaryTooltipEquals(selector, group['tooltip']) - - def _test_table_row(self, datum, column, sum_count): - expected = [self.EDUCATION_NAMES[datum['education_level']], self.format_number(datum['count'])] - actual = [column[0].text, column[1].text] - self.assertListEqual(actual, expected) - self.assertIn('text-right', column[1].get_attribute('class')) diff --git a/acceptance_tests/test_course_index.py b/acceptance_tests/test_course_index.py deleted file mode 100644 index 0b8e463c3..000000000 --- a/acceptance_tests/test_course_index.py +++ /dev/null @@ -1,335 +0,0 @@ -import requests -from bok_choy.promise import EmptyPromise -from bok_choy.web_app_test import WebAppTest -from selenium.webdriver.common.keys import Keys - -from acceptance_tests import ( - ENABLE_COURSE_LIST_FILTERS, - ENABLE_COURSE_LIST_PASSING, - TEST_COURSE_ID, -) -from acceptance_tests.mixins import ( - AnalyticsApiClientMixin, - AnalyticsDashboardWebAppTestMixin, -) -from acceptance_tests.pages import CourseIndexPage - -_multiprocess_can_split_ = True - - -class CourseIndexTests(AnalyticsApiClientMixin, AnalyticsDashboardWebAppTestMixin, WebAppTest): - test_skip_link_url = False - - def setUp(self): - super().setUp() - self.page = CourseIndexPage(self.browser) - self.maxDiff = None - self.course_summaries = self.analytics_api_client.course_summaries() - - def test_page(self): - super().test_page() - self._test_course_list() - self._test_search() - self._test_clear_input() - self._test_clear_active_filter() - self._test_clear_all_filters() - self._test_summary_metrics() - if ENABLE_COURSE_LIST_FILTERS: - self._test_filters() - self._test_download_csv() - - def _test_course_list(self): - """ - Course list should contain a link to the test course. - """ - # text after the new line is only visible to screen readers - columns = [ - 'Course Name \nsort ascending', - 'Start Date \nclick to sort', - 'End Date \nclick to sort', - 'Total Enrollment \nclick to sort', - 'Current Enrollment \nclick to sort', - 'Change Last Week \nclick to sort', - 'Verified Enrollment \nclick to sort', - ] - if ENABLE_COURSE_LIST_PASSING: - columns.append('Passing Learners \nclick to sort') - - self.assertTable('.course-list-table', columns) - - # Validate that we have a list of courses - course_ids = self.page.q(css='.course-list .course-id') - self.assertTrue(course_ids.present) - - # The element should list the test course id. - self.assertIn(TEST_COURSE_ID, course_ids.text) - - # Validate the course links - course_links = self.page.q(css='.course-list .course-name-cell a').attrs('href') - - for link, course_id in zip(course_links, course_ids): - self.assertTrue(link.endswith(f'/courses/{course_id.text}')) - - def _test_search(self): - """ - Tests that a user can perform a search to filter the course list. - """ - # Search bar is present - search_bar = self.page.q(css='#search-course-list') - self.assertTrue(search_bar.present) - - # Clear any existing search first - self.clear_all_filters() - # Make sure all courses show before performing a search - self.check_cleared() - - # Perform search - search_input = self.driver.find_element_by_id('search-course-list') - search_input.send_keys(Keys.CONTROL, 'a') # in-case there is a previous search - search_input.send_keys('search') - # Check that clear icon shows up - clear = self.page.q(css='button.clear') - self.assertTrue(clear.present) - search_input.send_keys(Keys.ENTER) - - # Search bar contains query - search_input = self.page.q(css='#search-course-list') - self.assertEqual(search_input.attrs('value'), ['search']) - - # Check that active filters show search value - EmptyPromise( - lambda: self.page.q(css='ul.active-filters').present, - "Search performed" - ).fulfill() - active_filters = self.page.q(css='ul.active-filters') - self.assertTrue(active_filters.present) - search_active_filter = self.page.q(css='ul.active-filters li.filter-text_search') - self.assertTrue('search' in search_active_filter.text[0]) - - # No courses match search query, so alert should show - course_ids = self.page.q(css='.course-list .course-id') - self.assertFalse(course_ids.present) - - alert = self.page.q(css='.list-main .alert-information') - self.assertTrue(alert.present) - self.assertTrue('No courses matched your criteria' in alert.text[0]) - - def _test_filter(self, filter_id, display_name, course_in_filter=False, clear_existing_filters=True): - """ - Tests that a user can check a filter option to filter the course list. - """ - # Filter is present - filter_box = self.page.q(css='#' + filter_id) - self.assertTrue(filter_box.present, "missing filter '{display_name}'".format(display_name=display_name)) - - if clear_existing_filters: - # Clear any existing filter first - self.clear_all_filters() - # Make sure all courses show before performing a filter - self.check_cleared() - - # Perform filter - filter_box.click() - - # Check that active filters show search value - EmptyPromise( - lambda: self.page.q(css='ul.active-filters').present, - "Search performed" - ).fulfill() - active_filters = self.page.q(css='ul.active-filters') - self.assertTrue(active_filters.present) - self.assertTrue(display_name in active_filters.text[0]) - - course_ids = self.page.q(css='.course-list .course-id') - num_results = self.page.q(css='.course-list .course-list-num-results .num-results') - num_results_sr = self.page.q(css='.course-list .num-results-sr') - if course_in_filter: - self.assertTrue(course_ids.present) - self.assertTrue('1' in num_results.text[0]) - self.assertTrue('1' in num_results_sr.text[0]) - else: - # No courses match filter, so alert should show - self.assertFalse(course_ids.present) - alert = self.page.q(css='.list-main .alert-information') - self.assertTrue(alert.present) - self.assertTrue('No courses matched your criteria' in alert.text[0]) - self.assertTrue('0' in num_results.text[0]) - self.assertTrue('0' in num_results_sr.text[0]) - - def clear_all_filters(self): - # Check that the clear button is present. AKA a search/filter has been made. - clear_all_filters = self.page.q(css='ul.active-filters button.action-clear-all-filters') - if clear_all_filters.present: - # Press clear search input - clear_all_filters.first.click() - - def check_cleared(self): - EmptyPromise( - lambda: (self.driver.find_element_by_id('search-course-list').get_attribute('value') != 'search'), - "Search input cleared" - ).fulfill() - - # Search bar no longer contains query - search_input = self.driver.find_element_by_id('search-course-list') - self.assertNotEqual(search_input.get_attribute('value'), 'search') - - # Check that active filters are hidden - EmptyPromise( - lambda: not self.page.q(css='ul.active-filters').present, - "Active filters hidden" - ).fulfill() - active_filters = self.page.q(css='ul.active-filters') - self.assertFalse(active_filters.present) - - # Now that search is gone, the list should show with the test course - EmptyPromise( - lambda: (self.page.q(css='.course-list .course-id').present), - "Table unfiltered" - ).fulfill() - course_ids = self.page.q(css='.course-list .course-id') - self.assertTrue(course_ids.present) - self.assertIn(TEST_COURSE_ID, course_ids.text) - - def _test_clear_input(self): - """ - Tests that a user can clear the search filter to unfilter the results. - """ - self._test_search() # populate a search if it hasn't already - - # Check that clear icon shows up - clear = self.page.q(css='button.clear') - self.assertTrue(clear.present) - - # Press clear search input - clear.first.click() - - self.check_cleared() - - def _test_clear_active_filter(self): - """ - Tests that a user can clear the search filter to unfilter the results using active filters clear controls. - """ - self._test_search() # populate a search if it hasn't already - - search_active_filter = self.page.q(css='ul.active-filters li.filter-text_search button') - - # Press the active filter button (which should clear that filter) - search_active_filter.first.click() - - self.check_cleared() - - def _test_clear_all_filters(self): - """ - Tests that a user can clear the search filter to unfilter the results using clear all filters control. - """ - self._test_search() # populate a search if it hasn't already - - clear_all_filters = self.page.q(css='ul.active-filters button.action-clear-all-filters') - - # Press the "Clear" button link which should clear all filters including the search - clear_all_filters.first.click() - - self.check_cleared() - - def _test_individual_filters(self): - """ - Tests checking each option under each filter set. - - The test course will only be displayed under "Upcoming" or "self_paced" filters. - """ - # maps id of filter in DOM to display name shown in active filters - filters = { - "Archived": "Archived", - "Current": "Current", - "Upcoming": "Upcoming", - "unknown": "Unknown", - "instructor_paced": "Instructor-Paced", - "self_paced": "Self-Paced", - } - course_in_filters = ['Upcoming', 'self_paced'] - for filter_id, display_name in filters.items(): - self._test_filter(filter_id, display_name, - course_in_filter=filter_id in course_in_filters) - - def _test_multiple_filters(self, filter_sequence): - """ - Tests checking multiple filter options together and whether the course is shown after each filter application. - - filter_sequence should be a list of tuples where each element, by index, is: - 0. the filter id to apply - 1. the filter display name - 2. boolean for whether the test course is shown in the list after the filter is applied. - """ - for index, filter_data in enumerate(filter_sequence): - filter_id = filter_data[0] - name = filter_data[1] - course_shown = filter_data[2] - first_filter = index == 0 - self._test_filter(filter_id, name, course_in_filter=course_shown, clear_existing_filters=first_filter) - - def _test_filters(self): - self._test_individual_filters() - - # Filters ORed within a set - self._test_multiple_filters([ - ('Archived', 'Archived', False), - ('Upcoming', 'Upcoming', True), - ('Current', 'Current', True), - ('unknown', 'Unknown', True), - ]) - - # Filters ANDed between sets - self._test_multiple_filters([ - ('Upcoming', 'Upcoming', True), - ('instructor_paced', 'Instructor-Paced', False), - ('self_paced', 'Self-Paced', True), - ('Demo_Program', 'Demo Program', True), - ]) - - def _test_download_csv(self): - # Download button is present - download_button = self.page.q(css='a.action-download-data') - self.assertTrue(download_button.present) - - link = download_button.attrs('href')[0] - - # Steal the cookies from the logged-in firefox browser and use them in a python-initiated request - 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(link, **kwargs) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.headers['content-type'], 'text/csv') - - def _test_summary_metrics(self): - """Verify that the metric tiles display the correct information. - - Test data must include at least one course with at least one verified enrollment which the - test user can access. - """ - course_summaries = self.course_summaries.course_summaries(course_ids=[TEST_COURSE_ID]) - current_enrollment = course_summaries[0]['count'] - total_enrollment = course_summaries[0]['cumulative_count'] - i = 7 - count_change_i_days = course_summaries[0]['count_change_%s_days' % i] - verified_enrollment = course_summaries[0]['enrollment_modes']['verified']['count'] - masters_enrollment = course_summaries[0]['enrollment_modes']['masters']['count'] - - tooltip = 'Current enrollments across all of your courses.' - self.assertMetricTileValid('current_enrollment', current_enrollment, tooltip) - - tooltip = 'Total enrollments across all of your courses.' - self.assertMetricTileValid('total_enrollment', total_enrollment, tooltip) - - tooltip = 'Total change in enrollment last week across all of your courses.' - self.assertMetricTileValid('enrollment_change_%s_days' % i, count_change_i_days, tooltip) - - tooltip = 'Verified enrollments across all of your courses.' - self.assertMetricTileValid('verified_enrollment', verified_enrollment, tooltip) - - tooltip = "Master's enrollments across all of your courses." - self.assertMetricTileValid('masters_enrollment', masters_enrollment, tooltip) diff --git a/acceptance_tests/test_course_performance.py b/acceptance_tests/test_course_performance.py deleted file mode 100644 index bf7632789..000000000 --- a/acceptance_tests/test_course_performance.py +++ /dev/null @@ -1,438 +0,0 @@ -import datetime -from unittest import skipUnless - -from bok_choy.web_app_test import WebAppTest - -from acceptance_tests import ENABLE_COURSE_API -from acceptance_tests.mixins import CoursePageTestsMixin -from acceptance_tests.pages import ( - CoursePerformanceAnswerDistributionPage, - CoursePerformanceAssignmentPage, - CoursePerformanceGradedContentByTypePage, - CoursePerformanceGradedContentPage, - CoursePerformanceUngradedAnswerDistributionPage, - CoursePerformanceUngradedContentPage, - CoursePerformanceUngradedSectionPage, - CoursePerformanceUngradedSubsectionPage, -) -from common.course_structure import CourseStructure - -_multiprocess_can_split_ = True - - -class CoursePerformancePageTestsMixin(CoursePageTestsMixin): - - help_path = 'performance/Performance_Answers.html' - table_selector = 'div[data-role="data-table"]' - - def test_page(self): - super().test_page() - self._test_chart() - self._test_table() - - def _test_chart(self): - chart_selector = '#chart-view' - self.fulfill_loading_promise(chart_selector) - self.assertElementHasContent(chart_selector) - - def _test_table(self): - raise NotImplementedError - - def _get_data_update_message(self): - problems = self.course.problems() - last_updated = datetime.datetime.min - - for problem in problems: - last_updated = max(last_updated, datetime.datetime.strptime(problem['created'], self.api_datetime_format)) - - updated_date_and_time = self.format_last_updated_date_and_time(last_updated) - return ('Problem submission data was last updated {} at {} UTC.').format( - updated_date_and_time['update_date'], updated_date_and_time['update_time']) - - def _format_number_or_hyphen(self, value): - if value: - return self.format_number(value) - return '-' - - def _build_display_percentage_or_hyphen(self, correct, total): - if correct: - return self.build_display_percentage(correct, total) - return '-' - - def _get_problems_dict(self): - # Retrieve the submissions from the Analytics Data API and create a lookup table. - problems = self.course.problems() - return {problem['module_id']: problem for problem in problems} - - def _get_assignments(self, assignment_type=None): - blocks = self.course_api_client.blocks().get() - assignments = CourseStructure.course_structure_to_assignments(blocks, graded=True, - assignment_type=assignment_type) - - return self._build_submissions(assignments, self._get_problems_dict()) - - def _get_sections(self): - blocks = self.course_api_client.blocks().get() - sections = CourseStructure.course_structure_to_sections(blocks, 'problem', graded=False) - problems = self._get_problems_dict() - for section in sections: - self._build_submissions(section['children'], problems) - return self._build_submissions(sections, problems) - - def _find_child_block(self, blocks, child_id): - for block in blocks: - if block['id'] == child_id: - return block - return None - - def _build_submissions(self, blocks, problems): - # Sum the submission counts - for parent_block in blocks: - total = 0 - correct = 0 - num_modules = 0 - - for child_block in parent_block['children']: - submission_entry = problems.get(child_block['id'], None) - - if submission_entry: - total += submission_entry['total_submissions'] - correct += submission_entry['correct_submissions'] - num_modules += 1 - - child_block.update({ - 'total_submissions': submission_entry['total_submissions'], - 'correct_submissions': submission_entry['correct_submissions'], - 'num_modules': 1 - }) - elif 'total_submissions' in child_block and 'correct_submissions' in child_block: - total += child_block['total_submissions'] - correct += child_block['correct_submissions'] - num_modules += child_block['num_modules'] - else: - num_modules += 1 - child_block.update({ - 'total_submissions': 0, - 'correct_submissions': 0, - 'num_modules': 1, - }) - - parent_block.update({ - 'total_submissions': total, - 'correct_submissions': correct, - 'num_modules': num_modules - }) - - return blocks - - def assertBlockRows(self, blocks): - table = self.page.browser.find_element_by_css_selector(self.table_selector) - rows = table.find_elements_by_css_selector('tbody tr') - self.assertEqual(len(rows), len(blocks)) - - for index, row in enumerate(rows): - block = blocks[index] - cols = row.find_elements_by_css_selector('td') - self.assertRowTextEquals(cols, self.get_expected_row(index, block)) - - def get_expected_row(self, index, block): - return [str(index + 1), block['name']] - - -# pylint: disable=abstract-method -class CoursePerformanceAveragedTableMixin(CoursePerformancePageTestsMixin): - - def get_expected_row(self, index, block): - row = super().get_expected_row(index, block) - num_modules_denominator = float(block.get('num_modules', 1)) - row += [ - str(self._format_number_or_hyphen(block.get('num_modules', 0))), - str(self._format_number_or_hyphen( - block['correct_submissions'] / num_modules_denominator - if num_modules_denominator else None - )), - str(self._format_number_or_hyphen( - (block['total_submissions'] - block['correct_submissions']) / num_modules_denominator - if num_modules_denominator else None - )), - str(self._format_number_or_hyphen( - block['total_submissions'] / num_modules_denominator - if num_modules_denominator else None - )), - str(self._build_display_percentage_or_hyphen( - block['correct_submissions'], - block['total_submissions'] - )) - ] - return row - - -# pylint: disable=abstract-method -class CoursePerformanceModuleTableMixin(CoursePerformancePageTestsMixin): - - def get_expected_row(self, index, block): - row = super().get_expected_row(index, block) - row += [ - str(self._format_number_or_hyphen(block['correct_submissions'])), - str(self._format_number_or_hyphen( - block['total_submissions'] - block['correct_submissions'])), - str(self._format_number_or_hyphen(block['total_submissions'])), - str(self._build_display_percentage_or_hyphen( - block['correct_submissions'], - block['total_submissions'] - )) - ] - return row - - -@skipUnless(ENABLE_COURSE_API, 'Course API must be enabled to test the graded content page.') -class CoursePerformanceGradedContentTests(CoursePerformancePageTestsMixin, WebAppTest): - """ - Tests for the course graded content page. - """ - - def _test_data_update_message(self): - # There is no data update message displayed on this page. - pass - - def _get_grading_policy(self): - """ - Retrieve the course's grading policy from the Course API. - """ - policy = self.course_api_client.grading_policies(self.page.course_id).get() - - for item in policy: - weight = item['weight'] - item['weight_as_percentage'] = '{:.0f}%'.format(weight * 100) - - return policy - - def setUp(self): - super().setUp() - self.page = CoursePerformanceGradedContentPage(self.browser) - self.grading_policy = self._get_grading_policy() - - def _test_chart(self): - """ - Test the assignment types display and values. - """ - elements = self.page.browser.find_elements_by_css_selector('.grading-policy .policy-item') - - for index, element in enumerate(elements): - grading_policy = self.grading_policy[index] - assignment_type = grading_policy['assignment_type'] - - # Verify the URL to view the assignments is correct. - actual = element.find_element_by_css_selector('a').get_attribute('href') - expected = f'{self.page.page_url}{assignment_type}/' - self.assertEqual(actual, expected) - - # Verify the displayed weight - actual = element.find_element_by_css_selector('.weight').text - expected = grading_policy['weight_as_percentage'] - self.assertEqual(actual, expected) - - # Verify the weighted column sizes - style = element.get_attribute('style') - width = 'width: {}'.format(grading_policy['weight_as_percentage']) - self.assertIn(width, style) - - # Verify the printed assignment type - actual = element.find_element_by_css_selector('.type').text - self.assertEqual(actual, assignment_type) - - def _test_table(self): - pass - - -@skipUnless(ENABLE_COURSE_API, 'Course API must be enabled to test the course assignment type detail page.') -class CoursePerformanceGradedContentByTypeTests(CoursePerformanceAveragedTableMixin, WebAppTest): - """ - Tests for the course assignment type detail page. - """ - - def setUp(self): - super().setUp() - self.page = CoursePerformanceGradedContentByTypePage(self.browser) - self.assignment_type = self.page.assignment_type - self.course = self.analytics_api_client.courses(self.page.course_id) - self.assignments = self._get_assignments(self.assignment_type) - - def _test_table(self): - self.assertTableColumnHeadingsEqual(self.table_selector, - ['Order', 'Assignment Name', 'Problems', - 'Average Correct', 'Average Incorrect', - 'Average Submissions Per Problem', 'Percentage Correct']) - self.assertBlockRows(self.assignments) - - -@skipUnless(ENABLE_COURSE_API, 'Course API must be enabled to test the course assignment detail page.') -class CoursePerformanceAssignmentTests(CoursePerformanceModuleTableMixin, WebAppTest): - """ - Tests for the course assignment detail page. - """ - - def _get_assignment(self): - assignments = self._get_assignments() - for assignment in assignments: - if assignment['id'] == self.assignment_id: - return assignment - - raise AttributeError('Assignment not found!') - - def setUp(self): - super().setUp() - self.page = CoursePerformanceAssignmentPage(self.browser) - self.assignment_id = self.page.assignment_id - self.course = self.analytics_api_client.courses(self.page.course_id) - self.assignment = self._get_assignment() - - def _test_table(self): - # Check the column headings - self.assertTableColumnHeadingsEqual(self.table_selector, [ - 'Order', 'Problem Name', 'Correct', 'Incorrect', 'Total', 'Percentage Correct']) - self.assertBlockRows(self.assignment['children']) - - -class CoursePerformanceAnswerDistributionMixin(CoursePerformancePageTestsMixin): - - course = None - module = None - answer_distribution = None - - def setUp(self): - super().setUp() - self.page = self.get_page() - self.course = self.analytics_api_client.courses(self.page.course_id) - self.module = self.analytics_api_client.modules(self.page.course_id, self.page.problem_id) - api_response = self.module.answer_distribution() - data = [i for i in api_response if i['part_id'] == self.page.part_id] - self.answer_distribution = sorted(data, key=lambda a: a['last_response_count'], reverse=True) - - def get_page(self): - raise NotImplementedError - - def test_page(self): - super().test_page() - self._test_heading_question() - self._test_problem_description() - - def _test_heading_question(self): - element = self.page.q(css='.section-heading') - self.assertEqual(element.text[0], 'How did learners answer this problem?') - - def _test_problem_description(self): - section_selector = '.module-description' - - element = self.page.q(css=section_selector + ' p') - self.assertIsNotNone(element[0]) - - self.assertValidHref(section_selector + ' a') - - def _test_chart(self): - chart_selector = '#performance-chart-view' - self.fulfill_loading_promise(chart_selector) - self.assertElementHasContent(chart_selector) - - element = self.page.q(css='#distQuestionsMenu') - if element: - self.assertIn('Submissions for Part', element[0].text) - else: - element = self.page.q(css='.chart-info') - self.assertIn('Submissions', element[0].text) - - container_selector = '.analytics-chart-container' - element = self.page.q(css=container_selector + ' i') - expected_tooltip = 'This chart shows the most common answers submitted by learners, ordered by frequency.' - self.assertEqual(element[0].get_attribute('data-original-title'), expected_tooltip) - - def _test_table(self): - table_section_selector = "div[data-role=performance-table]" - self.assertTable(table_section_selector, ['Answer', 'Correct', 'Submission Count'], - 'a[data-role=performance-csv]') - - rows = self.page.browser.find_elements_by_css_selector(f'{table_section_selector} tbody tr') - - value_field = 'answer_value' - - for i, row in enumerate(rows): - answer = self.answer_distribution[i] - columns = row.find_elements_by_css_selector('td') - - actual = [] - for col in columns: - actual.append(col.text) - - expected = [answer[value_field] if answer[value_field] else '(empty)'] - correct = '-' - if answer['correct']: - correct = 'Correct' - expected.append(correct) - expected.append(self.format_number(answer['last_response_count'])) - - self.assertListEqual(actual, expected) - self.assertIn('text-right', columns[2].get_attribute('class')) - - -@skipUnless(ENABLE_COURSE_API, 'Course API must be enabled to test the answer distribution page.') -class CoursePerformanceAnswerDistributionTests(CoursePerformanceAnswerDistributionMixin, WebAppTest): - - def get_page(self): - return CoursePerformanceAnswerDistributionPage(self.browser) - - -@skipUnless(ENABLE_COURSE_API, 'Course API must be enabled to test the answer distribution page.') -class CoursePerformanceUngradedAnswerDistributionTests(CoursePerformanceAnswerDistributionMixin, WebAppTest): - - def get_page(self): - return CoursePerformanceUngradedAnswerDistributionPage(self.browser) - - -@skipUnless(ENABLE_COURSE_API, 'Course API must be enabled to test ungraded content.') -class CoursePerformanceUngradedContentTests(CoursePerformanceAveragedTableMixin, WebAppTest): - - def setUp(self): - super().setUp() - self.page = CoursePerformanceUngradedContentPage(self.browser) - self.course = self.analytics_api_client.courses(self.page.course_id) - self.sections = self._get_sections() - - def _test_table(self): - self.assertTableColumnHeadingsEqual(self.table_selector, - ['Order', 'Section Name', 'Problems', 'Average Correct', - 'Average Incorrect', 'Average Submissions Per Problem', - 'Percentage Correct']) - self.assertBlockRows(self.sections) - - -@skipUnless(ENABLE_COURSE_API, 'Course API must be enabled to test ungraded content.') -class CoursePerformanceUngradedSectionTests(CoursePerformanceAveragedTableMixin, WebAppTest): - - def setUp(self): - super().setUp() - self.page = CoursePerformanceUngradedSectionPage(self.browser) - self.course = self.analytics_api_client.courses(self.page.course_id) - self.section = self._find_child_block(self._get_sections(), self.page.section_id) - - def _test_table(self): - self.assertTableColumnHeadingsEqual(self.table_selector, - ['Order', 'Subsection Name', 'Problems', 'Average Correct', - 'Average Incorrect', 'Average Submissions Per Problem', - 'Percentage Correct']) - self.assertBlockRows(self.section['children']) - - -@skipUnless(ENABLE_COURSE_API, 'Course API must be enabled to test ungraded content.') -class CoursePerformanceUngradedSubsectionTests(CoursePerformanceModuleTableMixin, WebAppTest): - - def setUp(self): - super().setUp() - self.page = CoursePerformanceUngradedSubsectionPage(self.browser) - self.course = self.analytics_api_client.courses(self.page.course_id) - subsections = self._find_child_block(self._get_sections(), self.page.section_id)['children'] - self.problems = self._find_child_block(subsections, self.page.subsection_id)['children'] - - def _test_table(self): - self.assertTableColumnHeadingsEqual(self.table_selector, ['Order', 'Problem Name', 'Correct', - 'Incorrect', 'Total', 'Percentage Correct']) - self.assertBlockRows(self.problems) diff --git a/acceptance_tests/test_error_pages.py b/acceptance_tests/test_error_pages.py deleted file mode 100644 index c61aee0ca..000000000 --- a/acceptance_tests/test_error_pages.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest import skipUnless - -from bok_choy.web_app_test import WebAppTest - -from acceptance_tests import ( - APPLICATION_NAME, - ENABLE_ERROR_PAGE_TESTS, - PLATFORM_NAME, - SUPPORT_EMAIL, -) -from acceptance_tests.pages import ( - AccessDeniedErrorPage, - NotFoundErrorPage, - ServerErrorPage, - ServiceUnavailableErrorPage, -) - - -@skipUnless(ENABLE_ERROR_PAGE_TESTS, 'Error page tests are not enabled.') -class ErrorPagesTests(WebAppTest): - error_page_classes = [ServerErrorPage, NotFoundErrorPage, AccessDeniedErrorPage, ServiceUnavailableErrorPage] - - def test_valid_pages(self): - for page_class in self.error_page_classes: - page = page_class(self.browser) - - # Visit the page - page.visit() - - # Check the title - expected = f'{page.error_title} | {PLATFORM_NAME} {APPLICATION_NAME}' - self.assertEqual(expected, self.browser.title) - - # Check the support link - element = page.q(css='a[data-role=support-email]') - self.assertTrue(element.present) - href = element.attrs('href')[0] - self.assertEqual(href, f'mailto:{SUPPORT_EMAIL}') diff --git a/acceptance_tests/test_landing.py b/acceptance_tests/test_landing.py deleted file mode 100644 index 015e71d86..000000000 --- a/acceptance_tests/test_landing.py +++ /dev/null @@ -1,81 +0,0 @@ -from bok_choy.web_app_test import WebAppTest - -from acceptance_tests import ( - OPEN_SOURCE_URL, - RESEARCH_URL, - SHOW_LANDING_RESEARCH, - SUPPORT_EMAIL, -) -from acceptance_tests.mixins import ( - FooterLegalMixin, - LoginMixin, - LogoutMixin, - PageTestMixin, -) -from acceptance_tests.pages import LandingPage - -_multiprocess_can_split_ = False - - -class LandingTests(PageTestMixin, LoginMixin, LogoutMixin, FooterLegalMixin, WebAppTest): - def setUp(self): - super().setUp() - self.page = LandingPage(self.browser) - - def test_page(self): - super().test_page() - # landing page will not be viewable by logged in users - self.assertFalse(self.page.is_browser_on_page()) - - # landing page only accessible to logged out users - self.logout() - # logout page will redirect to the landing page - self._test_lenses() - self._test_audience_messages() - - def _test_lenses(self): - question_elements = self.page.q(css='.lens-question') - self.assertTrue(question_elements.present) - - expected_questions = ['Who are my learners?', 'What are learners engaging with in my course?', - 'How well is my content supporting learners?'] - num_lenses = len(expected_questions) - self.assertEqual(len(question_elements), num_lenses) - - for i in range(num_lenses): - self.assertEqual(question_elements[i].text, expected_questions[i]) - - summary_elements = self.page.q(css='.lens-summary') - self.assertTrue(summary_elements.present) - self.assertTrue(len(summary_elements), num_lenses) - - lens_icon_elements = self.page.q(css='.lens-summary h1 span') - self.assertTrue(lens_icon_elements.present) - self.assertTrue(len(lens_icon_elements), num_lenses) - - # make sure that the icons are hidden from screen readers - for i in range(num_lenses): - self.assertEqual(lens_icon_elements.attrs('aria-hidden')[i], 'true') - - def _test_audience_messages(self): - element = self.page.q(css='.audience-message') - self.assertTrue(element.present) - - expected_headers = ['Join the Open Source Community', 'Need Help?'] - if SHOW_LANDING_RESEARCH: - expected_headers.insert(1, 'Research at edX') - num_actions = len(expected_headers) - - module_selector = '.audience-message-module' - header_elements = self.page.q(css=module_selector + ' h1') - self.assertTrue(header_elements.present) - self.assertEqual(len(header_elements), num_actions) - - action_link_elements = self.page.q(css=module_selector + ' a') - self.assertTrue(action_link_elements.present) - self.assertEqual(len(action_link_elements), num_actions) - - expected_links = [OPEN_SOURCE_URL, RESEARCH_URL, f'mailto:{SUPPORT_EMAIL}'] - for i in range(num_actions): - self.assertEqual(header_elements[i].text, expected_headers[i]) - self.assertEqual(action_link_elements.attrs('href')[i], expected_links[i]) diff --git a/requirements/base.txt b/requirements/base.txt index 4643c00dd..39ec46b92 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -18,7 +18,7 @@ cffi==1.16.0 # via # cryptography # pynacl -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 # via requests click==8.1.7 # via @@ -34,7 +34,7 @@ defusedxml==0.8.0rc2 # via # python3-openid # social-auth-core -django==4.2.6 +django==4.2.7 # via # -c requirements/constraints.txt # -r requirements/base.in @@ -102,13 +102,13 @@ edx-ccx-keys==1.2.1 # via -r requirements/base.in edx-django-release-util==1.3.0 # via -r requirements/base.in -edx-django-utils==5.7.0 +edx-django-utils==5.8.0 # via # -r requirements/base.in # edx-drf-extensions # edx-rest-api-client # edx-toggles -edx-drf-extensions==8.12.0 +edx-drf-extensions==8.13.0 # via -r requirements/base.in edx-i18n-tools==1.3.0 # via -r requirements/base.in @@ -145,7 +145,7 @@ path==16.7.1 # path-py path-py==12.5.0 # via -r requirements/base.in -pbr==5.11.1 +pbr==6.0.0 # via stevedore polib==1.2.0 # via edx-i18n-tools diff --git a/requirements/doc.txt b/requirements/doc.txt index 546fb8dad..f8bc3b8e4 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -34,7 +34,7 @@ cffi==1.16.0 # -r requirements/base.txt # cryptography # pynacl -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 # via # -r requirements/base.txt # requests @@ -57,7 +57,7 @@ defusedxml==0.8.0rc2 # -r requirements/base.txt # python3-openid # social-auth-core -django==4.2.6 +django==4.2.7 # via # -c requirements/constraints.txt # -r requirements/base.txt @@ -132,13 +132,13 @@ edx-ccx-keys==1.2.1 # via -r requirements/base.txt edx-django-release-util==1.3.0 # via -r requirements/base.txt -edx-django-utils==5.7.0 +edx-django-utils==5.8.0 # via # -r requirements/base.txt # edx-drf-extensions # edx-rest-api-client # edx-toggles -edx-drf-extensions==8.12.0 +edx-drf-extensions==8.13.0 # via -r requirements/base.txt edx-i18n-tools==1.3.0 # via -r requirements/base.txt @@ -196,7 +196,7 @@ path==16.7.1 # path-py path-py==12.5.0 # via -r requirements/base.txt -pbr==5.11.1 +pbr==6.0.0 # via # -r requirements/base.txt # stevedore diff --git a/requirements/local.txt b/requirements/local.txt index 7a71b349b..d0dc24e48 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -20,8 +20,6 @@ backports-zoneinfo==0.2.1 # via # -r requirements/test.txt # django -bok-choy==2.0.2 - # via -r requirements/test.txt build==1.0.3 # via # -r requirements/pip_tools.txt @@ -210,10 +208,6 @@ jinja2==3.1.2 # via # -r requirements/test.txt # code-annotations -lazy==1.6 - # via - # -r requirements/test.txt - # bok-choy libsass==0.22.0 # via -r requirements/test.txt logutils==0.3.5 @@ -375,10 +369,6 @@ requests-oauthlib==1.3.1 # via # -r requirements/test.txt # social-auth-core -selenium==3.141.0 - # via - # -r requirements/test.txt - # bok-choy semantic-version==2.10.0 # via # -r requirements/test.txt diff --git a/requirements/test.in b/requirements/test.in index 42afd2d3c..d2dfe3618 100755 --- a/requirements/test.in +++ b/requirements/test.in @@ -4,7 +4,6 @@ -r base.txt astroid -bok-choy coverage ddt django-dynamic-fixture diff --git a/requirements/test.txt b/requirements/test.txt index 8c0fd1204..18657fc67 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -16,22 +16,25 @@ astroid==3.0.1 # -r requirements/test.in # pylint # pylint-celery +attrs==23.1.0 + # via + # outcome + # trio backports-zoneinfo==0.2.1 # via # -r requirements/base.txt # django -bok-choy==2.0.2 - # via -r requirements/test.in certifi==2023.7.22 # via # -r requirements/base.txt # requests + # selenium cffi==1.16.0 # via # -r requirements/base.txt # cryptography # pynacl -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 # via # -r requirements/base.txt # requests @@ -58,7 +61,7 @@ cryptography==41.0.5 # -r requirements/base.txt # pyjwt # social-auth-core -ddt==1.6.0 +ddt==1.7.0 # via -r requirements/test.in defusedxml==0.8.0rc2 # via @@ -67,6 +70,7 @@ defusedxml==0.8.0rc2 # social-auth-core dill==0.3.7 # via pylint +django==4.2.7 # via # -c requirements/constraints.txt # -r requirements/base.txt @@ -139,13 +143,13 @@ edx-ccx-keys==1.2.1 # via -r requirements/base.txt edx-django-release-util==1.3.0 # via -r requirements/base.txt -edx-django-utils==5.7.0 +edx-django-utils==5.8.0 # via # -r requirements/base.txt # edx-drf-extensions # edx-rest-api-client # edx-toggles -edx-drf-extensions==8.12.0 +edx-drf-extensions==8.13.0 # via -r requirements/base.txt edx-i18n-tools==1.3.0 # via -r requirements/base.txt @@ -161,13 +165,19 @@ edx-rest-api-client==5.6.1 edx-toggles==5.1.0 # via -r requirements/base.txt exceptiongroup==1.1.3 - # via pytest + # via + # pytest + # trio + # trio-websocket +h11==0.14.0 + # via wsproto httpretty==1.1.4 # via -r requirements/test.in idna==3.4 # via # -r requirements/base.txt # requests + # trio iniconfig==2.0.0 # via pytest isort==5.12.0 @@ -176,8 +186,6 @@ jinja2==3.1.2 # via # -r requirements/base.txt # code-annotations -lazy==1.6 - # via bok-choy libsass==0.22.0 # via -r requirements/base.txt logutils==0.3.5 @@ -201,6 +209,8 @@ oauthlib==3.2.2 # -r requirements/base.txt # requests-oauthlib # social-auth-core +outcome==1.3.0.post0 + # via trio packaging==23.2 # via pytest path==16.7.1 @@ -210,7 +220,7 @@ path==16.7.1 # path-py path-py==12.5.0 # via -r requirements/base.txt -pbr==5.11.1 +pbr==6.0.0 # via # -r requirements/base.txt # stevedore @@ -266,6 +276,8 @@ pynacl==1.5.0 # via # -r requirements/base.txt # edx-django-utils +pysocks==1.7.1 + # via urllib3 pytest==7.4.3 # via # -r requirements/test.in @@ -273,7 +285,7 @@ pytest==7.4.3 # pytest-django pytest-cov==4.1.0 # via -r requirements/test.in -pytest-django==4.6.0 +pytest-django==4.7.0 # via -r requirements/test.in python-slugify==8.0.1 # via @@ -306,10 +318,8 @@ requests-oauthlib==1.3.1 # via # -r requirements/base.txt # social-auth-core -selenium==3.141.0 - # via - # -r requirements/test.in - # bok-choy +selenium==4.15.2 + # via -r requirements/test.in semantic-version==2.10.0 # via # -r requirements/base.txt @@ -325,6 +335,8 @@ slumber==0.7.1 # via # -r requirements/base.txt # edx-rest-api-client +sniffio==1.3.0 + # via trio snowballstemmer==2.2.0 # via pydocstyle social-auth-app-django==5.4.0 @@ -336,6 +348,8 @@ social-auth-core==4.5.0 # -r requirements/base.txt # edx-auth-backends # social-auth-app-django +sortedcontainers==2.4.0 + # via trio sqlparse==0.4.4 # via # -r requirements/base.txt @@ -357,8 +371,14 @@ tomli==2.0.1 # coverage # pylint # pytest -tomlkit==0.12.1 +tomlkit==0.12.2 # via pylint +trio==0.23.1 + # via + # selenium + # trio-websocket +trio-websocket==0.11.1 + # via selenium typing-extensions==4.8.0 # via # -r requirements/base.txt @@ -367,9 +387,11 @@ typing-extensions==4.8.0 # django-countries # edx-opaque-keys # pylint -urllib3==1.26.18 +urllib3[socks]==1.26.18 # via # -c requirements/constraints.txt # -r requirements/base.txt # requests # selenium +wsproto==1.2.0 + # via trio-websocket From bfdbfb8e8931b9a1bb8fca40b67678d1413a6fa3 Mon Sep 17 00:00:00 2001 From: salman2013 Date: Mon, 13 Nov 2023 14:09:30 +0500 Subject: [PATCH 2/5] chore: remove code which is not using anymore --- a11y_tests/mixins.py | 13 ------------- a11y_tests/pages.py | 25 ------------------------- 2 files changed, 38 deletions(-) delete mode 100644 a11y_tests/mixins.py delete mode 100644 a11y_tests/pages.py diff --git a/a11y_tests/mixins.py b/a11y_tests/mixins.py deleted file mode 100644 index 555e138d0..000000000 --- a/a11y_tests/mixins.py +++ /dev/null @@ -1,13 +0,0 @@ -from acceptance_tests.mixins import AnalyticsApiClientMixin, LoginMixin - - -class CoursePageTestsMixin(LoginMixin, AnalyticsApiClientMixin): - """ Mixin for course page tests. """ - - DASHBOARD_DATE_FORMAT = '%B %d, %Y' - page = None - - def setUp(self): - super().setUp() - self.api_date_format = self.analytics_api_client.DATE_FORMAT - self.api_datetime_format = self.analytics_api_client.DATETIME_FORMAT diff --git a/a11y_tests/pages.py b/a11y_tests/pages.py deleted file mode 100644 index ff392c2c7..000000000 --- a/a11y_tests/pages.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Tests for course analytics pages -""" - - -from acceptance_tests import DASHBOARD_SERVER_URL, TEST_COURSE_ID - - -class CourseEnrollmentDemographicsPage(CoursePage): - demographic = None - - def __init__(self, browser, course_id=None): - super().__init__(browser, course_id) - self.page_url += f'/enrollment/demographics/{self.demographic}/' - - def is_browser_on_page(self): - return ( - super().is_browser_on_page() - and f'Enrollment Demographics by {self.demographic.title()}' - in self.browser.title - ) - - -class CourseEnrollmentDemographicsAgePage(CourseEnrollmentDemographicsPage): - demographic = 'age' From 0bc0fd7cdc462313328e86fd9a05b240207419dd Mon Sep 17 00:00:00 2001 From: salman2013 Date: Tue, 14 Nov 2023 22:03:42 +0500 Subject: [PATCH 3/5] chore: remove more mixns which are not using any more --- acceptance_tests/mixins.py | 338 ------------------------------------- 1 file changed, 338 deletions(-) diff --git a/acceptance_tests/mixins.py b/acceptance_tests/mixins.py index 1895630e9..30c07cd6a 100644 --- a/acceptance_tests/mixins.py +++ b/acceptance_tests/mixins.py @@ -1,9 +1,6 @@ -import datetime -import locale from unittest import skip from analyticsclient.client import Client -from selenium.webdriver.common.keys import Keys from acceptance_tests import ( API_AUTH_TOKEN, @@ -23,22 +20,8 @@ SOAPBOX_SINGLE_PAGE_PATH, SUPPORT_EMAIL, ) -from acceptance_tests.pages import LMSLoginPage from common.clients import CourseStructureApiClient -MAX_SUMMARY_POINT_VALUE_LENGTH = 13 - - -class AnalyticsApiClientMixin: - analytics_api_client = None - - def setUp(self): - super().setUp() - - api_url = API_SERVER_URL - auth_token = API_AUTH_TOKEN - self.analytics_api_client = Client(api_url, auth_token=auth_token, timeout=10) - class CourseApiMixin: course_api_client = None @@ -120,135 +103,6 @@ def assertRowTextEquals(self, cols, expected_texts): self.assertListEqual(actual, expected_texts) -class PageTestMixin: - def test_page(self): - pass - - -class FooterMixin(AssertMixin): - footer_selector = "footer[class=footer]" - - def _test_footer(self): - # make sure we have the footer - element = self.page.q(css=self.footer_selector) - self.assertTrue(element.present) - - def test_page(self): - super().test_page() - self._test_footer() - - -class FooterLegalMixin(FooterMixin): - def _test_footer(self): - super()._test_footer() - - # Verify the terms of service link is present - selector = self.footer_selector + " a[data-role=tos]" - element = self.page.q(css=selector) - self.assertTrue(element.present) - self.assertEqual(element.text[0], 'Terms of Service') - - # Verify the privacy policy link is present - selector = self.footer_selector + " a[data-role=privacy-policy]" - element = self.page.q(css=selector) - self.assertTrue(element.present) - self.assertEqual(element.text[0], 'Privacy Policy') - - -class FooterFeedbackMixin(FooterMixin): - def _test_footer(self): - super()._test_footer() - # check that we have an email - self.assertValidFeedbackLink(self.footer_selector + " a[class=feedback-email]") - - # check that we have the support email - selector = self.footer_selector + " a[class=support-email]" - self.assertHrefEqual(selector, SUPPORT_EMAIL) - - -class PrimaryNavMixin(CourseApiMixin): - # set to True if the URL fragement should be checked when testing the skip link - test_skip_link_url = True - - def _test_user_menu(self): - """ - Verify the user menu functions properly. - """ - element = self.page.q(css='.active-user.dropdown-toggle') - self.assertTrue(element.present) - self.assertEqual(element.attrs('aria-expanded')[0], 'false') - - element.click() - - # Ensure the menu is actually visible onscreen - element = self.page.q(css='ul.dropdown-menu.active-user-nav') - self.assertTrue(element.visible) - - def _test_active_course(self): - """ Ensure the active course item contains either the course name or ID. """ - course_id = getattr(self.page, 'course_id', None) - - if not course_id: - return skip('Page has no course_id attribute set.') - - element = self.page.q(css='.navbar-header .active-course-name') - self.assertTrue(element.visible) - - course_name = self.get_course_name_or_id(course_id) - self.assertEqual(element.text[0], course_name) - - return None - - def _test_skip_link(self, test_url): - active_element = self.driver.switch_to.active_element - skip_link = self.page.q(css='.skip-link').results[0] - skip_link_ref = '#' + skip_link.get_attribute('href').split('#')[-1] - target_element = self.page.q(css=skip_link_ref) - self.assertEqual(len(target_element), 1) - - active_element.send_keys(Keys.TAB) - active_element = self.driver.switch_to.active_element - active_element.send_keys(Keys.ENTER) - - if test_url: - url_hash = self.driver.execute_script('return window.location.hash;') - self.assertEqual(url_hash, skip_link_ref) - - def test_page(self): - self._test_skip_link(self.test_skip_link_url) - self._test_user_menu() - self._test_active_course() - - -class LoginMixin: - def setUp(self): - super().setUp() - self.lms_login_page = LMSLoginPage(self.browser) - - def login(self): - if ENABLE_AUTO_AUTH: - self.login_with_auto_auth() - else: - self.login_with_lms() - - def login_with_auto_auth(self): - url = f'{DASHBOARD_SERVER_URL}/test/auto_auth/' - self.browser.get(url) - - def login_with_lms(self): - """ Visit LMS and login. """ - - # Note: We use Selenium directly here (as opposed to Bok Choy) to avoid issues with promises being broken. - self.lms_login_page.browser.get(self.lms_login_page.url) - self.lms_login_page.login(LMS_USERNAME, LMS_PASSWORD) - - -class LogoutMixin: - def logout(self): - url = f'{DASHBOARD_SERVER_URL}/logout/' - self.browser.get(url) - - class ContextSensitiveHelpMixin: help_path = 'index.html' @@ -259,195 +113,3 @@ def help_url(self): def test_page(self): # Validate the help link self.assertHrefEqual('#help', self.help_url) - - -class SoapboxMessagesMixin: - soapbox_selector = "div[class=announcement-container]" - - def _test_soapbox_messages(self): - # make sure we have the correct soapbox messages displayed - element = self.page.q(css=self.soapbox_selector) - self.assertTrue(element.present) - self.assertTrue(SOAPBOX_GLOBAL_MESSAGE in element.text) - self.assertFalse(SOAPBOX_INACTIVE_MESSAGE in element.text) - - if self.page.path == SOAPBOX_SINGLE_PAGE_PATH: - element = self.page.q(css=self.soapbox_selector) - self.assertTrue(SOAPBOX_SINGLE_PAGE_MESSAGE in element.text) - - def test_page(self): - super().test_page() - self._test_soapbox_messages() - - -class AnalyticsDashboardWebAppTestMixin(FooterMixin, PrimaryNavMixin, ContextSensitiveHelpMixin, AssertMixin, - LoginMixin, SoapboxMessagesMixin): - def test_page(self): - self.login() - self.page.visit() - PrimaryNavMixin.test_page(self) - ContextSensitiveHelpMixin.test_page(self) - - def date_strip_leading_zeroes(self, s): - """ - Remove the leading 0 on formatted date strings. - :param s: Date formatted as string - """ - return s.replace(' 0', ' ') - - @staticmethod - def format_number(value): - """ Format the given value for the current locale (e.g. include decimal separator). """ - if isinstance(value, int): - return locale.format("%d", value, grouping=True) # pylint: disable=deprecated-method - return locale.format("%.1f", value, grouping=True) # pylint: disable=deprecated-method - - def assertSummaryPointValueEquals(self, data_selector, value): - """ - Compares the value in the summary card the "value" argument. - - Arguments: - data_selector (String): Attribute selector (ex. data-stat-type=current_enrollment) - tip_text (String): expected value - """ - # Account for Django truncation - if len(value) > MAX_SUMMARY_POINT_VALUE_LENGTH: - value = value[:(MAX_SUMMARY_POINT_VALUE_LENGTH - 1)] + '…' - - element = self.page.q(css=f"div[{data_selector}] .summary-point-number") - self.assertTrue(element.present) - self.assertEqual(element.text[0], value) - - def assertSummaryTooltipEquals(self, data_selector, tip_text): - """ - Compares the tooltip in the summary card the "tip_text" argument. - - Arguments: - data_selector (String): Attribute selector (ex. data-stat-type=current_enrollment) - tip_text (String): expected text - """ - help_selector = f"div[{data_selector}] .summary-point-help" - element = self.page.q(css=help_selector) - self.assertTrue(element.present) - - # check to see if - screen_reader_element = self.page.q(css=help_selector + " > span[class=sr-only]") - self.assertTrue(screen_reader_element.present) - self.assertEqual(screen_reader_element.text[0], tip_text) - - tooltip_element = self.page.q(css=help_selector + " > span[data-toggle='tooltip']") - self.assertTrue(tooltip_element.present) - # the context of title gets move to "data-original-title" - self.assertEqual(tooltip_element[0].get_attribute('data-original-title'), tip_text) - - def assertMetricTileValid(self, stat_type, value, tooltip): - selector = 'data-stat-type=%s' % stat_type - if value is not None: - self.assertSummaryPointValueEquals(selector, self.format_number(value)) - self.assertSummaryTooltipEquals(selector, tooltip) - - -class CoursePageTestsMixin(AnalyticsApiClientMixin, FooterLegalMixin, FooterFeedbackMixin, - AnalyticsDashboardWebAppTestMixin): - """ Mixin for common course page assertions and tests. """ - - DASHBOARD_DATE_FORMAT = '%B %d, %Y' - page = None - - def setUp(self): - super().setUp() - self.api_date_format = self.analytics_api_client.DATE_FORMAT - self.api_datetime_format = self.analytics_api_client.DATETIME_FORMAT - - def assertDataUpdateMessageEquals(self, value): - element = self.page.q(css='div.data-update-message') - self.assertEqual(element.text[0], value) - - def format_time_as_dashboard(self, value): - return value.strftime(self.DASHBOARD_DATE_FORMAT) - - def _format_last_updated_time(self, d): - return d.strftime('%I:%M %p').lstrip('0') - - def format_last_updated_date_and_time(self, d): - return {'update_date': d.strftime(self.DASHBOARD_DATE_FORMAT), 'update_time': self._format_last_updated_time(d)} - - def build_display_percentage(self, count, total, zero_percent_default='0.0%'): - if total and count: - percent = count / float(total) * 100.0 - return f'{percent:.1f}%' if percent >= 1.0 else '< 1%' - return zero_percent_default - - def _get_data_update_message(self): - raise NotImplementedError - - def _test_data_update_message(self): - """ Validate the content in the data update message container. """ - - message = self._get_data_update_message() - self.assertDataUpdateMessageEquals(message) - - def _test_course_home_nav(self): - element = self.page.q(css='.course-label') - self.assertEqual(element.text[0], 'Course Home') - - def test_page(self): - """ - Primary test method. - - Sub-classes should override this method and add additional tests. Sub-classes can safely assume that, if tests - pass, execution of this parent method will leave the browser on the page being tested. - :return: - """ - super().test_page() - self._test_data_update_message() - self._test_course_home_nav() - - -class CourseDemographicsPageTestsMixin(CoursePageTestsMixin): - demographic_type = None - data_information_message = 'All above demographic data was self-reported at the time of registration.' - chart_selector = '#enrollment-chart-view' - table_section_selector = 'div[data-role=enrollment-table]' - table_download_selector = 'a[data-role=enrollment-csv]' - table_columns = None - demographic_data = None - - def test_page(self): - super().test_page() - self._test_data_information_message() - self._test_chart() - self._test_table() - - def _test_chart(self): - self.fulfill_loading_promise(self.chart_selector) - self.assertElementHasContent(self.chart_selector) - - def _test_table(self): - self.assertTable(self.table_section_selector, self.table_columns, self.table_download_selector) - - rows = self.page.browser.find_elements_by_css_selector(f'{self.table_section_selector} tbody tr') - self.assertGreater(len(rows), 0) - sum_count = 0.0 - if self.demographic_data and 'count' in self.demographic_data[0]: - sum_count = float(sum([datum['count'] for datum in self.demographic_data])) - - for i, row in enumerate(rows): - columns = row.find_elements_by_css_selector('td') - self._test_table_row(self.demographic_data[i], columns, sum_count) - - def _test_table_row(self, datum, column, sum_count): - raise NotImplementedError - - def _test_data_information_message(self): - element = self.page.q(css='div.data-information-message') - self.assertEqual(element.text[0], self.data_information_message) - - def _get_data_update_message(self): - return self._build_data_update_message(self.course.enrollment(self.demographic_type)) - - def _build_data_update_message(self, api_response): - current_data = api_response[0] - last_updated = datetime.datetime.strptime(current_data['created'], self.api_datetime_format) - return 'Demographic learner data was last updated %(update_date)s at %(update_time)s UTC.' % \ - self.format_last_updated_date_and_time(last_updated) From 3e1ab6c0f543e4c576e154d7818f478bcf28e4be Mon Sep 17 00:00:00 2001 From: salman2013 Date: Tue, 14 Nov 2023 22:15:09 +0500 Subject: [PATCH 4/5] chore: fix test cases --- acceptance_tests/mixins.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/acceptance_tests/mixins.py b/acceptance_tests/mixins.py index 30c07cd6a..54ab232dc 100644 --- a/acceptance_tests/mixins.py +++ b/acceptance_tests/mixins.py @@ -1,24 +1,9 @@ -from unittest import skip - -from analyticsclient.client import Client - from acceptance_tests import ( - API_AUTH_TOKEN, - API_SERVER_URL, COURSE_API_KEY, COURSE_API_URL, DASHBOARD_FEEDBACK_EMAIL, - DASHBOARD_SERVER_URL, DOC_BASE_URL, - ENABLE_AUTO_AUTH, ENABLE_COURSE_API, - LMS_PASSWORD, - LMS_USERNAME, - SOAPBOX_GLOBAL_MESSAGE, - SOAPBOX_INACTIVE_MESSAGE, - SOAPBOX_SINGLE_PAGE_MESSAGE, - SOAPBOX_SINGLE_PAGE_PATH, - SUPPORT_EMAIL, ) from common.clients import CourseStructureApiClient From 447ab65df8c526539aacfb1afab2cf61a1807631 Mon Sep 17 00:00:00 2001 From: salman2013 Date: Wed, 15 Nov 2023 11:46:00 +0500 Subject: [PATCH 5/5] chore: remove selenium as it was not using anymore --- .github/docker-compose-github.yml | 1 - requirements/base.txt | 2 +- requirements/common_constraints.txt | 7 ++--- requirements/doc.txt | 2 +- requirements/test.in | 1 - requirements/test.txt | 42 +++++------------------------ requirements/tox.txt | 14 +++++++--- tox.ini | 1 - 8 files changed, 23 insertions(+), 47 deletions(-) diff --git a/.github/docker-compose-github.yml b/.github/docker-compose-github.yml index d7007af75..315536551 100644 --- a/.github/docker-compose-github.yml +++ b/.github/docker-compose-github.yml @@ -33,7 +33,6 @@ services: ENABLE_COURSE_LIST_FILTERS: "True" ENABLE_COURSE_LIST_PASSING: "True" SELENIUM_BROWSER: "firefox" - BOKCHOY_HEADLESS: "true" depends_on: - "es" - "analyticsapi" diff --git a/requirements/base.txt b/requirements/base.txt index 39ec46b92..02eeaaaba 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -133,7 +133,7 @@ lxml==4.9.3 # via edx-i18n-tools markupsafe==2.1.3 # via jinja2 -newrelic==9.1.1 +newrelic==9.1.2 # via edx-django-utils oauthlib==3.2.2 # via diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt index c84245937..acbb3dc47 100644 --- a/requirements/common_constraints.txt +++ b/requirements/common_constraints.txt @@ -22,6 +22,7 @@ elasticsearch<7.14.0 # django-simple-history>3.0.0 adds indexing and causes a lot of migrations to be affected django-simple-history==3.0.0 -# tox>4.0.0 isn't yet compatible with many tox plugins, causing CI failures in almost all repos. -# Details can be found in this discussion: https://github.com/tox-dev/tox/discussions/1810 -tox<4.0.0 +# virtualenv latest version requires platformdirs<4.0 which conflicts with tox>4.0 version +# This constraint can be removed once the issue +# https://github.com/pypa/virtualenv/issues/2666 gets resolved +platformdirs<4.0 diff --git a/requirements/doc.txt b/requirements/doc.txt index f8bc3b8e4..678a5e909 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -176,7 +176,7 @@ markupsafe==2.1.3 # via # -r requirements/base.txt # jinja2 -newrelic==9.1.1 +newrelic==9.1.2 # via # -r requirements/base.txt # edx-django-utils diff --git a/requirements/test.in b/requirements/test.in index d2dfe3618..54afd2556 100755 --- a/requirements/test.in +++ b/requirements/test.in @@ -15,5 +15,4 @@ edx-lint pytest pytest-cov pytest-django -selenium testfixtures diff --git a/requirements/test.txt b/requirements/test.txt index 18657fc67..670b9b3dc 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -16,10 +16,6 @@ astroid==3.0.1 # -r requirements/test.in # pylint # pylint-celery -attrs==23.1.0 - # via - # outcome - # trio backports-zoneinfo==0.2.1 # via # -r requirements/base.txt @@ -28,7 +24,6 @@ certifi==2023.7.22 # via # -r requirements/base.txt # requests - # selenium cffi==1.16.0 # via # -r requirements/base.txt @@ -165,19 +160,13 @@ edx-rest-api-client==5.6.1 edx-toggles==5.1.0 # via -r requirements/base.txt exceptiongroup==1.1.3 - # via - # pytest - # trio - # trio-websocket -h11==0.14.0 - # via wsproto + # via pytest httpretty==1.1.4 # via -r requirements/test.in idna==3.4 # via # -r requirements/base.txt # requests - # trio iniconfig==2.0.0 # via pytest isort==5.12.0 @@ -200,7 +189,7 @@ markupsafe==2.1.3 # jinja2 mccabe==0.7.0 # via pylint -newrelic==9.1.1 +newrelic==9.1.2 # via # -r requirements/base.txt # edx-django-utils @@ -209,8 +198,6 @@ oauthlib==3.2.2 # -r requirements/base.txt # requests-oauthlib # social-auth-core -outcome==1.3.0.post0 - # via trio packaging==23.2 # via pytest path==16.7.1 @@ -225,7 +212,9 @@ pbr==6.0.0 # -r requirements/base.txt # stevedore platformdirs==3.11.0 - # via pylint + # via + # -c requirements/common_constraints.txt + # pylint pluggy==1.3.0 # via pytest polib==1.2.0 @@ -276,8 +265,6 @@ pynacl==1.5.0 # via # -r requirements/base.txt # edx-django-utils -pysocks==1.7.1 - # via urllib3 pytest==7.4.3 # via # -r requirements/test.in @@ -318,8 +305,6 @@ requests-oauthlib==1.3.1 # via # -r requirements/base.txt # social-auth-core -selenium==4.15.2 - # via -r requirements/test.in semantic-version==2.10.0 # via # -r requirements/base.txt @@ -335,8 +320,6 @@ slumber==0.7.1 # via # -r requirements/base.txt # edx-rest-api-client -sniffio==1.3.0 - # via trio snowballstemmer==2.2.0 # via pydocstyle social-auth-app-django==5.4.0 @@ -348,8 +331,6 @@ social-auth-core==4.5.0 # -r requirements/base.txt # edx-auth-backends # social-auth-app-django -sortedcontainers==2.4.0 - # via trio sqlparse==0.4.4 # via # -r requirements/base.txt @@ -371,14 +352,8 @@ tomli==2.0.1 # coverage # pylint # pytest -tomlkit==0.12.2 +tomlkit==0.12.3 # via pylint -trio==0.23.1 - # via - # selenium - # trio-websocket -trio-websocket==0.11.1 - # via selenium typing-extensions==4.8.0 # via # -r requirements/base.txt @@ -387,11 +362,8 @@ typing-extensions==4.8.0 # django-countries # edx-opaque-keys # pylint -urllib3[socks]==1.26.18 +urllib3==1.26.18 # via # -c requirements/constraints.txt # -r requirements/base.txt # requests - # selenium -wsproto==1.2.0 - # via trio-websocket diff --git a/requirements/tox.txt b/requirements/tox.txt index 6beb47a60..5a343329e 100644 --- a/requirements/tox.txt +++ b/requirements/tox.txt @@ -11,9 +11,14 @@ filelock==3.13.1 # tox # virtualenv packaging==23.2 - # via tox + # via + # pyproject-api + # tox platformdirs==3.11.0 - # via virtualenv + # via + # -c requirements/common_constraints.txt + # tox + # virtualenv pluggy==1.3.0 # via tox py==1.11.0 @@ -21,10 +26,11 @@ py==1.11.0 six==1.16.0 # via tox tomli==2.0.1 - # via tox + # via + # pyproject-api + # tox tox==3.28.0 # via - # -c requirements/common_constraints.txt # -r requirements/tox.in # tox-battery tox-battery==0.6.2 diff --git a/tox.ini b/tox.ini index 6b7b6475a..8b74dba97 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,6 @@ passenv = COVERAGE_DIR DISPLAY SELENIUM_BROWSER - BOKCHOY_HEADLESS deps = django42: -r requirements/django.txt -r {toxinidir}/requirements/test.txt