diff --git a/circle.yml b/circle.yml index 7e79e4b9..460d0329 100644 --- a/circle.yml +++ b/circle.yml @@ -4,7 +4,7 @@ machine: dependencies: override: - "pip install -U pip wheel setuptools" - - "pip install -e git://github.com/edx/xblock-sdk.git@22c1b2f173919bef22f2d9d9295ec5396d02dffd#egg=xblock-sdk" + - "pip install -e git://github.com/edx/xblock-sdk.git@bddf9f4a2c6e4df28a411c8f632cc2250170ae9d#egg=xblock-sdk" - "pip install -r requirements.txt" - "pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements/base.txt" - "pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements/test.txt" diff --git a/problem_builder/public/js/review_blocks.js b/problem_builder/public/js/review_blocks.js index ffe8ef84..0684003a 100644 --- a/problem_builder/public/js/review_blocks.js +++ b/problem_builder/public/js/review_blocks.js @@ -8,6 +8,18 @@ function ExportBase(runtime, element, initData) { var generateDataUriFromImageURL = function(imgURL) { // Given the URL to an image, IF the image has already been cached by the browser, // returns a data: URI with the contents of the image (image will be converted to PNG) + + // Expand relative urls and urls without an explicit protocol into absolute urls + var a = document.createElement('a'); + a.href = imgURL; + imgURL = a.href; + + // If the image is from another domain, just return its URL. We can't + // create a data URL from cross-domain images: + // https://html.spec.whatwg.org/multipage/scripting.html#dom-canvas-todataurl + if (a.origin !== window.location.origin) + return imgURL; + var img = new Image(); img.src = imgURL; if (!img.complete) diff --git a/problem_builder/static/test/swoop-bg.png b/problem_builder/static/test/swoop-bg.png new file mode 100644 index 00000000..b82dabf7 Binary files /dev/null and b/problem_builder/static/test/swoop-bg.png differ diff --git a/problem_builder/static/test/swoop-step1.png b/problem_builder/static/test/swoop-step1.png new file mode 100644 index 00000000..163fe642 Binary files /dev/null and b/problem_builder/static/test/swoop-step1.png differ diff --git a/problem_builder/static/test/swoop-step2.png b/problem_builder/static/test/swoop-step2.png new file mode 100644 index 00000000..53bb2995 Binary files /dev/null and b/problem_builder/static/test/swoop-step2.png differ diff --git a/problem_builder/tests/integration/test_dashboard.py b/problem_builder/tests/integration/test_dashboard.py index c1dd1771..fe10cdd4 100644 --- a/problem_builder/tests/integration/test_dashboard.py +++ b/problem_builder/tests/integration/test_dashboard.py @@ -17,6 +17,8 @@ # along with this program in a file in the toplevel directory called # "AGPLv3". If not, see . # +import json +from functools import wraps from textwrap import dedent from mock import Mock, patch from .base_test import ProblemBuilderBaseTest @@ -52,6 +54,40 @@ def get_submissions(self, key, limit=1): return [] +def check_dashboard_and_report(fixture, set_mentoring_values=True, **kwargs): + """ + Decorator for dashboard test methods. + + - Sets up the given fixture + - Runs the decorated test + - Clicks the download link + - Opens the report + - Runs the decorated test again against the report + + Any extra keyword arguments are passed to the dashboard XBlock. + """ + def wrapper(test): + @wraps(test) + def wrapped(test_case): + test_case._install_fixture(fixture) + if kwargs: + dashboard = test_case.vertical.get_child('test-scenario.pb-dashboard.d0.u0') + for key, value in kwargs.items(): + setattr(dashboard, key, value) + dashboard.save() + if set_mentoring_values: + test_case._set_mentoring_values() + test_case.go_to_view('student_view') + test(test_case) + download_link = test_case.browser.find_element_by_css_selector('.report-download-link') + download_link.click() + test_case.browser.get(download_link.get_attribute('href')) + test_case.assertRegexpMatches(test_case.browser.current_url, r'^data:text/html;base64,') + test(test_case) + return wrapped + return wrapper + + class TestDashboardBlock(ProblemBuilderBaseTest): """ Test the Student View of a dashboard XBlock linked to some problem builder blocks @@ -75,6 +111,9 @@ class TestDashboardBlock(ProblemBuilderBaseTest): /> """) + # Clean up screenshots if the tests pass + cleanup_on_success = True + def setUp(self): super(TestDashboardBlock, self).setUp() @@ -128,12 +167,21 @@ def _assert_cell_contents(self, cell, expected_visible_text, expected_screen_rea def _format_sr_text(self, visible_text): return "Score: {value}".format(value=visible_text) + def _set_mentoring_values(self): + pbs = self.browser.find_elements_by_css_selector('.mentoring') + for pb in pbs: + mcqs = pb.find_elements_by_css_selector('fieldset.choices') + for idx, mcq in enumerate(mcqs): + choices = mcq.find_elements_by_css_selector('.choices .choice label') + choices[idx].click() + self.click_submit(pb) + + @check_dashboard_and_report(SIMPLE_DASHBOARD, set_mentoring_values=False) def test_empty_dashboard(self): """ Test that when the student has not submitted any question answers, we still see the dashboard, and its lists all the MCQ questions in the way we expect. """ - self._install_fixture(self.SIMPLE_DASHBOARD) dashboard = self.browser.find_element_by_css_selector('.pb-dashboard') step_headers = dashboard.find_elements_by_css_selector('thead') self.assertEqual(len(step_headers), 3) @@ -148,24 +196,11 @@ def test_empty_dashboard(self): cell = mcq.find_element_by_css_selector('td:last-child') self._assert_cell_contents(cell, '', 'No value yet') - def _set_mentoring_values(self): - pbs = self.browser.find_elements_by_css_selector('.mentoring') - for pb in pbs: - mcqs = pb.find_elements_by_css_selector('fieldset.choices') - for idx, mcq in enumerate(mcqs): - choices = mcq.find_elements_by_css_selector('.choices .choice label') - choices[idx].click() - self.click_submit(pb) - + @check_dashboard_and_report(SIMPLE_DASHBOARD) def test_dashboard(self): """ Submit an answer to each MCQ, then check that the dashboard reflects those answers. """ - self._install_fixture(self.SIMPLE_DASHBOARD) - self._set_mentoring_values() - - # Reload the page: - self.go_to_view("student_view") dashboard = self.browser.find_element_by_css_selector('.pb-dashboard') headers = dashboard.find_elements_by_class_name('report-header') self.assertEqual(len(headers), 0) @@ -191,6 +226,52 @@ def test_dashboard(self): expected_average = {0: "2", 1: "3", 2: "1"}[step_num] self._assert_cell_contents(right_col, expected_average, self._format_sr_text(expected_average)) + @check_dashboard_and_report(SIMPLE_DASHBOARD, visual_rules=json.dumps({ + 'background': '/static/test/swoop-bg.png', + })) + def test_dashboard_image(self): + """ + Test that the dashboard image is displayed correctly, both on the page + and it the report. We allow minor differences here as the report + screenshot is not a perfect match. + """ + self.assertScreenshot('.pb-dashboard-visual svg', 'dashboard-image', threshold=100) + + @check_dashboard_and_report(SIMPLE_DASHBOARD, visual_rules=json.dumps({ + 'background': ('//raw.githubusercontent.com/open-craft/problem-builder/omar/report-download/' + 'problem_builder/static/test/swoop-bg.png'), + })) + def test_dashboard_image_cross_domain(self): + """ + Test that cross-domain dashboard images are displayed correctly. We + allow minor differences here as the report screenshot is not a perfect + match. + """ + self.assertScreenshot('.pb-dashboard-visual svg', 'dashboard-image', threshold=100) + + @check_dashboard_and_report( + SIMPLE_DASHBOARD, + visual_rules=json.dumps({ + 'background': '/static/test/swoop-bg.png', + 'images': [ + '/static/test/swoop-step1.png', + '/static/test/swoop-step2.png', + ], + }), + color_rules='\n'.join([ + '0: grey', + 'x <= 2: #aa2626', + 'x <= 3: #e7ce76', + '#84b077', + ]), + ) + def test_dashboard_image_overlay(self): + """ + Test that image overlays are displayed correctly on the dashboard. + """ + self.assertScreenshot('.pb-dashboard-visual svg', 'dashboard-image-overlay', threshold=100) + + @check_dashboard_and_report(ALTERNATIVE_DASHBOARD) def test_dashboard_alternative(self): """ Submit an answer to each MCQ, then check that the dashboard reflects those answers with alternative @@ -200,11 +281,6 @@ def test_dashboard_alternative(self): * Numerical values are not shown * Include HTML header and footer snippets """ - self._install_fixture(self.ALTERNATIVE_DASHBOARD) - self._set_mentoring_values() - - # Reload the page: - self.go_to_view("student_view") dashboard = self.browser.find_element_by_css_selector('.pb-dashboard') header_p = dashboard.find_element_by_id('header-paragraph') self.assertEquals(header_p.text, 'Header') @@ -233,17 +309,13 @@ def test_dashboard_alternative(self): self.assertEqual(left_col.text, average_labels[step_num]) right_col = avg_row.find_element_by_css_selector('.value') expected_average = {0: "2", 1: "3", 2: "1"}[step_num] - self._assert_cell_contents(right_col, '', self._format_sr_text(expected_average)) + self._assert_cell_contents(right_col, '', self._format_sr_text(expected_average)) + @check_dashboard_and_report(HIDE_QUESTIONS_DASHBOARD) def test_dashboard_exclude_questions(self): """ Submit an answer to each MCQ, then check that the dashboard ignores questions it is configured to ignore """ - self._install_fixture(self.HIDE_QUESTIONS_DASHBOARD) - self._set_mentoring_values() - - # Reload the page: - self.go_to_view("student_view") dashboard = self.browser.find_element_by_css_selector('.pb-dashboard') steps = dashboard.find_elements_by_css_selector('tbody') self.assertEqual(len(steps), 3) @@ -267,15 +339,11 @@ def test_dashboard_exclude_questions(self): expected_average = {0: "1", 1: "3", 2: "1"}[step_num] self._assert_cell_contents(right_col, expected_average, self._format_sr_text(expected_average)) + @check_dashboard_and_report(MALFORMED_HIDE_QUESTIONS_DASHBOARD) def test_dashboard_malformed_exclude_questions(self): """ Submit an answer to each MCQ, then check that the dashboard ignores questions it is configured to ignore """ - self._install_fixture(self.MALFORMED_HIDE_QUESTIONS_DASHBOARD) - self._set_mentoring_values() - - # Reload the page: - self.go_to_view("student_view") dashboard = self.browser.find_element_by_css_selector('.pb-dashboard') steps = dashboard.find_elements_by_css_selector('tbody') self.assertEqual(len(steps), 3) diff --git a/screenshots/baseline/dashboard-image-overlay.png b/screenshots/baseline/dashboard-image-overlay.png new file mode 100644 index 00000000..47de53c7 Binary files /dev/null and b/screenshots/baseline/dashboard-image-overlay.png differ diff --git a/screenshots/baseline/dashboard-image.png b/screenshots/baseline/dashboard-image.png new file mode 100644 index 00000000..98e62e31 Binary files /dev/null and b/screenshots/baseline/dashboard-image.png differ