From fc54d87cfe51426932dcdb2b6195ad27d1b38514 Mon Sep 17 00:00:00 2001 From: Muhammad Farhan Khan Date: Wed, 25 Oct 2023 00:03:19 +0500 Subject: [PATCH] refactor: remove `bok-choy` and replace `xblockutils` with `xblock.utils` --- .github/workflows/ci.yml | 17 +- .gitignore | 4 +- Changelog.md | 8 + Makefile | 29 +- README.md | 13 +- drag_and_drop_v2/__init__.py | 2 +- drag_and_drop_v2/drag_and_drop_v2.py | 8 +- requirements/base.in | 2 +- requirements/base.txt | 18 +- requirements/ci.txt | 2 +- requirements/dev.in | 1 - requirements/dev.txt | 133 +--- requirements/quality.txt | 38 +- requirements/test.in | 2 - requirements/test.txt | 28 +- requirements/workbench.in | 15 - requirements/workbench.txt | 319 -------- run_tests.py | 40 - tests/integration/__init__.py | 0 tests/integration/data/200x200.svg | 12 - tests/integration/data/400x300.svg | 12 - tests/integration/data/60x60.svg | 11 - tests/integration/data/dnd-bg-square.svg | 28 - tests/integration/data/dnd-bg-wide.svg | 29 - tests/integration/data/old_version_data.json | 76 -- tests/integration/data/test_data.json | 56 -- tests/integration/data/test_data_a11y.json | 62 -- tests/integration/data/test_data_other.json | 56 -- tests/integration/data/test_html_data.json | 58 -- .../data/test_html_titles_data.json | 30 - tests/integration/data/test_item_dropped.json | 49 -- .../data/test_multiple_options_data.json | 59 -- .../data/test_sizing_template.json | 96 --- tests/integration/data/test_zone_align.json | 210 ----- tests/integration/test_base.py | 535 ------------- tests/integration/test_custom_data_render.py | 67 -- tests/integration/test_events.py | 241 ------ tests/integration/test_interaction.py | 744 ------------------ .../test_interaction_assessment.py | 517 ------------ tests/integration/test_render.py | 346 -------- tests/integration/test_sizing.py | 365 --------- tests/integration/test_studio.py | 289 ------- tests/integration/test_title_and_question.py | 62 -- tests/unit/test_fixtures.py | 2 +- tox.ini | 30 +- 45 files changed, 51 insertions(+), 4670 deletions(-) delete mode 100644 requirements/workbench.in delete mode 100755 run_tests.py delete mode 100644 tests/integration/__init__.py delete mode 100644 tests/integration/data/200x200.svg delete mode 100644 tests/integration/data/400x300.svg delete mode 100644 tests/integration/data/60x60.svg delete mode 100644 tests/integration/data/dnd-bg-square.svg delete mode 100644 tests/integration/data/dnd-bg-wide.svg delete mode 100644 tests/integration/data/old_version_data.json delete mode 100644 tests/integration/data/test_data.json delete mode 100644 tests/integration/data/test_data_a11y.json delete mode 100644 tests/integration/data/test_data_other.json delete mode 100644 tests/integration/data/test_html_data.json delete mode 100644 tests/integration/data/test_html_titles_data.json delete mode 100644 tests/integration/data/test_item_dropped.json delete mode 100644 tests/integration/data/test_multiple_options_data.json delete mode 100644 tests/integration/data/test_sizing_template.json delete mode 100644 tests/integration/data/test_zone_align.json delete mode 100644 tests/integration/test_base.py delete mode 100644 tests/integration/test_custom_data_render.py delete mode 100644 tests/integration/test_events.py delete mode 100644 tests/integration/test_interaction.py delete mode 100644 tests/integration/test_interaction_assessment.py delete mode 100644 tests/integration/test_render.py delete mode 100644 tests/integration/test_sizing.py delete mode 100644 tests/integration/test_studio.py delete mode 100644 tests/integration/test_title_and_question.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fad7c4586..17fc6d68c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,20 +15,12 @@ concurrency: jobs: tests: runs-on: ${{ matrix.os }} - services: - # Using SQLite3 for integration tests throws `django.db.utils.OperationalError: database table is locked: workbench_xblockstate`. - mysql: - image: mysql:8 - env: - MYSQL_ROOT_PASSWORD: rootpw - ports: - - 3307:3306 strategy: fail-fast: false matrix: os: [ubuntu-20.04] python-version: [3.8] - toxenv: [py38-django32, py38-django42, integration-django32, integration-django42, quality, translations-django32, translations-django42] + toxenv: [py38-django32, py38-django42, quality, translations-django32, translations-django42] steps: - name: checkout repo @@ -41,13 +33,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - # `libgtk2.0-0` and `libxt6` are required by an older version of Firefox. - - name: Install Required System Packages - if: ${{ startsWith(matrix.toxenv, 'integration') }} - run: | - sudo apt-get update - sudo apt-get install -y libxmlsec1-dev ubuntu-restricted-extras xvfb libxml2-dev libxslt-dev libevent-dev libgtk2.0-0 libxt6 - - name: Install translations dependencies if: ${{ startsWith(matrix.toxenv, 'translations') }} run: | diff --git a/.gitignore b/.gitignore index c5dceb6c5..fb2ffd2ae 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,7 @@ coverage.xml # Translations *.pot -# Integration test output: +# test output: /*.log /tests.*.png var/* @@ -58,5 +58,3 @@ target/ # IDEs .idea .idea/* - -test_helpers/ diff --git a/Changelog.md b/Changelog.md index 2e0cd428c..bc96176da 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,14 @@ Drag and Drop XBlock changelog Unreleased --------------------------- +Version 3.3.0 (2023-10-24) +--------------------------- + +* Removed xblock-utils package + * Replace `xblockutils.*` imports with `xblock.utils.*`. The old imports are used as a fallback for compatibility with older releases. +* Removed bok-choy package along with all integration tests. + + Version 3.2.2 (2023-10-19) --------------------------- diff --git a/Makefile b/Makefile index 3cf47557d..e9dd1a190 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: clean help compile_translations dummy_translations extract_translations detect_changed_source_translations \ build_dummy_translations validate_translations pull_translations push_translations check_translations_up_to_date \ - install_firefox requirements selfcheck test test.python test.unit test.quality upgrade mysql + requirements selfcheck test test.python test.unit test.quality upgrade .DEFAULT_GOAL := help @@ -11,8 +11,6 @@ EXTRACTED_DJANGO_PARTIAL := $(EXTRACT_DIR)/django-partial.po EXTRACTED_DJANGOJS_PARTIAL := $(EXTRACT_DIR)/djangojs-partial.po EXTRACTED_DJANGO := $(EXTRACT_DIR)/django.po -FIREFOX_VERSION := "43.0" - help: ## display this help message @echo "Please use \`make ' where is one of" @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' @@ -62,13 +60,6 @@ push_translations: ## push translations to transifex check_translations_up_to_date: extract_translations compile_translations dummy_translations detect_changed_source_translations ## extract, compile, and check if translation files are up-to-date -install_firefox: - @mkdir -p test_helpers - @test -f ./test_helpers/firefox/firefox && echo "Firefox already installed." || \ - (cd test_helpers && \ - wget -N "https://archive.mozilla.org/pub/firefox/releases/$(FIREFOX_VERSION)/linux-x86_64/en-US/firefox-$(FIREFOX_VERSION).tar.bz2" && \ - tar -xjf firefox-$(FIREFOX_VERSION).tar.bz2) - piptools: ## install pinned version of pip-compile and pip-sync pip install -r requirements/pip.txt pip install -r requirements/pip-tools.txt @@ -76,22 +67,19 @@ piptools: ## install pinned version of pip-compile and pip-sync requirements: piptools ## install test requirements locally pip-sync requirements/ci.txt -requirements_python: install_firefox piptools ## install all requirements locally +requirements_python: piptools ## install all requirements locally pip-sync requirements/dev.txt requirements/private.* test.quality: selfcheck ## run quality checkers on the codebase tox -e quality -test.python: ## run python unit and integration tests - PATH=test_helpers/firefox:$$PATH xvfb-run python run_tests.py $(TEST) +test.python: ## run python unit tests in the local virtualenv + pytest --cov drag_and_drop_v2 $(TEST) test.unit: ## run all unit tests - tox -- $(TEST) + tox $(TEST) -test.integration: ## run all integration tests - tox -e integration -- $(TEST) - -test: test.unit test.integration test.quality ## Run all tests +test: test.unit test.quality ## Run all tests tox -e translations # Define PIP_COMPILE_OPTS=-v to get more information during make upgrade. @@ -108,14 +96,9 @@ upgrade: ## update the requirements/*.txt files with the latest packages satisfy $(PIP_COMPILE) -o requirements/base.txt requirements/base.in $(PIP_COMPILE) -o requirements/test.txt requirements/test.in $(PIP_COMPILE) -o requirements/quality.txt requirements/quality.in - $(PIP_COMPILE) -o requirements/workbench.txt requirements/workbench.in $(PIP_COMPILE) -o requirements/ci.txt requirements/ci.in $(PIP_COMPILE) -o requirements/dev.txt requirements/dev.in sed -i '/^[dD]jango==/d' requirements/test.txt - sed -i '/^[dD]jango==/d' requirements/workbench.txt - -mysql: ## run mysql database for integration tests - docker run --rm -it --name mysql -p 3307:3306 -e MYSQL_ROOT_PASSWORD=rootpw -e MYSQL_DATABASE=db mysql:8 selfcheck: ## check that the Makefile is well-formed @echo "The Makefile is well-formed." diff --git a/README.md b/README.md index e0c7a8b2a..5e5e5f57d 100644 --- a/README.md +++ b/README.md @@ -465,10 +465,6 @@ Inside a fresh virtualenv, `cd` into the root folder of this repository $ make requirements ``` -To run integration tests, you need to start MySQL first: -```bash -$ make mysql -``` You can then run the entire test suite via: @@ -480,7 +476,6 @@ To run specific test groups, use one of the following commands: ```bash $ make test.unit -$ make test.integration $ make test.quality $ make test.translations ``` @@ -490,11 +485,6 @@ To run individual unit tests, use: ```bash $ make test.unit TEST=tests/unit/test_basics.py::BasicTests::test_student_view_data ``` -To run individual integration tests, use: - -```bash -$ make test.integration TEST=tests.integration.test_studio.TestStudio.test_custom_image -``` Manual testing (without tox) ---------------------------- @@ -502,9 +492,8 @@ Manual testing (without tox) To run tests without tox, use: ```bash -$ make mysql $ make requirements_python -$ make test.python TEST=tests.unit.test_basics.BasicTests.test_student_view_data +$ make test.python TEST=tests/unit/test_basics.py::BasicTests::test_student_view_data ``` diff --git a/drag_and_drop_v2/__init__.py b/drag_and_drop_v2/__init__.py index 52c4ffdc4..7189ac6e1 100644 --- a/drag_and_drop_v2/__init__.py +++ b/drag_and_drop_v2/__init__.py @@ -1,4 +1,4 @@ """ Drag and Drop v2 XBlock """ from .drag_and_drop_v2 import DragAndDropBlock -__version__ = "3.2.2" +__version__ = "3.3.0" diff --git a/drag_and_drop_v2/drag_and_drop_v2.py b/drag_and_drop_v2/drag_and_drop_v2.py index 474df4f12..ceef8d37e 100644 --- a/drag_and_drop_v2/drag_and_drop_v2.py +++ b/drag_and_drop_v2/drag_and_drop_v2.py @@ -23,9 +23,13 @@ from xblock.exceptions import JsonHandlerError from xblock.fields import Boolean, Dict, Float, Integer, Scope, String from xblock.scorable import ScorableXBlockMixin, Score +try: + from xblock.utils.resources import ResourceLoader + from xblock.utils.settings import ThemableXBlockMixin, XBlockWithSettingsMixin +except ModuleNotFoundError: # For backward compatibility with releases older than Quince. + from xblockutils.resources import ResourceLoader + from xblockutils.settings import ThemableXBlockMixin, XBlockWithSettingsMixin from web_fragments.fragment import Fragment -from xblockutils.resources import ResourceLoader -from xblockutils.settings import ThemableXBlockMixin, XBlockWithSettingsMixin from .compat import get_grading_ignore_decoys_waffle_flag from .default_data import DEFAULT_DATA diff --git a/requirements/base.in b/requirements/base.in index 293cb58e5..d1a9094c1 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -3,4 +3,4 @@ django-statici18n bleach[css] -xblock-utils +XBlock[django] diff --git a/requirements/base.txt b/requirements/base.txt index 98c2858a3..7dad7a8b0 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -10,9 +10,9 @@ asgiref==3.7.2 # via django bleach[css]==6.1.0 # via -r requirements/base.in -boto3==1.28.68 +boto3==1.28.69 # via fs-s3fs -botocore==1.31.68 +botocore==1.31.69 # via # boto3 # s3transfer @@ -42,9 +42,7 @@ lazy==1.6 lxml==4.9.3 # via xblock mako==1.2.4 - # via - # xblock - # xblock-utils + # via xblock markupsafe==2.1.3 # via # mako @@ -64,9 +62,7 @@ pyyaml==6.0.1 s3transfer==0.7.0 # via boto3 simplejson==3.19.2 - # via - # xblock - # xblock-utils + # via xblock six==1.16.0 # via # bleach @@ -82,9 +78,7 @@ typing-extensions==4.8.0 urllib3==1.26.18 # via botocore web-fragments==2.1.0 - # via - # xblock - # xblock-utils + # via xblock webencodings==0.5.1 # via # bleach @@ -92,8 +86,6 @@ webencodings==0.5.1 webob==1.8.7 # via xblock xblock[django]==1.8.1 - # via xblock-utils -xblock-utils==4.0.0 # via -r requirements/base.in # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/ci.txt b/requirements/ci.txt index 8115a7d03..996aae914 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -29,5 +29,5 @@ tox==3.28.0 # tox-battery tox-battery==0.6.2 # via -r requirements/ci.in -virtualenv==20.24.5 +virtualenv==20.24.6 # via tox diff --git a/requirements/dev.in b/requirements/dev.in index ff607dbaa..7456111c3 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -4,4 +4,3 @@ -r pip-tools.txt # pip-tools and its dependencies, for managing requirements files -r quality.txt # Core and quality check dependencies -r ci.txt # dependencies for setting up testing in CI --r workbench.txt # workbench dependencies diff --git a/requirements/dev.txt b/requirements/dev.txt index 7d4442bd4..e9ace7ff9 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -7,17 +7,14 @@ appdirs==1.4.4 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # fs arrow==1.3.0 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # cookiecutter asgiref==3.7.2 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # django astroid==2.3.3 # via @@ -27,27 +24,18 @@ astroid==2.3.3 binaryornot==0.4.4 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # cookiecutter bleach[css]==6.1.0 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # bleach -bok-choy==0.7.1 - # via - # -c requirements/constraints.txt - # -r requirements/quality.txt - # -r requirements/workbench.txt -boto3==1.28.68 +boto3==1.28.69 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # fs-s3fs -botocore==1.31.68 +botocore==1.31.69 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # boto3 # s3transfer build==1.0.3 @@ -57,23 +45,19 @@ build==1.0.3 certifi==2023.7.22 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # requests chardet==5.2.0 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # binaryornot charset-normalizer==3.3.1 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # requests click==8.1.7 # via # -r requirements/pip-tools.txt # -r requirements/quality.txt - # -r requirements/workbench.txt # click-log # code-annotations # cookiecutter @@ -90,18 +74,14 @@ code-annotations==1.5.0 cookiecutter==2.4.0 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # xblock-sdk coverage[toml]==7.3.2 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # coverage # pytest-cov ddt==1.6.0 - # via - # -r requirements/quality.txt - # -r requirements/workbench.txt + # via -r requirements/quality.txt distlib==0.3.7 # via # -r requirements/ci.txt @@ -110,7 +90,6 @@ django==3.2.22 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/quality.txt - # -r requirements/workbench.txt # django-appconf # django-statici18n # edx-i18n-tools @@ -119,22 +98,16 @@ django==3.2.22 django-appconf==1.0.5 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # django-statici18n django-statici18n==2.4.0 - # via - # -r requirements/quality.txt - # -r requirements/workbench.txt + # via -r requirements/quality.txt edx-i18n-tools==1.3.0 - # via - # -r requirements/quality.txt - # -r requirements/workbench.txt -edx-lint==5.3.4 + # via -r requirements/quality.txt +edx-lint==5.3.6 # via -r requirements/quality.txt exceptiongroup==1.1.3 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # pytest filelock==3.12.4 # via @@ -144,20 +117,17 @@ filelock==3.12.4 fs==2.4.16 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # fs-s3fs # openedx-django-pyfs # xblock fs-s3fs==1.1.1 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # openedx-django-pyfs # xblock-sdk idna==3.4 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # requests importlib-metadata==6.8.0 # via @@ -166,7 +136,6 @@ importlib-metadata==6.8.0 iniconfig==2.0.0 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # pytest isort==4.3.21 # via @@ -175,20 +144,16 @@ isort==4.3.21 jinja2==3.1.2 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # code-annotations # cookiecutter jmespath==1.0.1 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # boto3 # botocore lazy==1.6 # via # -r requirements/quality.txt - # -r requirements/workbench.txt - # bok-choy # xblock lazy-object-proxy==1.4.3 # via @@ -197,25 +162,20 @@ lazy-object-proxy==1.4.3 lxml==4.9.3 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # edx-i18n-tools # xblock # xblock-sdk mako==1.2.4 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # xblock - # xblock-utils markdown-it-py==3.0.0 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # rich markupsafe==2.1.3 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # jinja2 # mako # xblock @@ -226,52 +186,29 @@ mccabe==0.6.1 mdurl==0.1.2 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # markdown-it-py mock==5.1.0 - # via - # -r requirements/quality.txt - # -r requirements/workbench.txt -mysqlclient==2.2.0 - # via -r requirements/workbench.txt -needle==0.5.0 - # via - # -r requirements/quality.txt - # -r requirements/workbench.txt - # bok-choy -nose==1.3.7 - # via - # -r requirements/quality.txt - # -r requirements/workbench.txt - # needle + # via -r requirements/quality.txt openedx-django-pyfs==3.4.0 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # xblock packaging==23.2 # via # -r requirements/ci.txt # -r requirements/pip-tools.txt # -r requirements/quality.txt - # -r requirements/workbench.txt # build # pytest # tox path==16.7.1 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # edx-i18n-tools pbr==5.11.1 # via # -r requirements/quality.txt # stevedore -pillow==10.1.0 - # via - # -r requirements/quality.txt - # -r requirements/workbench.txt - # needle pip-tools==7.3.0 # via -r requirements/pip-tools.txt platformdirs==3.11.0 @@ -282,13 +219,11 @@ pluggy==1.3.0 # via # -r requirements/ci.txt # -r requirements/quality.txt - # -r requirements/workbench.txt # pytest # tox polib==1.2.0 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # edx-i18n-tools py==1.11.0 # via @@ -299,7 +234,6 @@ pycodestyle==2.11.1 pygments==2.16.1 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # rich pylint==2.4.2 # via @@ -313,7 +247,7 @@ pylint-celery==0.3 # via # -r requirements/quality.txt # edx-lint -pylint-django==2.5.4 +pylint-django==2.5.5 # via # -r requirements/quality.txt # edx-lint @@ -325,7 +259,6 @@ pylint-plugin-utils==0.8.2 pypng==0.20220715.0 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # xblock-sdk pyproject-hooks==1.0.0 # via @@ -334,40 +267,31 @@ pyproject-hooks==1.0.0 pytest==7.4.2 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # pytest-cov # pytest-django pytest-cov==4.1.0 - # via - # -r requirements/quality.txt - # -r requirements/workbench.txt + # via -r requirements/quality.txt pytest-django==4.5.2 - # via - # -r requirements/quality.txt - # -r requirements/workbench.txt + # via -r requirements/quality.txt python-dateutil==2.8.2 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # arrow # botocore # xblock python-slugify==8.0.1 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # code-annotations # cookiecutter pytz==2023.3.post1 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # django # xblock pyyaml==6.0.1 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # code-annotations # cookiecutter # edx-i18n-tools @@ -375,41 +299,27 @@ pyyaml==6.0.1 requests==2.31.0 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # cookiecutter # xblock-sdk rich==13.6.0 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # cookiecutter s3transfer==0.7.0 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # boto3 -selenium==2.53.6 - # via - # -c requirements/constraints.txt - # -r requirements/quality.txt - # -r requirements/workbench.txt - # bok-choy - # needle simplejson==3.19.2 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # xblock # xblock-sdk - # xblock-utils six==1.16.0 # via # -r requirements/ci.txt # -r requirements/quality.txt - # -r requirements/workbench.txt # astroid # bleach - # bok-choy # edx-lint # fs # fs-s3fs @@ -418,7 +328,6 @@ six==1.16.0 sqlparse==0.4.4 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # django stevedore==5.1.0 # via @@ -427,19 +336,16 @@ stevedore==5.1.0 text-unidecode==1.3 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # python-slugify tinycss2==1.2.1 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # bleach tomli==2.0.1 # via # -r requirements/ci.txt # -r requirements/pip-tools.txt # -r requirements/quality.txt - # -r requirements/workbench.txt # build # coverage # pip-tools @@ -456,41 +362,34 @@ tox-battery==0.6.2 types-python-dateutil==2.8.19.14 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # arrow typing-extensions==4.8.0 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # asgiref # rich urllib3==1.26.18 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # botocore # requests -virtualenv==20.24.5 +virtualenv==20.24.6 # via # -r requirements/ci.txt # tox web-fragments==2.1.0 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # xblock # xblock-sdk - # xblock-utils webencodings==0.5.1 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # bleach # tinycss2 webob==1.8.7 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # xblock # xblock-sdk wheel==0.41.2 @@ -504,18 +403,10 @@ wrapt==1.11.2 xblock[django]==1.8.1 # via # -r requirements/quality.txt - # -r requirements/workbench.txt # xblock # xblock-sdk - # xblock-utils xblock-sdk==0.7.0 - # via - # -r requirements/quality.txt - # -r requirements/workbench.txt -xblock-utils==4.0.0 - # via - # -r requirements/quality.txt - # -r requirements/workbench.txt + # via -r requirements/quality.txt zipp==3.17.0 # via # -r requirements/pip-tools.txt diff --git a/requirements/quality.txt b/requirements/quality.txt index ba972c45f..5dead2f5c 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -28,15 +28,11 @@ bleach[css]==6.1.0 # via # -r requirements/test.txt # bleach -bok-choy==0.7.1 - # via - # -c requirements/constraints.txt - # -r requirements/test.txt -boto3==1.28.68 +boto3==1.28.69 # via # -r requirements/test.txt # fs-s3fs -botocore==1.31.68 +botocore==1.31.69 # via # -r requirements/test.txt # boto3 @@ -92,7 +88,7 @@ django-statici18n==2.4.0 # via -r requirements/test.txt edx-i18n-tools==1.3.0 # via -r requirements/test.txt -edx-lint==5.3.4 +edx-lint==5.3.6 # via -r requirements/quality.in exceptiongroup==1.1.3 # via @@ -132,7 +128,6 @@ jmespath==1.0.1 lazy==1.6 # via # -r requirements/test.txt - # bok-choy # xblock lazy-object-proxy==1.4.3 # via astroid @@ -146,7 +141,6 @@ mako==1.2.4 # via # -r requirements/test.txt # xblock - # xblock-utils markdown-it-py==3.0.0 # via # -r requirements/test.txt @@ -165,14 +159,6 @@ mdurl==0.1.2 # markdown-it-py mock==5.1.0 # via -r requirements/test.txt -needle==0.5.0 - # via - # -r requirements/test.txt - # bok-choy -nose==1.3.7 - # via - # -r requirements/test.txt - # needle openedx-django-pyfs==3.4.0 # via # -r requirements/test.txt @@ -187,10 +173,6 @@ path==16.7.1 # edx-i18n-tools pbr==5.11.1 # via stevedore -pillow==10.1.0 - # via - # -r requirements/test.txt - # needle pluggy==1.3.0 # via # -r requirements/test.txt @@ -214,7 +196,7 @@ pylint==2.4.2 # pylint-plugin-utils pylint-celery==0.3 # via edx-lint -pylint-django==2.5.4 +pylint-django==2.5.5 # via edx-lint pylint-plugin-utils==0.8.2 # via @@ -269,24 +251,16 @@ s3transfer==0.7.0 # via # -r requirements/test.txt # boto3 -selenium==2.53.6 - # via - # -c requirements/constraints.txt - # -r requirements/test.txt - # bok-choy - # needle simplejson==3.19.2 # via # -r requirements/test.txt # xblock # xblock-sdk - # xblock-utils six==1.16.0 # via # -r requirements/test.txt # astroid # bleach - # bok-choy # edx-lint # fs # fs-s3fs @@ -329,7 +303,6 @@ web-fragments==2.1.0 # -r requirements/test.txt # xblock # xblock-sdk - # xblock-utils webencodings==0.5.1 # via # -r requirements/test.txt @@ -347,11 +320,8 @@ xblock[django]==1.8.1 # -r requirements/test.txt # xblock # xblock-sdk - # xblock-utils xblock-sdk==0.7.0 # via -r requirements/test.txt -xblock-utils==4.0.0 - # via -r requirements/test.txt # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/requirements/test.in b/requirements/test.in index 08aaab81f..f854691d2 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -7,8 +7,6 @@ pytest-cov # pytest extension for code coverage statistics pytest-django # pytest extension for better Django support ddt # data-driven tests -bok-choy # integration tests -selenium # integration tests mock # required by the workbench openedx-django-pyfs # required by the workbench diff --git a/requirements/test.txt b/requirements/test.txt index 0edb6c6e9..b7294cd7a 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -20,15 +20,11 @@ bleach[css]==6.1.0 # via # -r requirements/base.txt # bleach -bok-choy==0.7.1 - # via - # -c requirements/constraints.txt - # -r requirements/test.in -boto3==1.28.68 +boto3==1.28.69 # via # -r requirements/base.txt # fs-s3fs -botocore==1.31.68 +botocore==1.31.69 # via # -r requirements/base.txt # boto3 @@ -92,7 +88,6 @@ jmespath==1.0.1 lazy==1.6 # via # -r requirements/base.txt - # bok-choy # xblock lxml==4.9.3 # via @@ -104,7 +99,6 @@ mako==1.2.4 # via # -r requirements/base.txt # xblock - # xblock-utils markdown-it-py==3.0.0 # via rich markupsafe==2.1.3 @@ -117,10 +111,6 @@ mdurl==0.1.2 # via markdown-it-py mock==5.1.0 # via -r requirements/test.in -needle==0.5.0 - # via bok-choy -nose==1.3.7 - # via needle openedx-django-pyfs==3.4.0 # via # -r requirements/base.txt @@ -130,8 +120,6 @@ packaging==23.2 # via pytest path==16.7.1 # via edx-i18n-tools -pillow==10.1.0 - # via needle pluggy==1.3.0 # via pytest polib==1.2.0 @@ -177,23 +165,15 @@ s3transfer==0.7.0 # via # -r requirements/base.txt # boto3 -selenium==2.53.6 - # via - # -c requirements/constraints.txt - # -r requirements/test.in - # bok-choy - # needle simplejson==3.19.2 # via # -r requirements/base.txt # xblock # xblock-sdk - # xblock-utils six==1.16.0 # via # -r requirements/base.txt # bleach - # bok-choy # fs # fs-s3fs # python-dateutil @@ -228,7 +208,6 @@ web-fragments==2.1.0 # -r requirements/base.txt # xblock # xblock-sdk - # xblock-utils webencodings==0.5.1 # via # -r requirements/base.txt @@ -244,11 +223,8 @@ xblock[django]==1.8.1 # -r requirements/base.txt # xblock # xblock-sdk - # xblock-utils xblock-sdk==0.7.0 # via -r requirements/test.in -xblock-utils==4.0.0 - # via -r requirements/base.txt # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/requirements/workbench.in b/requirements/workbench.in deleted file mode 100644 index d5bf4bb05..000000000 --- a/requirements/workbench.in +++ /dev/null @@ -1,15 +0,0 @@ -# Requirements copy-pasted from the workbench. --c constraints.txt - --r test.txt # Core and testing dependencies for this package - -mysqlclient - -fs-s3fs -lxml -requests -pypng -simplejson -web-fragments -webob -XBlock[django] diff --git a/requirements/workbench.txt b/requirements/workbench.txt index 51c97307d..e69de29bb 100644 --- a/requirements/workbench.txt +++ b/requirements/workbench.txt @@ -1,319 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.8 -# by the following command: -# -# make upgrade -# -appdirs==1.4.4 - # via - # -r requirements/test.txt - # fs -arrow==1.3.0 - # via - # -r requirements/test.txt - # cookiecutter -asgiref==3.7.2 - # via - # -r requirements/test.txt - # django -binaryornot==0.4.4 - # via - # -r requirements/test.txt - # cookiecutter -bleach[css]==6.1.0 - # via - # -r requirements/test.txt - # bleach -bok-choy==0.7.1 - # via - # -c requirements/constraints.txt - # -r requirements/test.txt -boto3==1.28.68 - # via - # -r requirements/test.txt - # fs-s3fs -botocore==1.31.68 - # via - # -r requirements/test.txt - # boto3 - # s3transfer -certifi==2023.7.22 - # via - # -r requirements/test.txt - # requests -chardet==5.2.0 - # via - # -r requirements/test.txt - # binaryornot -charset-normalizer==3.3.1 - # via - # -r requirements/test.txt - # requests -click==8.1.7 - # via - # -r requirements/test.txt - # cookiecutter -cookiecutter==2.4.0 - # via - # -r requirements/test.txt - # xblock-sdk -coverage[toml]==7.3.2 - # via - # -r requirements/test.txt - # coverage - # pytest-cov -ddt==1.6.0 - # via -r requirements/test.txt - # via - # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/test.txt - # django-appconf - # django-statici18n - # edx-i18n-tools - # openedx-django-pyfs - # xblock-sdk -django-appconf==1.0.5 - # via - # -r requirements/test.txt - # django-statici18n -django-statici18n==2.4.0 - # via -r requirements/test.txt -edx-i18n-tools==1.3.0 - # via -r requirements/test.txt -exceptiongroup==1.1.3 - # via - # -r requirements/test.txt - # pytest -fs==2.4.16 - # via - # -r requirements/test.txt - # fs-s3fs - # openedx-django-pyfs - # xblock -fs-s3fs==1.1.1 - # via - # -r requirements/test.txt - # -r requirements/workbench.in - # openedx-django-pyfs - # xblock-sdk -idna==3.4 - # via - # -r requirements/test.txt - # requests -iniconfig==2.0.0 - # via - # -r requirements/test.txt - # pytest -jinja2==3.1.2 - # via - # -r requirements/test.txt - # cookiecutter -jmespath==1.0.1 - # via - # -r requirements/test.txt - # boto3 - # botocore -lazy==1.6 - # via - # -r requirements/test.txt - # bok-choy - # xblock -lxml==4.9.3 - # via - # -r requirements/test.txt - # -r requirements/workbench.in - # edx-i18n-tools - # xblock - # xblock-sdk -mako==1.2.4 - # via - # -r requirements/test.txt - # xblock - # xblock-utils -markdown-it-py==3.0.0 - # via - # -r requirements/test.txt - # rich -markupsafe==2.1.3 - # via - # -r requirements/test.txt - # jinja2 - # mako - # xblock -mdurl==0.1.2 - # via - # -r requirements/test.txt - # markdown-it-py -mock==5.1.0 - # via -r requirements/test.txt -mysqlclient==2.2.0 - # via -r requirements/workbench.in -needle==0.5.0 - # via - # -r requirements/test.txt - # bok-choy -nose==1.3.7 - # via - # -r requirements/test.txt - # needle -openedx-django-pyfs==3.4.0 - # via - # -r requirements/test.txt - # xblock -packaging==23.2 - # via - # -r requirements/test.txt - # pytest -path==16.7.1 - # via - # -r requirements/test.txt - # edx-i18n-tools -pillow==10.1.0 - # via - # -r requirements/test.txt - # needle -pluggy==1.3.0 - # via - # -r requirements/test.txt - # pytest -polib==1.2.0 - # via - # -r requirements/test.txt - # edx-i18n-tools -pygments==2.16.1 - # via - # -r requirements/test.txt - # rich -pypng==0.20220715.0 - # via - # -r requirements/test.txt - # -r requirements/workbench.in - # xblock-sdk -pytest==7.4.2 - # via - # -r requirements/test.txt - # pytest-cov - # pytest-django -pytest-cov==4.1.0 - # via -r requirements/test.txt -pytest-django==4.5.2 - # via -r requirements/test.txt -python-dateutil==2.8.2 - # via - # -r requirements/test.txt - # arrow - # botocore - # xblock -python-slugify==8.0.1 - # via - # -r requirements/test.txt - # cookiecutter -pytz==2023.3.post1 - # via - # -r requirements/test.txt - # django - # xblock -pyyaml==6.0.1 - # via - # -r requirements/test.txt - # cookiecutter - # edx-i18n-tools - # xblock -requests==2.31.0 - # via - # -r requirements/test.txt - # -r requirements/workbench.in - # cookiecutter - # xblock-sdk -rich==13.6.0 - # via - # -r requirements/test.txt - # cookiecutter -s3transfer==0.7.0 - # via - # -r requirements/test.txt - # boto3 -selenium==2.53.6 - # via - # -c requirements/constraints.txt - # -r requirements/test.txt - # bok-choy - # needle -simplejson==3.19.2 - # via - # -r requirements/test.txt - # -r requirements/workbench.in - # xblock - # xblock-sdk - # xblock-utils -six==1.16.0 - # via - # -r requirements/test.txt - # bleach - # bok-choy - # fs - # fs-s3fs - # python-dateutil -sqlparse==0.4.4 - # via - # -r requirements/test.txt - # django -text-unidecode==1.3 - # via - # -r requirements/test.txt - # python-slugify -tinycss2==1.2.1 - # via - # -r requirements/test.txt - # bleach -tomli==2.0.1 - # via - # -r requirements/test.txt - # coverage - # pytest -types-python-dateutil==2.8.19.14 - # via - # -r requirements/test.txt - # arrow -typing-extensions==4.8.0 - # via - # -r requirements/test.txt - # asgiref - # rich -urllib3==1.26.18 - # via - # -r requirements/test.txt - # botocore - # requests -web-fragments==2.1.0 - # via - # -r requirements/test.txt - # -r requirements/workbench.in - # xblock - # xblock-sdk - # xblock-utils -webencodings==0.5.1 - # via - # -r requirements/test.txt - # bleach - # tinycss2 -webob==1.8.7 - # via - # -r requirements/test.txt - # -r requirements/workbench.in - # xblock - # xblock-sdk -xblock[django]==1.8.1 - # via - # -r requirements/test.txt - # -r requirements/workbench.in - # xblock - # xblock-sdk - # xblock-utils -xblock-sdk==0.7.0 - # via -r requirements/test.txt -xblock-utils==4.0.0 - # via -r requirements/test.txt - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/run_tests.py b/run_tests.py deleted file mode 100755 index 0cf0bd5b6..000000000 --- a/run_tests.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python -""" -Run tests for the Drag and Drop V2 XBlock. - -This script is required to run our selenium tests inside the xblock-sdk workbench -because the workbench SDK's settings file is not inside any python module. -""" - -import logging -import os -import sys -import workbench - -if __name__ == "__main__": - # Find the location of the XBlock SDK. Note: it must be installed in development mode. - # ('python setup.py develop' or 'pip install -e') - xblock_sdk_dir = os.path.dirname(os.path.dirname(workbench.__file__)) - sys.path.append(xblock_sdk_dir) - - # Use the workbench settings file: - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "workbench.settings") - # Configure a range of ports in case the default port of 8081 is in use - os.environ.setdefault("DJANGO_LIVE_TEST_SERVER_ADDRESS", "localhost:8081-8099") - - # Silence too verbose Django logging - logging.disable(logging.DEBUG) - - try: - os.mkdir('var') - except OSError: - # The var dir may already exist. - pass - - from django.core.management import execute_from_command_line - args = sys.argv[1:] - paths = [arg for arg in args if arg[0] != '-'] - if not paths: - paths = ["tests/"] - options = [arg for arg in args if arg not in paths] - execute_from_command_line([sys.argv[0], "test"] + paths + options) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/integration/data/200x200.svg b/tests/integration/data/200x200.svg deleted file mode 100644 index 92b50c5d5..000000000 --- a/tests/integration/data/200x200.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - -200 x 200px - diff --git a/tests/integration/data/400x300.svg b/tests/integration/data/400x300.svg deleted file mode 100644 index 3c02f9765..000000000 --- a/tests/integration/data/400x300.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - -400 x 300px - diff --git a/tests/integration/data/60x60.svg b/tests/integration/data/60x60.svg deleted file mode 100644 index f7d663660..000000000 --- a/tests/integration/data/60x60.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - -60 x 60px - diff --git a/tests/integration/data/dnd-bg-square.svg b/tests/integration/data/dnd-bg-square.svg deleted file mode 100644 index 456a71c42..000000000 --- a/tests/integration/data/dnd-bg-square.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - <- 1/3 -> - <- 50% -> - <- 75% -> - - diff --git a/tests/integration/data/dnd-bg-wide.svg b/tests/integration/data/dnd-bg-wide.svg deleted file mode 100644 index 3d649db2c..000000000 --- a/tests/integration/data/dnd-bg-wide.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - <- 1/3 -> - <- 50% -> - <- 75% -> - - diff --git a/tests/integration/data/old_version_data.json b/tests/integration/data/old_version_data.json deleted file mode 100644 index cda5eb225..000000000 --- a/tests/integration/data/old_version_data.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "question_text": "Note: This is an example of data in the format used by prior versions of this block. Not in particular the old `size` data.", - "zones": [ - { - "index": 1, - "width": 200, - "title": "Zone 1", - "height": 100, - "y": "200", - "x": "100", - "id": "zone-1" - }, - { - "index": 2, - "width": 200, - "title": "Zone 2", - "height": 100, - "y": 0, - "x": 0, - "id": "zone-2" - } - ], - "items": [ - { - "displayName": "1", - "feedback": { - "incorrect": "No 1", - "correct": "Yes 1" - }, - "zone": "Zone 1", - "backgroundImage": "", - "id": 0, - "size": { - "width": "190px", - "height": "auto" - } - }, - { - "displayName": "2", - "feedback": { - "incorrect": "No 2", - "correct": "Yes 2" - }, - "zone": "Zone 2", - "backgroundImage": "", - "id": 1, - "size": { - "width": "190px", - "height": "100px" - } - }, - { - "displayName": "Pic", - "feedback": { - "incorrect": "", - "correct": "" - }, - "zone": "Zone 1", - "backgroundImage": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg==", - "id": 2, - "size": { - "width": "100px", - "height": "auto" - } - } - ], - "state": { - "items": {}, - "finished": true - }, - "feedback": { - "start": "Intro Feedback", - "finish": "Final Feedback" - }, - "title": "Drag and Drop (Old-style data)" -} diff --git a/tests/integration/data/test_data.json b/tests/integration/data/test_data.json deleted file mode 100644 index e3d75a632..000000000 --- a/tests/integration/data/test_data.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "zones": [ - { - "width": 200, - "title": "Zone 1", - "height": 100, - "y": "200", - "x": "100", - "uid": "zone-1" - }, - { - "width": 200, - "title": "Zone 2", - "height": 100, - "y": 0, - "x": 0, - "uid": "zone-2" - } - ], - "items": [ - { - "displayName": "1 here", - "feedback": { - "incorrect": "No 1", - "correct": "Yes 1" - }, - "zone": "zone-1", - "imageURL": "", - "id": 0 - }, - { - "displayName": "2 here", - "feedback": { - "incorrect": "No 2", - "correct": "Yes 2" - }, - "zone": "zone-2", - "imageURL": "", - "id": 1 - }, - { - "displayName": "X", - "feedback": { - "incorrect": "No Zone for this", - "correct": "" - }, - "zone": "none", - "imageURL": "", - "id": 2 - } - ], - "feedback": { - "start": "Some Intro Feed", - "finish": "Some Final Feed" - } -} diff --git a/tests/integration/data/test_data_a11y.json b/tests/integration/data/test_data_a11y.json deleted file mode 100644 index 079a74056..000000000 --- a/tests/integration/data/test_data_a11y.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "zones": [ - { - "title": "Zone 1", - "description": "This describes zone 1", - "height": 178, - "width": 196, - "y": "30", - "x": "160", - "id": "the deprecated id attribute is ignored." - }, - { - "title": "Zone 2", - "description": "This describes zone 2", - "height": 140, - "width": 340, - "y": "210", - "x": "86", - } - ], - "items": [ - { - "displayName": "1", - "imageURL": "https://placehold.it/100x100", - "imageDescription": "This describes the background image of item 1", - "feedback": { - "incorrect": "No, 1 does not belong here", - "correct": "Yes, 1 goes here" - }, - "zone": "Zone 1", - "id": 0 - }, - { - "displayName": "2", - "imageURL": "https://placehold.it/100x100", - "imageDescription": "This describes the background image of item 2", - "feedback": { - "incorrect": "No, 2 does not belong here", - "correct": "Yes, 2 goes here" - }, - "zone": "Zone 2", - "id": 1 - }, - { - "displayName": "X", - "imageURL": "", - "feedback": { - "incorrect": "You silly, there are no zones for X", - "correct": "" - }, - "zone": "none", - "id": 2 - } - ], - "feedback": { - "start": "Drag the items onto the image above.", - "finish": "Good work! You have completed this drag and drop problem." - }, - "targetImgDescription": "This describes the target image", - "displayLabels": {display_labels_value}, - "displayBorders": {display_borders_value}, -} diff --git a/tests/integration/data/test_data_other.json b/tests/integration/data/test_data_other.json deleted file mode 100644 index f206f966d..000000000 --- a/tests/integration/data/test_data_other.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "zones": [ - { - "width": 200, - "title": "Zone 51", - "height": 100, - "y": "400", - "x": "200", - "uid": "zone-51" - }, - { - "width": 200, - "title": "Zone 52", - "height": 100, - "y": "200", - "x": "100", - "uid": "zone-52" - } - ], - "items": [ - { - "displayName": "Item 1", - "feedback": { - "incorrect": "Incorrect 1", - "correct": "Correct 1" - }, - "zone": "zone-51", - "imageURL": "", - "id": 10 - }, - { - "displayName": "Item 2", - "feedback": { - "incorrect": "Incorrect 2", - "correct": "Correct 2" - }, - "zone": "zone-52", - "imageURL": "", - "id": 20 - }, - { - "displayName": "X", - "feedback": { - "incorrect": "No Zone for this", - "correct": "" - }, - "zone": "none", - "imageURL": "", - "id": 30 - } - ], - "feedback": { - "start": "Other Intro Feed", - "finish": "Other Final Feed" - } -} diff --git a/tests/integration/data/test_html_data.json b/tests/integration/data/test_html_data.json deleted file mode 100644 index 02fde6009..000000000 --- a/tests/integration/data/test_html_data.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "zones": [ - { - "width": 200, - "title": "Zone 1", - "height": 100, - "y": 200, - "x": 100, - "uid": "zone-1" - }, - { - "width": 200, - "title": "Zone 2", - "height": 100, - "y": 0, - "x": 0, - "uid": "zone-2" - } - ], - "items": [ - { - "displayName": "1", - "feedback": { - "incorrect": "No 1", - "correct": "Yes 1" - }, - "zone": "zone-1", - "imageURL": "", - "id": 0 - }, - { - "displayName": "2", - "feedback": { - "incorrect": "No 2", - "correct": "Yes 2" - }, - "zone": "zone-2", - "imageURL": "", - "id": 1 - }, - { - "displayName": "X", - "feedback": { - "incorrect": "No Zone for X", - "correct": "" - }, - "zone": "none", - "imageURL": "", - "id": 2 - } - ], - "feedback": { - "start": "Intro Feed", - "finish": "Final Feed" - }, - "targetImg": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg==", - "targetImgDescription": "This describes the target image" -} diff --git a/tests/integration/data/test_html_titles_data.json b/tests/integration/data/test_html_titles_data.json deleted file mode 100644 index 1a969800a..000000000 --- a/tests/integration/data/test_html_titles_data.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "zones": [ - { - "width": 200, - "title": "Zone -11", - "height": 100, - "y": 200, - "x": 100, - "uid": "zone-1" - } - ], - "items": [ - { - "displayName": "1", - "feedback": { - "incorrect": "No 1", - "correct": "Yes 1" - }, - "zone": "zone-1", - "imageURL": "", - "id": 0 - } - ], - "feedback": { - "start": "Intro Feed", - "finish": "Final Feed" - }, - "targetImg": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg==", - "targetImgDescription": "This describes the target image" -} diff --git a/tests/integration/data/test_item_dropped.json b/tests/integration/data/test_item_dropped.json deleted file mode 100644 index b02cc3db7..000000000 --- a/tests/integration/data/test_item_dropped.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "zones": [ - { - "width": 200, - "title": "Zone 1", - "height": 100, - "y": "200", - "x": "100", - "uid": "zone-1" - }, - { - "width": 200, - "title": "Zone 2", - "height": 100, - "y": 0, - "x": 0, - "uid": "zone-2" - } - ], - "items": [ - { - "displayName": "Has name", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zones": [ - "zone-1" - ], - "imageURL": "", - "id": 0 - }, - { - "displayName": "", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "zone-2", - "imageURL": "https://placehold.it/100x100", - "id": 1 - } - ], - "feedback": { - "start": "Start feedback", - "finish": "Finish feedback" - } -} - diff --git a/tests/integration/data/test_multiple_options_data.json b/tests/integration/data/test_multiple_options_data.json deleted file mode 100644 index 83c08d7a2..000000000 --- a/tests/integration/data/test_multiple_options_data.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "zones": [ - { - "width": 200, - "title": "Zone 1", - "height": 100, - "y": "200", - "x": "100", - "uid": "zone-1" - }, - { - "width": 200, - "title": "Zone 2", - "height": 100, - "y": 0, - "x": 0, - "uid": "zone-2" - } - ], - "items": [ - { - "displayName": "1 here", - "feedback": { - "incorrect": "No 1", - "correct": "Yes 1" - }, - "zones": [ - "zone-1", - "zone-2" - ], - "imageURL": "", - "id": 0 - }, - { - "displayName": "2 here", - "feedback": { - "incorrect": "No 2", - "correct": "Yes 2" - }, - "zone": "zone-2", - "imageURL": "", - "id": 1 - }, - { - "displayName": "X", - "feedback": { - "incorrect": "No Zone for this", - "correct": "" - }, - "zone": "none", - "imageURL": "", - "id": 2 - } - ], - "feedback": { - "start": "Some Intro Feed", - "finish": "Some Final Feed" - } -} diff --git a/tests/integration/data/test_sizing_template.json b/tests/integration/data/test_sizing_template.json deleted file mode 100644 index 315897de4..000000000 --- a/tests/integration/data/test_sizing_template.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - {% if img == "wide" %} - "targetImg": "{{img_wide_url}}", - "zones": [ - {"title": "Zone 1/3", "width": 533, "height": 200, "x": "0", "y": "0" {% if align_zones %}, "align": "left" {% endif %} }, - {"title": "Zone 50%", "width": 800, "height": 200, "x": "0", "y": "350" {% if align_zones %}, "align": "center" {% endif %} }, - {"title": "Zone 75%", "width": 1200, "height": 200, "x": "0", "y": "700" {% if align_zones %}, "align": "right" {% endif %} } - ], - {% else %} - "targetImg": "{{img_square_url}}", - "zones": [ - {"title": "Zone 1/3", "width": 166, "height": 100, "x": "0", "y": "0" {% if align_zones %}, "align": "left" {% endif %} }, - {"title": "Zone 50%", "width": 250, "height": 100, "x": "0", "y": "200" {% if align_zones %}, "align": "center" {% endif %} }, - {"title": "Zone 75%", "width": 375, "height": 100, "x": "0", "y": "400" {% if align_zones %}, "align": "right" {% endif %} } - ], - {% endif %} - "displayBorders": true, - "items": [ - { - "displayName": "Auto", - "feedback": {"incorrect": "", "correct": ""}, - "zone": "Zone 1/3", - "imageURL": "", - "id": 0 - }, - { - "displayName": "Auto with long text that should wrap because draggables are given a maximum width", - "feedback": {"incorrect": "", "correct": ""}, - "zone": "Zone 1/3", - "imageURL": "", - "id": 1 - }, - { - "displayName": "33.3%", - "feedback": {"incorrect": "", "correct": ""}, - "zone": "Zone 1/3", - "imageURL": "", - "id": 2, - "widthPercent": 33.3 - }, - { - "displayName": "50%", - "feedback": {"incorrect": "", "correct": ""}, - "zone": "Zone 50%", - "imageURL": "", - "id": 3, - "widthPercent": 50 - }, - { - "displayName": "75%", - "feedback": {"incorrect": "", "correct": ""}, - "zone": "Zone 75%", - "imageURL": "", - "id": 4, - "widthPercent": 75 - }, - { - "displayName": "IMG 400x300", - "feedback": {"incorrect": "", "correct": ""}, - "zone": "Zone 50%", - "imageURL": "{{img_400x300_url}}", - "id": 5, - }, - { - "displayName": "IMG 200x200", - "feedback": {"incorrect": "", "correct": ""}, - "zone": "Zone 50%", - "imageURL": "{{img_200x200_url}}", - "id": 6, - }, - { - "displayName": "IMG 400x300", - "feedback": {"incorrect": "", "correct": ""}, - "zone": "Zone 50%", - "imageURL": "{{img_400x300_url}}", - "id": 7, - "widthPercent": 50 - }, - { - "displayName": "IMG 200x200", - "feedback": {"incorrect": "", "correct": ""}, - "zone": "Zone 50%", - "imageURL": "{{img_200x200_url}}", - "id": 8, - "widthPercent": 50 - }, - { - "displayName": "IMG 60x60", - "feedback": {"incorrect": "", "correct": ""}, - "zone": "Zone 1/3", - "imageURL": "{{img_60x60_url}}", - "id": 9 - } - ], - "feedback": {"start": "Some Intro Feedback", "finish": "Some Final Feedback"} -} diff --git a/tests/integration/data/test_zone_align.json b/tests/integration/data/test_zone_align.json deleted file mode 100644 index 2e1120920..000000000 --- a/tests/integration/data/test_zone_align.json +++ /dev/null @@ -1,210 +0,0 @@ -{ - "zones": [ - { - "index": 1, - "title": "Zone No Align", - "width": 200, - "height": 100, - "x": 0, - "y": 0, - "align": "", - "id": "zone-none" - }, - { - "index": 1, - "title": "Zone Invalid Align", - "width": 200, - "height": 100, - "x": 0, - "y": 100, - "align": "invalid", - "id": "zone-invalid" - }, - { - "index": 2, - "title": "Zone Left Align", - "width": 200, - "height": 100, - "x": 0, - "y": 200, - "align": "left", - "id": "zone-left" - }, - { - "index": 3, - "title": "Zone Right Align", - "width": 200, - "height": 100, - "x": 0, - "y": 300, - "align": "right", - "id": "zone-right" - }, - { - "index": 4, - "title": "Zone Center Align", - "width": 200, - "height": 100, - "x": 0, - "y": 400, - "align": "center", - "id": "zone-center" - } - ], - "items": [ - { - "displayName": "AAAA", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone No Align", - "imageURL": "", - "id": 0 - }, - { - "displayName": "AAAA", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone No Align", - "imageURL": "", - "id": 1 - }, - { - "displayName": "AAAA", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone No Align", - "imageURL": "", - "id": 2 - }, - { - "displayName": "BBBB", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone Invalid Align", - "imageURL": "", - "id": 3 - }, - { - "displayName": "BBBB", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone Invalid Align", - "imageURL": "", - "id": 4 - }, - { - "displayName": "BBBB", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone Invalid Align", - "imageURL": "", - "id": 5 - }, - { - "displayName": "CCCC", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone Left Align", - "imageURL": "", - "id": 6 - }, - { - "displayName": "CCCC", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone Left Align", - "imageURL": "", - "id": 7 - }, - { - "displayName": "CCCC", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone Left Align", - "imageURL": "", - "id": 8 - }, - { - "displayName": "DDDD", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone Right Align", - "imageURL": "", - "id": 9 - }, - { - "displayName": "DDDD", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone Right Align", - "imageURL": "", - "id": 10 - }, - { - "displayName": "DDDD", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone Right Align", - "imageURL": "", - "id": 11 - }, - { - "displayName": "EEEE", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone Center Align", - "imageURL": "", - "id": 12 - }, - { - "displayName": "EEEE", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone Center Align", - "imageURL": "", - "id": 13 - }, - { - "displayName": "EEEE", - "feedback": { - "incorrect": "No", - "correct": "Yes" - }, - "zone": "Zone Center Align", - "imageURL": "", - "id": 14 - }, - ], - "feedback": { - "start": "Intro", - "finish": "Final" - }, -} diff --git a/tests/integration/test_base.py b/tests/integration/test_base.py deleted file mode 100644 index c85bb7ed7..000000000 --- a/tests/integration/test_base.py +++ /dev/null @@ -1,535 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Imports ########################################################### - -from __future__ import absolute_import - -import json -from collections import namedtuple -from xml.sax.saxutils import escape - -from bok_choy.promise import EmptyPromise -from selenium.common.exceptions import NoSuchElementException -from selenium.webdriver import ActionChains -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support.ui import WebDriverWait -from six.moves import range -from workbench import scenarios -from xblockutils.base_test import SeleniumBaseTest -from xblockutils.resources import ResourceLoader - -from drag_and_drop_v2.default_data import (BOTTOM_ZONE_ID, BOTTOM_ZONE_TITLE, - DEFAULT_DATA, FINISH_FEEDBACK, - ITEM_ANY_ZONE_FEEDBACK, - ITEM_ANY_ZONE_NAME, - ITEM_BOTTOM_ZONE_NAME, - ITEM_CORRECT_FEEDBACK_BOTTOM, - ITEM_CORRECT_FEEDBACK_MIDDLE, - ITEM_CORRECT_FEEDBACK_TOP, - ITEM_INCORRECT_FEEDBACK, - ITEM_MIDDLE_ZONE_NAME, - ITEM_NO_ZONE_FEEDBACK, - ITEM_NO_ZONE_NAME, - ITEM_TOP_ZONE_NAME, MIDDLE_ZONE_ID, - MIDDLE_ZONE_TITLE, START_FEEDBACK, - TOP_ZONE_ID, TOP_ZONE_TITLE) -from drag_and_drop_v2.utils import Constants - -# Globals ########################################################### - -loader = ResourceLoader(__name__) - - -# Classes ########################################################### - -ItemDefinition = namedtuple( # pylint: disable=invalid-name - "ItemDefinition", - [ - "item_id", - "item_name", - "image_url", - "zone_ids", - "zone_title", - "feedback_positive", - "feedback_negative", - ] -) - - -class BaseIntegrationTest(SeleniumBaseTest): - default_css_selector = '.themed-xblock.xblock--drag-and-drop' - module_name = __name__ - - _additional_escapes = { - '"': """, - "'": "'" - } - - # pylint: disable=too-many-arguments - # pylint: disable=bad-continuation - @classmethod - def _make_scenario_xml( - cls, display_name="Test DnDv2", show_title=True, problem_text="Question", completed=False, - show_problem_header=True, max_items_per_zone=0, data=None, mode=Constants.STANDARD_MODE - ): - if not data: - data = json.dumps(DEFAULT_DATA) - return """ - - - - """.format( - display_name=escape(display_name), - show_title=show_title, - problem_text=escape(problem_text), - show_problem_header=show_problem_header, - completed=completed, - max_items_per_zone=max_items_per_zone, - mode=mode, - data=escape(data, cls._additional_escapes) - ) - - def _get_custom_scenario_xml(self, filename): - data = loader.load_unicode(filename) - return "".format( - data=escape(data, self._additional_escapes) - ) - - def _add_scenario(self, identifier, title, xml): - scenarios.add_xml_scenario(identifier, title, xml) - self.addCleanup(scenarios.remove_scenario, identifier) - - def _get_items(self): - items_container = self._page.find_element_by_css_selector('.item-bank') - return items_container.find_elements_by_css_selector('.option') - - def _get_zones(self): - return self._page.find_elements_by_css_selector(".drag-container .zone") - - def _get_popup(self): - return self._page.find_element_by_css_selector(".popup") - - def _get_popup_wrapper(self): - return self._page.find_element_by_css_selector(".popup-wrapper") - - def _get_popup_content(self): - return self._page.find_element_by_css_selector(".popup .popup-content") - - def _get_keyboard_help(self): - return self._page.find_element_by_css_selector(".keyboard-help") - - def _get_keyboard_help_button(self): - return self._page.find_element_by_css_selector(".keyboard-help-button") - - def _get_keyboard_help_dialog(self): - return self._page.find_element_by_css_selector(".keyboard-help-dialog") - - def _get_go_to_beginning_button(self): - return self._page.find_element_by_css_selector('.go-to-beginning-button') - - def _get_reset_button(self): - return self._page.find_element_by_css_selector('.problem-action-button-wrapper .reset') - - def _get_show_answer_button(self): - return self._page.find_element_by_css_selector('.problem-action-button-wrapper .show') - - def _get_submit_button(self): - return self._page.find_element_by_css_selector('.submit-attempt-container .submit') - - def _get_attempts_info(self): - return self._page.find_element_by_css_selector('.submission-feedback') - - def _get_feedback(self): - return self._page.find_element_by_css_selector(".feedback-content") - - def _get_feedback_message(self): - return self._page.find_element_by_css_selector(".feedback .message") - - def _get_explanation(self): - return self._page.find_element_by_css_selector(".solution-span") - - def scroll_down(self, pixels=50): - self.browser.execute_script("$(window).scrollTop({})".format(pixels)) - - def is_element_in_viewport(self, element): - """Determines if the element lies at least partially in the viewport.""" - viewport = self.browser.execute_script( - "return {" - "top: window.scrollY," - "left: window.scrollX," - "bottom: window.scrollY + window.outerHeight," - "right: window.scrollX + window.outerWidth" - "};" - ) - - return all([ - any([ - viewport["top"] <= element.rect["y"] <= viewport["bottom"], - viewport["top"] <= element.rect["y"] + element.rect["height"] <= viewport["bottom"] - ]), - any([ - viewport["left"] <= element.rect["x"] <= viewport["right"], - viewport["left"] <= element.rect["x"] + element.rect["width"] <= viewport["right"] - ]) - ]) - - def _get_style(self, selector, style, computed=True): - if computed: - query = 'return getComputedStyle($("{selector}").get(0)).{style}' - else: - query = 'return $("{selector}").get(0).style.{style}' - return self.browser.execute_script(query.format(selector=selector, style=style)) - - def assertFocused(self, element): - focused_element = self.browser.switch_to.active_element - self.assertTrue(element == focused_element, 'expected element to have focus') - - def assertNotFocused(self, element): - focused_element = self.browser.switch_to.active_element - self.assertTrue(element != focused_element, 'expected element to not have focus') - - @staticmethod - def get_element_html(element): - return element.get_attribute('innerHTML').strip() - - @staticmethod - def get_element_classes(element): - return element.get_attribute('class').split() - - def wait_until_html_in(self, html, elem): - wait = WebDriverWait(elem, 2) - wait.until(lambda e: html in e.get_attribute('innerHTML'), - u"{} should be in {}".format(html, elem.get_attribute('innerHTML'))) - - @staticmethod - def wait_until_has_class(class_name, elem): - wait = WebDriverWait(elem, 2) - wait.until(lambda e: class_name in e.get_attribute('class').split(), - u"Class name {} not in {}".format(class_name, elem.get_attribute('class'))) - - def wait_for_ajax(self, timeout=15): - """ - Wait for jQuery to be loaded and for all ajax requests to finish. - Same as bok-choy's PageObject.wait_for_ajax() - """ - def is_ajax_finished(): - """ Check if all the ajax calls on the current page have completed. """ - return self.browser.execute_script("return typeof(jQuery)!='undefined' && jQuery.active==0") - - EmptyPromise(is_ajax_finished, "Finished waiting for ajax requests.", timeout=timeout).fulfill() - - -class DefaultDataTestMixin(object): - """ - Provides a test scenario with default options. - """ - PAGE_TITLE = 'Drag and Drop v2' - PAGE_ID = 'drag_and_drop_v2' - - items_map = { - 0: ItemDefinition( - 0, ITEM_TOP_ZONE_NAME, "", [TOP_ZONE_ID], TOP_ZONE_TITLE, - ITEM_CORRECT_FEEDBACK_TOP, ITEM_INCORRECT_FEEDBACK - ), - 1: ItemDefinition( - 1, ITEM_MIDDLE_ZONE_NAME, "", [MIDDLE_ZONE_ID], MIDDLE_ZONE_TITLE, - ITEM_CORRECT_FEEDBACK_MIDDLE, ITEM_INCORRECT_FEEDBACK - ), - 2: ItemDefinition( - 2, ITEM_BOTTOM_ZONE_NAME, "", [BOTTOM_ZONE_ID], BOTTOM_ZONE_TITLE, - ITEM_CORRECT_FEEDBACK_BOTTOM, ITEM_INCORRECT_FEEDBACK - ), - 3: ItemDefinition( - 3, ITEM_ANY_ZONE_NAME, "", [MIDDLE_ZONE_ID, TOP_ZONE_ID, BOTTOM_ZONE_ID], MIDDLE_ZONE_TITLE, - ITEM_ANY_ZONE_FEEDBACK, ITEM_INCORRECT_FEEDBACK - ), - 4: ItemDefinition(4, ITEM_NO_ZONE_NAME, "", [], None, "", ITEM_NO_ZONE_FEEDBACK), - } - - all_zones = [ - (TOP_ZONE_ID, TOP_ZONE_TITLE), - (MIDDLE_ZONE_ID, MIDDLE_ZONE_TITLE), - (BOTTOM_ZONE_ID, BOTTOM_ZONE_TITLE) - ] - - feedback = { - "intro": START_FEEDBACK, - "final": FINISH_FEEDBACK, - } - - def _get_scenario_xml(self): # pylint: disable=no-self-use - return "" - - -class InteractionTestBase(object): - POPUP_ERROR_CLASS = "popup-incorrect" - - def setUp(self): - super(InteractionTestBase, self).setUp() - - scenario_xml = self._get_scenario_xml() - self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml) - self._page = self.go_to_page(self.PAGE_TITLE) - # Resize window so that the entire drag container is visible. - # Selenium has issues when dragging to an area that is off screen. - self.browser.set_window_size(1024, 1024) - wait = WebDriverWait(self.browser, 2) - wait.until(lambda browser: browser.get_window_size()["width"] == 1024) - - @staticmethod - def _get_items_with_zone(items_map): - return { - item_key: definition for item_key, definition in items_map.items() - if definition.zone_ids != [] - } - - @staticmethod - def _get_items_without_zone(items_map): - return { - item_key: definition for item_key, definition in items_map.items() - if definition.zone_ids == [] - } - - @staticmethod - def _get_items_by_zone(items_map): - zone_ids = {definition.zone_ids[0] for _, definition in items_map.items() if definition.zone_ids} - return { - zone_id: {item_key: definition for item_key, definition in items_map.items() - if definition.zone_ids and definition.zone_ids[0] is zone_id} - for zone_id in zone_ids - } - - @staticmethod - def _get_incorrect_zone_for_item(item, zones): - """Returns the first zone that is not correct for this item.""" - zone_id = None - zone_title = None - for z_id, z_title in zones: - if z_id not in item.zone_ids: - zone_id = z_id - zone_title = z_title - break - return [zone_id, zone_title] - - def _get_item_by_value(self, item_value): - return self._page.find_elements_by_xpath(".//div[@data-value='{item_id}']".format(item_id=item_value))[0] - - def _get_unplaced_item_by_value(self, item_value): - items_container = self._get_item_bank() - return items_container.find_elements_by_xpath(".//div[@data-value='{item_id}']".format(item_id=item_value))[0] - - def _get_placed_item_by_value(self, item_value): - items_container = self._page.find_element_by_css_selector('.target') - return items_container.find_elements_by_xpath(".//div[@data-value='{item_id}']".format(item_id=item_value))[0] - - def _get_zone_by_id(self, zone_id): - zones_container = self._page.find_element_by_css_selector('.target') - return zones_container.find_elements_by_xpath(".//div[@data-uid='{zone_id}']".format(zone_id=zone_id))[0] - - def _get_dialog_components(self, dialog): # pylint: disable=no-self-use - dialog_modal_overlay = dialog.find_element_by_css_selector('.modal-window-overlay') - dialog_modal = dialog.find_element_by_css_selector('.modal-window') - return dialog_modal_overlay, dialog_modal - - def _get_dialog_dismiss_button(self, dialog_modal): # pylint: disable=no-self-use - return dialog_modal.find_element_by_css_selector('.modal-dismiss-button') - - def _get_item_bank(self): - return self._page.find_element_by_css_selector('.item-bank') - - def _get_zone_position(self, zone_id): - return self.browser.execute_script( - 'return $("div[data-uid=\'{zone_id}\']").prevAll(".zone").length'.format(zone_id=zone_id) - ) - - def _get_draggable_property(self, item_value): - """ - Returns the value of the 'draggable' property of item. - - Selenium has the element.get_attribute method that looks up properties and attributes, - but for some reason it *always* returns "true" for the 'draggable' property, event though - both the HTML attribute and the DOM property are set to false. - We work around that selenium bug by using JavaScript to get the correct value of 'draggable'. - """ - script = "return $('.option[data-value={}]').prop('draggable')".format(item_value) - return self.browser.execute_script(script) - - def assertDraggable(self, item_value): - self.assertTrue(self._get_draggable_property(item_value)) - - def assertNotDraggable(self, item_value): - self.assertFalse(self._get_draggable_property(item_value)) - - @staticmethod - def wait_until_ondrop_xhr_finished(elem): - """ - Waits until the XHR request triggered by dropping the item finishes loading. - """ - wait = WebDriverWait(elem, 2) - # While the XHR is in progress, a spinner icon is shown inside the item. - # When the spinner disappears, we can assume that the XHR request has finished. - wait.until( - lambda e: 'fa-spinner' not in e.get_attribute('innerHTML'), - u"Spinner should not be in {}".format(elem.get_attribute('innerHTML')) - ) - - def place_item(self, item_value, zone_id, action_key=None, wait=True): - """ - Place item with ID of item_value into zone with ID of zone_id. - zone_id=None means place item back to the item bank. - action_key=None means simulate mouse drag/drop instead of placing the item with keyboard. - """ - if action_key is None: - self.drag_item_to_zone(item_value, zone_id) - else: - self.move_item_to_zone(item_value, zone_id, action_key) - if wait: - self.wait_for_ajax() - - def drag_item_to_zone(self, item_value, zone_id): - """ - Drag item to desired zone using mouse interaction. - zone_id=None means drag item back to the item bank. - """ - element = self._get_item_by_value(item_value) - if zone_id is None: - target = self._get_item_bank() - else: - target = self._get_zone_by_id(zone_id) - action_chains = ActionChains(self.browser) - action_chains.drag_and_drop(element, target).perform() - - def move_item_to_zone(self, item_value, zone_id, action_key): - """ - Place item to descired zone using keybard interaction. - zone_id=None means place item back into the item bank. - """ - # Focus on the item, then press the action key: - item = self._get_item_by_value(item_value) - item.send_keys("") - item.send_keys(action_key) - # Focus is on first *zone* now - self.assert_item_grabbed(item) - # Get desired zone and figure out how many times we have to press Tab to focus the zone. - if zone_id is None: # moving back to the bank - zone = self._get_item_bank() - # When switching focus between zones in keyboard placement mode, - # the item bank always gets focused last (after all regular zones), - # so we have to press Tab once for every regular zone to move focus to the item bank. - tab_press_count = len(self.all_zones) - else: - zone = self._get_zone_by_id(zone_id) - # The number of times we have to press Tab to focus the desired zone equals the zero-based - # position of the zone (zero presses for first zone, one press for second zone, etc). - tab_press_count = self._get_zone_position(zone_id) - for _ in range(tab_press_count): - ActionChains(self.browser).send_keys(Keys.TAB).perform() - zone.send_keys(action_key) - - def assert_item_grabbed(self, item): - self.assertEqual(item.get_attribute('aria-grabbed'), 'true') - - def assert_item_not_grabbed(self, item): - self.assertEqual(item.get_attribute('aria-grabbed'), 'false') - - def assert_placed_item(self, item_value, zone_title, assessment_mode=False): - item = self._get_placed_item_by_value(item_value) - self.wait_until_visible(item) - self.wait_until_ondrop_xhr_finished(item) - item_content = item.find_element_by_css_selector('.item-content') - self.wait_until_visible(item_content) - item_description = item.find_element_by_css_selector('.sr.description') - self.wait_until_visible(item_description) - item_description_id = '-item-{}-description'.format(item_value) - - self.assertEqual(item.get_attribute('aria-grabbed'), 'false') - self.assertEqual(item_content.get_attribute('aria-describedby'), item_description_id) - self.assertEqual(item_description.get_attribute('id'), item_description_id) - if assessment_mode: - self.assertDraggable(item_value) - self.assertEqual(item.get_attribute('class'), 'option') - self.assertEqual(item.get_attribute('tabindex'), '0') - description = 'Placed in: {}' - else: - self.assertNotDraggable(item_value) - self.assertEqual(item.get_attribute('class'), 'option fade') - self.assertIsNone(item.get_attribute('tabindex')) - description = 'Correctly placed in: {}' - - # An item with multiple drop zones could be located in any one of these - # zones. In that case, zone_title will be a list, and we need to check - # whether the zone info in the description of the item matches any of - # the zones in that list. - if isinstance(zone_title, list): - self.assertIn(item_description.text, [description.format(title) for title in zone_title]) - else: - self.assertEqual(item_description.text, description.format(zone_title)) - - def assert_reverted_item(self, item_value): - item = self._get_item_by_value(item_value) - self.wait_until_visible(item) - self.wait_until_ondrop_xhr_finished(item) - item_content = item.find_element_by_css_selector('.item-content') - - self.assertDraggable(item_value) - self.assertEqual(item.get_attribute('class'), 'option') - self.assertEqual(item.get_attribute('tabindex'), '0') - self.assertEqual(item.get_attribute('aria-grabbed'), 'false') - self.assertEqual(item_content.get_attribute('aria-describedby'), None) - - try: - item.find_element_by_css_selector('.sr.description') - self.fail("Description element exists") - except NoSuchElementException: - pass - - def place_decoy_items(self, items_map, action_key): - decoy_items = self._get_items_without_zone(items_map) - # Place decoy items into first available zone. - zone_id, zone_title = self.all_zones[0] - for definition in decoy_items.values(): - self.place_item(definition.item_id, zone_id, action_key) - self.assert_placed_item(definition.item_id, zone_title, assessment_mode=True) - - def assert_decoy_items(self, items_map, assessment_mode=False): - decoy_items = self._get_items_without_zone(items_map) - for item_key in decoy_items: - item = self._get_item_by_value(item_key) - self.assertEqual(item.get_attribute('aria-grabbed'), 'false') - if assessment_mode: - self.assertDraggable(item_key) - self.assertEqual(item.get_attribute('class'), 'option') - else: - self.assertNotDraggable(item_key) - self.assertEqual(item.get_attribute('class'), 'option fade') - - def _switch_to_block(self, idx): - """ Only needed if there are multiple blocks on the page. """ - self._page = self.browser.find_elements_by_css_selector(self.default_css_selector)[idx] - self.scroll_down(0) - - def assert_popup_correct(self, popup): - self.assertNotIn(self.POPUP_ERROR_CLASS, popup.get_attribute('class')) - - def assert_popup_incorrect(self, popup): - self.assertIn(self.POPUP_ERROR_CLASS, popup.get_attribute('class')) - - def assert_button_enabled(self, submit_button, enabled=True): - self.assertEqual(submit_button.is_enabled(), enabled) - - def assert_reader_feedback_messages(self, expected_message_lines): - expected_paragraphs = ['

{}

'.format(line) for line in expected_message_lines] - expected_html = ''.join(expected_paragraphs) - feedback_area = self._page.find_element_by_css_selector('.reader-feedback-area') - actual_html = feedback_area.get_attribute('innerHTML') - self.assertEqual(actual_html, expected_html) diff --git a/tests/integration/test_custom_data_render.py b/tests/integration/test_custom_data_render.py deleted file mode 100644 index dd0808afd..000000000 --- a/tests/integration/test_custom_data_render.py +++ /dev/null @@ -1,67 +0,0 @@ -from .test_base import BaseIntegrationTest - - -class TestCustomDataDragAndDropRendering(BaseIntegrationTest): - PAGE_TITLE = 'Drag and Drop v2' - PAGE_ID = 'drag_and_drop_v2' - - def setUp(self): - super(TestCustomDataDragAndDropRendering, self).setUp() - - scenario_xml = self._get_custom_scenario_xml("data/test_html_data.json") - self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml) - - self._page = self.go_to_page(self.PAGE_TITLE) - - header1 = self.browser.find_element_by_css_selector('h1') - self.assertEqual(header1.text, 'XBlock: ' + self.PAGE_TITLE) - - def test_items_rendering(self): - items = self._get_items() - - self.assertEqual(len(items), 3) - self.assertIn('1', self.get_element_html(items[0])) - self.assertIn('2', self.get_element_html(items[1])) - self.assertIn('X', self.get_element_html(items[2])) - - def test_html_title_renders_properly(self): - """ - Tests HTML titles are rendered properly - """ - zones = self._get_zones() - self.assertEqual(u'Zone\ndroppable\nNo items placed here', zones[0].text) - self.assertNotEqual(u'Zone -1\ndroppable\nNo items placed here', zones[0].text) - - def test_background_image(self): - bg_image = self.browser.find_element_by_css_selector(".xblock--drag-and-drop .target-img") - custom_image_url = ( - "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciI" - "HdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg==" - ) - custom_image_description = "This describes the target image" - self.assertEqual(bg_image.get_attribute("src"), custom_image_url) - self.assertEqual(bg_image.get_attribute("alt"), custom_image_description) - - -class TestZoneTitleAsHTML(BaseIntegrationTest): - PAGE_TITLE = 'Drag and Drop v2' - PAGE_ID = 'drag_and_drop_v2' - - def setUp(self): - super(TestZoneTitleAsHTML, self).setUp() - - scenario_xml = self._get_custom_scenario_xml("data/test_html_titles_data.json") - self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml) - - self._page = self.go_to_page(self.PAGE_TITLE) - - header1 = self.browser.find_element_by_css_selector('h1') - self.assertEqual(header1.text, 'XBlock: ' + self.PAGE_TITLE) - - def test_html_title_renders_properly(self): - """ - Tests HTML titles are rendered properly - """ - zones = self._get_zones() - self.assertEqual('Zone\ndroppable\nNo items placed here', zones[0].text) - self.assertNotEqual('Zone -1\ndroppable\nNo items placed here', zones[0].text) diff --git a/tests/integration/test_events.py b/tests/integration/test_events.py deleted file mode 100644 index b189ab77b..000000000 --- a/tests/integration/test_events.py +++ /dev/null @@ -1,241 +0,0 @@ -from __future__ import absolute_import - -from ddt import data, ddt, unpack -from mock import Mock, patch -from selenium.webdriver.common.keys import Keys -from workbench.runtime import WorkbenchRuntime - -from drag_and_drop_v2.default_data import (BOTTOM_ZONE_ID, - ITEM_CORRECT_FEEDBACK_TOP, - ITEM_INCORRECT_FEEDBACK, - ITEM_MIDDLE_ZONE_NAME, - ITEM_TOP_ZONE_NAME, MIDDLE_ZONE_ID, - MIDDLE_ZONE_TITLE, TOP_ZONE_ID, - TOP_ZONE_TITLE) -from tests.integration.test_base import (BaseIntegrationTest, - DefaultDataTestMixin, - InteractionTestBase, ItemDefinition) -from tests.integration.test_interaction import ParameterizedTestsMixin -from tests.integration.test_interaction_assessment import ( - AssessmentTestMixin, DefaultAssessmentDataTestMixin) - - -class BaseEventsTests(InteractionTestBase, BaseIntegrationTest): - def setUp(self): - mock = Mock() - context = patch.object(WorkbenchRuntime, 'publish', mock) - context.start() - self.addCleanup(context.stop) - self.publish = mock - super(BaseEventsTests, self).setUp() - - -@ddt -class EventsFiredTest(DefaultDataTestMixin, ParameterizedTestsMixin, BaseEventsTests): - """ - Tests that the analytics events are fired and in the proper order. - """ - # These events must be fired in this order. - scenarios = ( - { - 'name': 'edx.drag_and_drop_v2.loaded', - 'data': {}, - }, - { - 'name': 'edx.drag_and_drop_v2.item.picked_up', - 'data': {'item_id': 0}, - }, - { - 'name': 'grade', - 'data': {'max_value': 1, 'value': (2.0 / 5), 'only_if_higher': None}, - }, - { - 'name': 'progress', - 'data': {} - }, - { - 'name': 'edx.drag_and_drop_v2.item.dropped', - 'data': { - 'is_correct': True, - 'item': ITEM_TOP_ZONE_NAME, - 'item_id': 0, - 'location': TOP_ZONE_TITLE, - 'location_id': TOP_ZONE_ID, - }, - }, - { - 'name': 'edx.drag_and_drop_v2.feedback.opened', - 'data': { - 'content': ITEM_CORRECT_FEEDBACK_TOP, - 'truncated': False, - }, - }, - { - 'name': 'edx.drag_and_drop_v2.feedback.closed', - 'data': { - 'manually': True, - 'content': ITEM_CORRECT_FEEDBACK_TOP, - 'truncated': False, - }, - }, - ) - - def _get_scenario_xml(self): # pylint: disable=no-self-use - return "" - - @data(*enumerate(scenarios)) # pylint: disable=star-args - @unpack - def test_event(self, index, event): - self.parameterized_item_positive_feedback_on_good_move_standard(self.items_map) - _, name, published_data = self.publish.call_args_list[index][0] - self.assertEqual(name, event['name']) - self.assertEqual(published_data, event['data']) - - -@ddt -class AssessmentEventsFiredTest(DefaultAssessmentDataTestMixin, AssessmentTestMixin, BaseEventsTests): - scenarios = ( - { - 'name': 'edx.drag_and_drop_v2.loaded', - 'data': {}, - }, - { - 'name': 'edx.drag_and_drop_v2.item.picked_up', - 'data': {'item_id': 0}, - }, - { - 'name': 'edx.drag_and_drop_v2.item.dropped', - 'data': { - 'is_correct': False, - 'item': ITEM_TOP_ZONE_NAME, - 'item_id': 0, - 'location': MIDDLE_ZONE_TITLE, - 'location_id': MIDDLE_ZONE_ID, - }, - }, - { - 'name': 'edx.drag_and_drop_v2.item.picked_up', - 'data': {'item_id': 1}, - }, - { - 'name': 'edx.drag_and_drop_v2.item.dropped', - 'data': { - 'is_correct': False, - 'item': ITEM_MIDDLE_ZONE_NAME, - 'item_id': 1, - 'location': TOP_ZONE_TITLE, - 'location_id': TOP_ZONE_ID, - }, - }, - { - 'name': 'grade', - 'data': {'max_value': 1, 'value': (1.0 / 5), 'only_if_higher': None}, - }, - { - 'name': 'progress', - 'data': {} - }, - { - 'name': 'edx.drag_and_drop_v2.feedback.opened', - 'data': { - 'content': "\n".join([ITEM_INCORRECT_FEEDBACK, ITEM_INCORRECT_FEEDBACK]), - 'truncated': False, - }, - }, - ) - - def test_event(self): - self.scroll_down(pixels=100) - - self.place_item(0, MIDDLE_ZONE_ID) - self.wait_until_ondrop_xhr_finished(self._get_item_by_value(0)) - self.place_item(1, TOP_ZONE_ID) - self.wait_until_ondrop_xhr_finished(self._get_item_by_value(0)) - - self.click_submit() - self.wait_for_ajax() - for index, event in enumerate(self.scenarios): - _, name, published_data = self.publish.call_args_list[index][0] - self.assertEqual(name, event['name']) - self.assertEqual(published_data, event['data']) - - def test_grade(self): - """ - Test grading after submitting solution in assessment mode - """ - self.place_item(0, TOP_ZONE_ID, Keys.RETURN) # Correctly placed item - self.place_item(1, BOTTOM_ZONE_ID, Keys.RETURN) # Incorrectly placed item - self.place_item(4, MIDDLE_ZONE_ID, Keys.RETURN) # Incorrectly placed decoy - self.click_submit() - - events = self.publish.call_args_list - published_grade = next((event[0][2] for event in events if event[0][1] == 'grade')) - expected_grade = {'max_value': 1, 'value': (1.0 / 5.0), 'only_if_higher': None} - self.assertEqual(published_grade, expected_grade) - - -@ddt -class ItemDroppedEventTest(DefaultDataTestMixin, BaseEventsTests): - """ - Test that the item.dropped event behaves properly. - - """ - items_map = { - 0: ItemDefinition(0, "Has name", "", 'zone-1', "Zone 1", "Yes", "No"), - 1: ItemDefinition(1, "", "https://placehold.it/100x100", 'zone-2', "Zone 2", "Yes", "No"), - } - - scenarios = ( - ( - ['zone-1', 'zone-2'], - [ - { - 'is_correct': True, - 'item': "Has name", - 'item_id': 0, - 'location': 'Zone 1', - 'location_id': 'zone-1' - }, - { - 'is_correct': True, - 'item': "https://placehold.it/100x100", - 'item_id': 1, - 'location': 'Zone 2', - 'location_id': 'zone-2' - } - ], - ), - ( - ['zone-2', 'zone-1'], - [ - { - 'is_correct': False, - 'item': "Has name", - 'item_id': 0, - 'location': 'Zone 2', - 'location_id': 'zone-2' - }, - { - 'is_correct': False, - 'item': "https://placehold.it/100x100", - 'item_id': 1, - 'location': 'Zone 1', - 'location_id': 'zone-1' - } - ], - ), - ) - - def _get_scenario_xml(self): - return self._get_custom_scenario_xml("data/test_item_dropped.json") - - @data(*scenarios) # pylint: disable=star-args - @unpack - def test_item_dropped_event(self, placement, expected_events): - for i, zone in enumerate(placement): - self.place_item(i, zone, Keys.RETURN) - - events = self.publish.call_args_list - event_name = 'edx.drag_and_drop_v2.item.dropped' - published_events = [event[0][2] for event in events if event[0][1] == event_name] - self.assertEqual(published_events, expected_events) diff --git a/tests/integration/test_interaction.py b/tests/integration/test_interaction.py deleted file mode 100644 index ea3c6307a..000000000 --- a/tests/integration/test_interaction.py +++ /dev/null @@ -1,744 +0,0 @@ -# pylint: disable=too-many-lines -# -*- coding: utf-8 -*- - -# Imports ########################################################### - -from __future__ import absolute_import - -import re -import time - -from ddt import data, ddt, unpack -from selenium.common.exceptions import WebDriverException -from selenium.webdriver import ActionChains -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support.ui import WebDriverWait -from xblockutils.resources import ResourceLoader - -from tests.integration.test_base import (DefaultDataTestMixin, - InteractionTestBase, ItemDefinition) - -from .test_base import BaseIntegrationTest - -# Globals ########################################################### - -loader = ResourceLoader(__name__) - -# Classes ########################################################### - -ITEM_DRAG_KEYBOARD_KEYS = (None, Keys.RETURN, Keys.CONTROL+'m') - - -class ParameterizedTestsMixin(object): - def _test_popup_focus_and_close(self, popup, action_key): - dismiss_popup_button = popup.find_element_by_css_selector('.close-feedback-popup-button') - self.assertFocused(dismiss_popup_button) - # Assert focus is trapped - trying to tab out of the popup does not work, focus remains on the close button. - ActionChains(self.browser).send_keys(Keys.TAB).perform() - self.assertFocused(dismiss_popup_button) - # Close the popup now. - if action_key: - ActionChains(self.browser).send_keys(Keys.RETURN).perform() - else: - dismiss_popup_button.click() - self.assertFalse(popup.is_displayed()) - # Assert focus moves to first enabled button in item bank after closing the popup. - focusable_items_in_bank = [item for item in self._get_items() if item.get_attribute('tabindex') == '0'] - if len(focusable_items_in_bank) > 0: - self.assertFocused(focusable_items_in_bank[0]) - - def _test_next_tab_goes_to_go_to_beginning_button(self): - go_to_beginning_button = self._get_go_to_beginning_button() - self.assertNotFocused(go_to_beginning_button) - ActionChains(self.browser).send_keys(Keys.TAB).perform() - self.assertFocused(go_to_beginning_button) - - def parameterized_item_positive_feedback_on_good_move_standard( - self, items_map, scroll_down=100, action_key=None, feedback=None - ): - if feedback is None: - feedback = self.feedback - - popup = self._get_popup() - feedback_popup_content = self._get_popup_content() - - # Scroll drop zones into view to make sure Selenium can successfully drop items - self.scroll_down(pixels=scroll_down) - - items_with_zones = list(self._get_items_with_zone(items_map).values()) - for i, definition in enumerate(items_with_zones): - self.place_item(definition.item_id, definition.zone_ids[0], action_key) - self.wait_until_ondrop_xhr_finished(self._get_item_by_value(definition.item_id)) - self.assert_placed_item(definition.item_id, definition.zone_title, assessment_mode=False) - feedback_popup_html = feedback_popup_content.get_attribute('innerHTML') - self.assertEqual(feedback_popup_html, "

{}

".format(definition.feedback_positive)) - self.assert_popup_correct(popup) - self.assertTrue(popup.is_displayed()) - expected_sr_texts = [definition.feedback_positive] - if i == len(items_with_zones) - 1: - # We just dropped the last item, so the problem is done and we should see the final feedback. - overall_feedback = feedback['final'] - else: - overall_feedback = feedback['intro'] - expected_sr_texts.append(overall_feedback) - self.assert_reader_feedback_messages(expected_sr_texts) - self._test_popup_focus_and_close(popup, action_key) - - def parameterized_item_positive_feedback_on_good_move_assessment( - self, items_map, scroll_down=100, action_key=None, feedback=None - ): - if feedback is None: - feedback = self.feedback - - popup = self._get_popup() - feedback_popup_content = self._get_popup_content() - - # Scroll drop zones into view to make sure Selenium can successfully drop items - self.scroll_down(pixels=scroll_down) - - items_with_zones = list(self._get_items_with_zone(items_map).values()) - for definition in items_with_zones: - self.place_item(definition.item_id, definition.zone_ids[0], action_key) - self.wait_until_ondrop_xhr_finished(self._get_item_by_value(definition.item_id)) - self.assert_placed_item(definition.item_id, definition.zone_title, assessment_mode=True) - feedback_popup_html = feedback_popup_content.get_attribute('innerHTML') - self.assertEqual(feedback_popup_html, '') - self.assertFalse(popup.is_displayed()) - self.assert_reader_feedback_messages([]) - if action_key: - # Next TAB keypress should move focus to "Go to Beginning button" - self._test_next_tab_goes_to_go_to_beginning_button() - - def parameterized_item_negative_feedback_on_bad_move_standard( - self, items_map, all_zones, scroll_down=100, action_key=None, feedback=None - ): - if feedback is None: - feedback = self.feedback - - popup = self._get_popup() - feedback_popup_content = self._get_popup_content() - - # Scroll drop zones into view to make sure Selenium can successfully drop items - self.scroll_down(pixels=scroll_down) - - for definition in items_map.values(): - zone_id, _ = self._get_incorrect_zone_for_item(definition, all_zones) - if zone_id is not None: # Some items may be placed in any zone, ignore those. - self.place_item(definition.item_id, zone_id, action_key) - self.wait_until_html_in(definition.feedback_negative, feedback_popup_content) - self.assert_popup_incorrect(popup) - self.assertTrue(popup.is_displayed()) - self.assert_reverted_item(definition.item_id) - expected_sr_texts = [definition.feedback_negative, feedback['intro']] - self.assert_reader_feedback_messages(expected_sr_texts) - self._test_popup_focus_and_close(popup, action_key) - - def parameterized_item_negative_feedback_on_bad_move_assessment( - self, items_map, all_zones, scroll_down=100, action_key=None, feedback=None - ): - if feedback is None: - feedback = self.feedback - - popup = self._get_popup() - feedback_popup_content = self._get_popup_content() - - # Scroll drop zones into view to make sure Selenium can successfully drop items - self.scroll_down(pixels=scroll_down) - - for definition in items_map.values(): - zone_id, zone_title = self._get_incorrect_zone_for_item(definition, all_zones) - if zone_id is not None: # Some items may be placed in any zone, ignore those. - self.place_item(definition.item_id, zone_id, action_key) - self.wait_until_ondrop_xhr_finished(self._get_item_by_value(definition.item_id)) - feedback_popup_html = feedback_popup_content.get_attribute('innerHTML') - self.assertEqual(feedback_popup_html, '') - self.assertFalse(popup.is_displayed()) - self.assert_placed_item(definition.item_id, zone_title, assessment_mode=True) - self.assert_reader_feedback_messages([]) - if action_key: - self._test_next_tab_goes_to_go_to_beginning_button() - - def parameterized_move_items_between_zones(self, items_map, all_zones, scroll_down=100, action_key=None): - # Scroll drop zones into view to make sure Selenium can successfully drop items - self.scroll_down(pixels=scroll_down) - - # Take each item, place it into first zone, then continue moving it until it has visited all zones. - for item_key in items_map.keys(): - for zone_id, zone_title in all_zones: - self.place_item(item_key, zone_id, action_key) - self.assert_placed_item(item_key, zone_title, assessment_mode=True) - if action_key: - self._test_next_tab_goes_to_go_to_beginning_button() - # Finally, move them all back to the bank. - self.place_item(item_key, None, action_key) - self.assert_reverted_item(item_key) - - def parameterized_cannot_move_items_between_zones(self, items_map, all_zones, scroll_down=100, action_key=None): - # Scroll drop zones into view to make sure Selenium can successfully drop items - self.scroll_down(pixels=scroll_down) - - # Take each item an assigned zone, place it into the correct zone, then ensure it cannot be moved to other. - # zones or back to the bank. - for item_key, definition in items_map.items(): - if definition.zone_ids: # skip decoy items - self.place_item(definition.item_id, definition.zone_ids[0], action_key) - self.assert_placed_item(definition.item_id, definition.zone_title, assessment_mode=False) - if action_key: - item = self._get_item_by_value(definition.item_id) - # When using the keyboard, ensure that dropped items cannot get "grabbed". - # Assert item has no tabindex. - self.assertIsNone(item.get_attribute('tabindex')) - # Focus on the item, then press the action key: - ActionChains(self.browser).move_to_element(item).send_keys(action_key).perform() - # Assert item is not grabbed. - self.assertEqual(item.get_attribute('aria-grabbed'), 'false') - else: - # When using the mouse, try to drag items and observe it doesn't work. - for zone_id, _zone_title in all_zones: - if zone_id not in definition.zone_ids: - self.place_item(item_key, zone_id, action_key) - self.assert_placed_item(definition.item_id, definition.zone_title, assessment_mode=False) - # Finally, try to move item back to the bank. - self.place_item(item_key, None, action_key) - self.assert_placed_item(definition.item_id, definition.zone_title, assessment_mode=False) - - def parameterized_final_feedback_and_reset( - self, items_map, feedback, scroll_down=100, action_key=None, assessment_mode=False - ): - feedback_message = self._get_feedback_message() - self.assertEqual(self.get_element_html(feedback_message), feedback['intro']) # precondition check - - items = self._get_items_with_zone(items_map) - - def get_locations(): - return {item_id: self._get_item_by_value(item_id).location for item_id in items.keys()} - - initial_locations = get_locations() - - # Scroll drop zones into view to make sure Selenium can successfully drop items - self.scroll_down(pixels=scroll_down) - - for item_key, definition in items.items(): - self.place_item(definition.item_id, definition.zone_ids[0], action_key) - self.assert_placed_item(definition.item_id, definition.zone_title, assessment_mode=assessment_mode) - - if assessment_mode: - # In assessment mode we also place decoy items onto the board, - # to make sure they are correctly reverted back to the bank on problem reset. - self.place_decoy_items(items_map, action_key) - else: - self.wait_until_html_in(feedback['final'], self._get_feedback_message()) - - # Check decoy items - self.assert_decoy_items(items_map, assessment_mode=assessment_mode) - - # Scroll "Reset problem" button into view to make sure Selenium can successfully click it - self.scroll_down(pixels=scroll_down+150) - - reset = self._get_reset_button() - if action_key is not None: # Using keyboard to interact with block - reset.send_keys(Keys.RETURN) - else: - reset.click() - - self.wait_until_html_in(feedback['intro'], self._get_feedback_message()) - time.sleep(0.1) - - locations_after_reset = get_locations() - for item_key in items.keys(): - self.assertDictEqual(locations_after_reset[item_key], initial_locations[item_key]) - self.assert_reverted_item(item_key) - - def interact_with_keyboard_help(self, scroll_down=100, use_keyboard=False): - keyboard_help_button = self._get_keyboard_help_button() - keyboard_help_dialog = self._get_keyboard_help_dialog() - dialog_modal_overlay, dialog_modal = self._get_dialog_components(keyboard_help_dialog) - dialog_dismiss_button = self._get_dialog_dismiss_button(dialog_modal) - - # Scroll "Keyboard help" button into view to make sure Selenium can successfully click it - self.scroll_down(pixels=scroll_down) - - if use_keyboard: - keyboard_help_button.send_keys(Keys.RETURN) - else: - keyboard_help_button.click() - - self.assertTrue(dialog_modal_overlay.is_displayed()) - self.assertTrue(dialog_modal.is_displayed()) - - if use_keyboard: - dialog_dismiss_button.send_keys(Keys.RETURN) - else: - dialog_dismiss_button.click() - - self.assertFalse(dialog_modal_overlay.is_displayed()) - self.assertFalse(dialog_modal.is_displayed()) - - if use_keyboard: # Check if "Keyboard Help" dialog can be dismissed using "ESC" - keyboard_help_button.send_keys(Keys.RETURN) - - self.assertTrue(dialog_modal_overlay.is_displayed()) - self.assertTrue(dialog_modal.is_displayed()) - - ActionChains(self.browser).send_keys(Keys.ESCAPE).perform() - - self.assertFalse(dialog_modal_overlay.is_displayed()) - self.assertFalse(dialog_modal.is_displayed()) - - -@ddt -class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, ParameterizedTestsMixin, BaseIntegrationTest): - """ - Testing interactions with Drag and Drop XBlock against default data. - All interactions are tested using mouse (action_key=None) and four different keyboard action keys. - If default data changes this will break. - """ - @data(*ITEM_DRAG_KEYBOARD_KEYS) - def test_item_positive_feedback_on_good_move(self, action_key): - self.parameterized_item_positive_feedback_on_good_move_standard(self.items_map, action_key=action_key) - - @data(*ITEM_DRAG_KEYBOARD_KEYS) - def test_item_negative_feedback_on_bad_move(self, action_key): - self.parameterized_item_negative_feedback_on_bad_move_standard( - self.items_map, self.all_zones, action_key=action_key - ) - - @data(*ITEM_DRAG_KEYBOARD_KEYS) - def test_cannot_move_items_between_zones(self, action_key): - self.parameterized_cannot_move_items_between_zones( - self.items_map, self.all_zones, action_key=action_key - ) - - def test_alt_text_image(self): - target_img = self._page.find_element_by_css_selector('.target-img') - alt_text = target_img.get_attribute('alt') - items_container = self._page.find_element_by_css_selector('.target') - if items_container.get_attribute('aria-describedby'): - self.assertEqual(items_container.get_attribute('aria-describedby').text, alt_text) - - def test_alt_text_keyboard_help_over_item(self): - for _, definition in self.items_map.items(): - item = self._get_unplaced_item_by_value(definition.item_id) - ActionChains(self.browser).move_to_element(item).perform() - self.assertEqual(item.find_element_by_css_selector('.sr.draggable').text, ", draggable") - item.send_keys("") - item.send_keys(Keys.ENTER) # grabbed an item - self.assertEqual(item.find_element_by_css_selector('.sr.draggable').text, ", draggable, grabbed") - item.send_keys(Keys.ESCAPE) - self.assertEqual(item.find_element_by_css_selector('.sr.draggable').text, ", draggable") - - def test_alt_text_for_zones(self): - self._get_popup() - self._get_popup_content() - self.scroll_down(pixels=100) - - # Place all items in zones where they belong - for definition in self._get_items_with_zone(self.items_map).values(): - self.place_item(definition.item_id, definition.zone_ids[0]) - - # Check if alt text appears for that item when the user tabs over the zone - for zone_id, items_dict in self._get_items_by_zone(self.items_map).items(): - if zone_id is None: - continue - zone = self._get_zone_by_id(zone_id) - zone_description = zone.find_element_by_id(zone.get_attribute('aria-describedby')).text - - # Iterate over all items placed in that zone and save a list of their descriptions - for _, definition in items_dict.items(): - item = self._get_placed_item_by_value(definition.item_id) - self.wait_until_visible(item) - item_content = item.find_element_by_css_selector('.item-content') - self.wait_until_visible(item_content) - self.assertTrue(item_content.text in zone_description) - - @data(*ITEM_DRAG_KEYBOARD_KEYS) - def test_final_feedback_and_reset(self, action_key): - self.parameterized_final_feedback_and_reset(self.items_map, self.feedback, action_key=action_key) - - @data(False, True) - def test_keyboard_help(self, use_keyboard): - self.interact_with_keyboard_help(use_keyboard=use_keyboard) - - def test_grade_display(self): - items_with_zones = list(self._get_items_with_zone(self.items_map).values()) - items_without_zones = list(self._get_items_without_zone(self.items_map).values()) - total_items = len(items_with_zones) + len(items_without_zones) - - progress = self._page.find_element_by_css_selector('.problem-progress') - self.assertEqual(progress.text, '1 point possible (ungraded)') - - # Place items into correct zones one by one: - for idx, item in enumerate(items_with_zones): - self.place_item(item.item_id, item.zone_ids[0]) - # The number of items in correct positions currently equals: - # the number of items already placed + any decoy items which should stay in the bank. - grade = (idx + 1 + len(items_without_zones)) / float(total_items) - formatted_grade = '{:.04f}'.format(grade) # display 4 decimal places - formatted_grade = re.sub(r'\.?0+$', '', formatted_grade) # remove trailing zeros - # Selenium does not see the refreshed text unless the text is in view (wtf??), so scroll back up. - self.scroll_down(pixels=0) - self.assertEqual(progress.text, '{}/1 point (ungraded)'.format(formatted_grade)) - - # After placing all items, we get the full score. - self.assertEqual(progress.text, '1/1 point (ungraded)') - - @data(*ITEM_DRAG_KEYBOARD_KEYS) - def test_cannot_select_multiple_items(self, action_key): - if action_key: - all_item_ids = list(self.items_map.keys()) - # Go through all items and select them all using the keyboard action key. - for item_id in all_item_ids: - item = self._get_item_by_value(item_id) - item.send_keys('') - item.send_keys(action_key) - # Item should be grabbed. - self.assert_item_grabbed(item) - # Other items should NOT be grabbed. - for other_item_id in all_item_ids: - if other_item_id != item_id: - other_item = self._get_item_by_value(other_item_id) - self.assert_item_not_grabbed(other_item) - - -class MultipleValidOptionsInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest): - - items_map = { - 0: ItemDefinition( - 0, - "Item", - "", - ['zone-1', 'zone-2'], - ["Zone 1", "Zone 2"], - ["Yes 1", "Yes 1"], - ["No 1", "No 1"] - ), - } - - def test_multiple_positive_feedback(self): - popup = self._get_popup() - feedback_popup_content = self._get_popup_content() - reset = self._get_reset_button() - self.scroll_down(pixels=100) - - for item in self.items_map.values(): - for i, zone in enumerate(item.zone_ids): - self.place_item(item.item_id, zone, None) - self.wait_until_html_in(item.feedback_positive[i], feedback_popup_content) - self.assert_popup_correct(popup) - self.assert_placed_item(item.item_id, item.zone_title[i]) - reset.click() - self.wait_until_disabled(reset) - - def _get_scenario_xml(self): - return self._get_custom_scenario_xml("data/test_multiple_options_data.json") - - -class PreventSpaceBarScrollTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest): - """" - Test that browser default page down action is prevented when pressing the space bar while - any zone is focused. - """ - def get_scroll(self): - return self.browser.execute_script('return $(window).scrollTop()') - - def hit_spacebar(self): - """ Send a spacebar event to the page/browser """ - try: - self._page.send_keys(Keys.SPACE) # Firefox (chrome doesn't allow sending keys to non-focusable elements) - except WebDriverException: - ActionChains(self.browser).send_keys(Keys.SPACE).perform() # Chrome (Firefox types this into the URL bar) - - def test_space_bar_scroll(self): - # Window should not be scrolled at first. - self.assertEqual(self.get_scroll(), 0) - # Pressing space bar while no zone is focused should scroll the window down (default browser action). - self.hit_spacebar() - # Window should be scrolled down a bit. - wait = WebDriverWait(self, 2) - # While the XHR is in progress, a spinner icon is shown inside the item. - # When the spinner disappears, we can assume that the XHR request has finished. - wait.until(lambda s: s.get_scroll() > 0) - # Scroll the window back. - self.scroll_down(pixels=0) - self.assertEqual(self.get_scroll(), 0) - # Now press Space while one of the zones is focused. - zone = self._get_zone_by_id(self.all_zones[0][0]) - zone.send_keys(Keys.SPACE) - # No scrolling should occur. - self.assertEqual(self.get_scroll(), 0) - - -class CustomDataInteractionTest(StandardInteractionTest): - items_map = { - 0: ItemDefinition(0, "Item 0", "", ['zone-1'], "Zone 1", "Yes 1", "No 1"), - 1: ItemDefinition(1, "Item 1", "", ['zone-2'], "Zone 2", "Yes 2", "No 2"), - 2: ItemDefinition(2, "Item 2", "", [], None, "", "No Zone for this") - } - - all_zones = [('zone-1', 'Zone 1'), ('zone-2', 'Zone 2')] - - feedback = { - "intro": "Some Intro Feed", - "final": "Some Final Feed" - } - - def _get_scenario_xml(self): - return self._get_custom_scenario_xml("data/test_data.json") - - -class CustomHtmlDataInteractionTest(StandardInteractionTest): - items_map = { - 0: ItemDefinition(0, "Item 0", "", ['zone-1'], 'Zone 1', "Yes 1", "No 1"), - 1: ItemDefinition(1, "Item 1", "", ['zone-2'], 'Zone 2', "Yes 2", "No 2"), - 2: ItemDefinition(2, "Item 2", "", [], None, "", "No Zone for X") - } - - all_zones = [('zone-1', 'Zone 1'), ('zone-2', 'Zone 2')] - - feedback = { - "intro": "Intro Feed", - "final": "Final Feed" - } - - def _get_scenario_xml(self): - return self._get_custom_scenario_xml("data/test_html_data.json") - - -class MultipleBlocksDataInteraction(ParameterizedTestsMixin, InteractionTestBase, BaseIntegrationTest): - PAGE_TITLE = 'Drag and Drop v2 Multiple Blocks' - PAGE_ID = 'drag_and_drop_v2_multi' - - BLOCK1_DATA_FILE = "data/test_data.json" - BLOCK2_DATA_FILE = "data/test_data_other.json" - - item_maps = { - 'block1': { - 0: ItemDefinition(0, "Item 0", "", ['zone-1'], 'Zone 1', "Yes 1", "No 1"), - 1: ItemDefinition(1, "Item 1", "", ['zone-2'], 'Zone 2', "Yes 2", "No 2"), - 2: ItemDefinition(2, "Item 2", "", [], None, "", "No Zone for this") - }, - 'block2': { - 10: ItemDefinition(10, "Item 10", "", ['zone-51'], 'Zone 51', "Correct 1", "Incorrect 1"), - 20: ItemDefinition(20, "Item 20", "", ['zone-52'], 'Zone 52', "Correct 2", "Incorrect 2"), - 30: ItemDefinition(30, "Item 30", "", [], None, "", "No Zone for this") - }, - } - - all_zones = { - 'block1': [('zone-1', 'Zone 1'), ('zone-2', 'Zone 2')], - 'block2': [('zone-51', 'Zone 51'), ('zone-52', 'Zone 52')] - } - - feedback = { - 'block1': {"intro": "Some Intro Feed", "final": "Some Final Feed"}, - 'block2': {"intro": "Other Intro Feed", "final": "Other Final Feed"}, - } - - def _get_scenario_xml(self): - blocks_xml = "\n".join([ - "".format(data=loader.load_unicode(filename)) - for filename in (self.BLOCK1_DATA_FILE, self.BLOCK2_DATA_FILE) - ]) - - return "{dnd_blocks}".format(dnd_blocks=blocks_xml) - - def test_item_positive_feedback_on_good_move(self): - self._switch_to_block(0) - self.parameterized_item_positive_feedback_on_good_move_standard( - self.item_maps['block1'], feedback=self.feedback['block1'] - ) - self._switch_to_block(1) - self.parameterized_item_positive_feedback_on_good_move_standard( - self.item_maps['block2'], feedback=self.feedback['block2'], scroll_down=1000 - ) - - def test_item_negative_feedback_on_bad_move(self): - self._switch_to_block(0) - self.parameterized_item_negative_feedback_on_bad_move_standard( - self.item_maps['block1'], self.all_zones['block1'], feedback=self.feedback['block1'] - ) - self._switch_to_block(1) - self.parameterized_item_negative_feedback_on_bad_move_standard( - self.item_maps['block2'], self.all_zones['block2'], feedback=self.feedback['block2'], scroll_down=1000 - ) - - def test_final_feedback_and_reset(self): - self._switch_to_block(0) - self.parameterized_final_feedback_and_reset(self.item_maps['block1'], self.feedback['block1']) - self._switch_to_block(1) - self.parameterized_final_feedback_and_reset(self.item_maps['block2'], self.feedback['block2'], scroll_down=1000) - - def test_keyboard_help(self): - self._switch_to_block(0) - # Test mouse and keyboard interaction - self.interact_with_keyboard_help() - self.interact_with_keyboard_help(use_keyboard=True) - - self._switch_to_block(1) - # Test mouse and keyboard interaction - self.interact_with_keyboard_help(scroll_down=1000) - self.interact_with_keyboard_help(scroll_down=0, use_keyboard=True) - - -@ddt -class ZoneAlignInteractionTest(InteractionTestBase, BaseIntegrationTest): - """ - Verifying Drag and Drop XBlock interactions using zone alignment. - """ - PAGE_TITLE = 'Drag and Drop v2' - PAGE_ID = 'drag_and_drop_v2' - ACTION_KEYS = ITEM_DRAG_KEYBOARD_KEYS - - def setUp(self): - super(ZoneAlignInteractionTest, self).setUp() - - def _get_scenario_xml(self): - return self._get_custom_scenario_xml("data/test_zone_align.json") - - def _assert_zone_align_item(self, item_id, zone_id, align, action_key=None): - """ - Test items placed in a zone with the given align setting. - Ensure that they are children of the zone. - """ - # parent container has the expected alignment - item_wrapper_selector = "div[data-uid='{zone_id}'] .item-wrapper".format(zone_id=zone_id) - self.assertEqual(self._get_style(item_wrapper_selector, 'textAlign'), align) - - # Items placed in zones with align setting are children of the zone - zone_item_selector = '{item_wrapper_selector} .option'.format(item_wrapper_selector=item_wrapper_selector) - prev_placed_items = self._page.find_elements_by_css_selector(zone_item_selector) - - self.place_item(item_id, zone_id, action_key) - placed_items = self._page.find_elements_by_css_selector(zone_item_selector) - self.assertEqual(len(placed_items), len(prev_placed_items) + 1) - - # Not children of the target - target_item = '.target > .option' - self.assertEqual(len(self._page.find_elements_by_css_selector(target_item)), 0) - - # Aligned items are relative positioned, with no transform or top/left - self.assertEqual(self._get_style(zone_item_selector, 'position'), 'relative') - self.assertEqual(self._get_style(zone_item_selector, 'transform'), 'none') - self.assertEqual(self._get_style(zone_item_selector, 'left'), '0px') - self.assertEqual(self._get_style(zone_item_selector, 'top'), '0px') - - self.assertEqual(self._get_style(zone_item_selector, 'display'), 'inline-block') - - @data( - ([0, 1, 2], "Zone No Align", "center"), - ([3, 4, 5], "Zone Invalid Align", "center"), - ([6, 7, 8], "Zone Left Align", "left"), - ([9, 10, 11], "Zone Right Align", "right"), - ([12, 13, 14], "Zone Center Align", "center"), - ) - @unpack - def test_zone_align(self, items, zone, alignment): - reset = self._get_reset_button() - for item in items: - for action_key in self.ACTION_KEYS: - self._assert_zone_align_item(item, zone, alignment, action_key) - # Reset exercise - self.scroll_down(pixels=200) - reset.click() - self.scroll_down(pixels=0) - self.wait_until_disabled(reset) - - -class TestMaxItemsPerZone(InteractionTestBase, BaseIntegrationTest): - """ - Tests for max items per dropzone feature - """ - PAGE_TITLE = 'Drag and Drop v2' - PAGE_ID = 'drag_and_drop_v2' - - assessment_mode = False - - def _get_scenario_xml(self): - scenario_data = loader.load_unicode("data/test_zone_align.json") - return self._make_scenario_xml(data=scenario_data, max_items_per_zone=2) - - def test_item_returned_to_bank(self): - """ - Tests that an item is returned to bank if max items per zone reached - """ - zone_id = "Zone No Align" - self.place_item(0, zone_id) - self.place_item(1, zone_id) - - # precondition check - max items placed into zone - self.assert_placed_item(0, zone_id, assessment_mode=self.assessment_mode) - self.assert_placed_item(1, zone_id, assessment_mode=self.assessment_mode) - - self.place_item(2, zone_id) - - self.assert_reverted_item(2) - feedback_popup = self._get_popup() - self.assertTrue(feedback_popup.is_displayed()) - - feedback_popup_content = self._get_popup_content() - self.assertIn( - "You cannot add any more items to this zone.", - feedback_popup_content.get_attribute('innerHTML') - ) - - def test_item_returned_to_bank_after_refresh(self): - """ - Tests that an item returned to the bank stays there after page refresh - """ - zone_id = "Zone Left Align" - self.place_item(6, zone_id) - self.place_item(7, zone_id) - - # precondition check - max items placed into zone - self.assert_placed_item(6, zone_id, assessment_mode=self.assessment_mode) - self.assert_placed_item(7, zone_id, assessment_mode=self.assessment_mode) - - self.place_item(8, zone_id) - - self.assert_reverted_item(8) - - self._page = self.go_to_page(self.PAGE_TITLE) # refresh the page - - self.assert_placed_item(6, zone_id, assessment_mode=self.assessment_mode) - self.assert_placed_item(7, zone_id, assessment_mode=self.assessment_mode) - self.assert_reverted_item(8) - - -class DragScrollingTest(InteractionTestBase, BaseIntegrationTest): - """Tests that drop targets are scrolled into view while dragging.""" - - PAGE_TITLE = 'Drag and Drop v2' - PAGE_ID = 'drag_and_drop_v2' - - def setUp(self): - super(DragScrollingTest, self).setUp() - self.browser.set_window_size(320, 480) - wait = WebDriverWait(self.browser, 2) - wait.until(lambda browser: browser.get_window_size()["width"] == 320) - - def _get_scenario_xml(self): - return self._get_custom_scenario_xml("data/test_html_data.json") - - def test_scrolling_during_placement(self): - item1_id = 0 - zone1_id = "zone-1" - - zone2_id = "zone-2" - - zone1 = self._get_zone_by_id(zone1_id) - zone2 = self._get_zone_by_id(zone2_id) - - # zone2 is at 0, 0 in the target container, so initially - # visible, even with the page header - self.assertTrue(self.is_element_in_viewport(zone2)) - - # zone 1 is at 100, 200 in its container, so with the page - # header, it's initially below the viewport - self.assertFalse(self.is_element_in_viewport(zone1)) - - # when placing the item in zone1, zone1 will scroll into view - self.place_item(item1_id, zone1_id) - self.assertTrue(self.is_element_in_viewport(zone1)) - - # and now zone2 is out of view - self.assertFalse(self.is_element_in_viewport(zone2)) diff --git a/tests/integration/test_interaction_assessment.py b/tests/integration/test_interaction_assessment.py deleted file mode 100644 index cbbd987df..000000000 --- a/tests/integration/test_interaction_assessment.py +++ /dev/null @@ -1,517 +0,0 @@ -# -*- coding: utf-8 -*- - -# Imports ########################################################### - -from __future__ import absolute_import -from copy import deepcopy -import json - -import re -import time -from xml.sax.saxutils import escape - -import ddt -from mock import Mock, patch -from selenium.common.exceptions import NoSuchElementException -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support.ui import WebDriverWait -from xblockutils.resources import ResourceLoader - -from drag_and_drop_v2.default_data import (BOTTOM_ZONE_ID, DEFAULT_DATA, FINISH_FEEDBACK, - MIDDLE_ZONE_ID, START_FEEDBACK, - TOP_ZONE_ID, TOP_ZONE_TITLE) -from drag_and_drop_v2.utils import Constants, FeedbackMessages, SHOWANSWER - -from .test_base import BaseIntegrationTest -from .test_interaction import (ITEM_DRAG_KEYBOARD_KEYS, DefaultDataTestMixin, - InteractionTestBase, ParameterizedTestsMixin, - TestMaxItemsPerZone) - -# Globals ########################################################### - -loader = ResourceLoader(__name__) - - -# Classes ########################################################### - -class DefaultAssessmentDataTestMixin(DefaultDataTestMixin): - """ - Provides a test scenario with default options in assessment mode. - """ - MAX_ATTEMPTS = 5 - SHOW_ANSWER_STATUS = SHOWANSWER.FINISHED - - def _get_scenario_xml(self): # pylint: disable=no-self-use - return """ - - - - """.format(mode=Constants.ASSESSMENT_MODE, - max_attempts=self.MAX_ATTEMPTS, - show_answer_status=self.SHOW_ANSWER_STATUS) - - -class AssessmentTestMixin(object): - """ - Provides helper methods for assessment tests - """ - @staticmethod - def _wait_until_enabled(element): - wait = WebDriverWait(element, 2) - wait.until(lambda e: e.is_displayed() and e.get_attribute('disabled') is None) - - def click_submit(self): - submit_button = self._get_submit_button() - - self._wait_until_enabled(submit_button) - - submit_button.click() - self.wait_for_ajax() - - def click_show_answer(self): - show_answer_button = self._get_show_answer_button() - - self._wait_until_enabled(show_answer_button) - - show_answer_button.click() - self.wait_for_ajax() - - -# pylint: disable=bad-continuation -@ddt.ddt -class AssessmentInteractionTest( - DefaultAssessmentDataTestMixin, AssessmentTestMixin, ParameterizedTestsMixin, - InteractionTestBase, BaseIntegrationTest -): - """ - Testing interactions with Drag and Drop XBlock against default data in assessment mode. - All interactions are tested using mouse (action_key=None) and four different keyboard action keys. - If default data changes this will break. - """ - @ddt.data(*ITEM_DRAG_KEYBOARD_KEYS) - def test_item_no_feedback_on_good_move(self, action_key): - self.parameterized_item_positive_feedback_on_good_move_assessment(self.items_map, action_key=action_key) - - @ddt.data(*ITEM_DRAG_KEYBOARD_KEYS) - def test_item_no_feedback_on_bad_move(self, action_key): - self.parameterized_item_negative_feedback_on_bad_move_assessment( - self.items_map, self.all_zones, action_key=action_key - ) - - @ddt.data(*ITEM_DRAG_KEYBOARD_KEYS) - def test_move_items_between_zones(self, action_key): - self.parameterized_move_items_between_zones( - self.items_map, self.all_zones, action_key=action_key - ) - - @ddt.data(*ITEM_DRAG_KEYBOARD_KEYS) - def test_final_feedback_and_reset(self, action_key): - self.parameterized_final_feedback_and_reset( - self.items_map, self.feedback, action_key=action_key, assessment_mode=True - ) - - @ddt.data(False, True) - def test_keyboard_help(self, use_keyboard): - self.interact_with_keyboard_help(use_keyboard=use_keyboard) - - def test_submit_button_shown(self): - first_item_definition = list(self._get_items_with_zone(self.items_map).values())[0] - - submit_button = self._get_submit_button() - self.assertTrue(submit_button.is_displayed()) - self.assertEqual(submit_button.get_attribute('disabled'), 'true') # no items are placed - - attempts_info = self._get_attempts_info() - expected_text = "You have used {num} of {max} attempts.".format(num=0, max=self.MAX_ATTEMPTS) - self.assertEqual(attempts_info.text, expected_text) - self.assertEqual(attempts_info.is_displayed(), self.MAX_ATTEMPTS > 0) - - self.place_item(first_item_definition.item_id, first_item_definition.zone_ids[0], None) - - self.assertEqual(submit_button.get_attribute('disabled'), None) - - def test_misplaced_items_returned_to_bank(self): - """ - Test items placed to incorrect zones are returned to item bank after submitting solution - """ - correct_items = {0: TOP_ZONE_ID} - misplaced_items = {1: BOTTOM_ZONE_ID, 2: MIDDLE_ZONE_ID} - - for item_id, zone_id in correct_items.items(): - self.place_item(item_id, zone_id) - - for item_id, zone_id in misplaced_items.items(): - self.place_item(item_id, zone_id) - - self.click_submit() - for item_id in correct_items: - self.assert_placed_item(item_id, TOP_ZONE_TITLE, assessment_mode=True) - - for item_id in misplaced_items: - self.assert_reverted_item(item_id) - - def test_misplaced_items_not_returned_to_bank_on_final_attempt(self): - """ - Test items placed on incorrect zones are not returned to item bank - after submitting solution on the final attempt, and remain placed after - subsequently refreshing the page. - """ - self.place_item(0, TOP_ZONE_ID, action_key=Keys.RETURN) - - # Reach final attempt - for _ in range(self.MAX_ATTEMPTS-1): - self.click_submit() - - # Place incorrect item on final attempt - self.place_item(1, TOP_ZONE_ID, action_key=Keys.RETURN) - self.click_submit() - - # Incorrect item remains placed - def _assert_placed(item_id, zone_title): - item = self._get_placed_item_by_value(item_id) - item_description = item.find_element_by_css_selector('.sr.description') - self.assertEqual(item_description.text, 'Placed in: {}'.format(zone_title)) - - _assert_placed(1, TOP_ZONE_TITLE) - - # Refresh the page - self._page = self.go_to_page(self.PAGE_TITLE) - - # Incorrect item remains placed after refresh - _assert_placed(1, TOP_ZONE_TITLE) - - def test_max_attempts_reached_submit_and_reset_disabled(self): - """ - Test "Submit" and "Reset" buttons are disabled when no more attempts remaining - """ - self.place_item(0, TOP_ZONE_ID) - - submit_button, reset_button = self._get_submit_button(), self._get_reset_button() - - attempts_info = self._get_attempts_info() - - for index in range(self.MAX_ATTEMPTS): - expected_text = "You have used {num} of {max} attempts.".format(num=index, max=self.MAX_ATTEMPTS) - self.assertEqual(attempts_info.text, expected_text) # precondition check - self.assertEqual(submit_button.get_attribute('disabled'), None) - self.assertEqual(reset_button.get_attribute('disabled'), None) - self.click_submit() - - self.assertEqual(submit_button.get_attribute('disabled'), 'true') - self.assertEqual(reset_button.get_attribute('disabled'), 'true') - - def _assert_show_answer_item_placement(self): - zones = dict(self.all_zones) - for item in self._get_items_with_zone(self.items_map).values(): - zone_titles = [zones[zone_id] for zone_id in item.zone_ids] - # When showing answers, correct items are placed as if assessment_mode=False - self.assert_placed_item(item.item_id, zone_titles, assessment_mode=False) - - for item_definition in self._get_items_without_zone(self.items_map).values(): - self.assertNotDraggable(item_definition.item_id) - item = self._get_item_by_value(item_definition.item_id) - self.assertEqual(item.get_attribute('aria-grabbed'), 'false') - self.assertEqual(item.get_attribute('class'), 'option fade') - - item_content = item.find_element_by_css_selector('.item-content') - self.assertEqual(item_content.get_attribute('aria-describedby'), None) - - try: - item.find_element_by_css_selector('.sr.description') - self.fail("Description element should not be present") - except NoSuchElementException: - pass - - def test_show_answer(self): - """ - Test "Show Answer" button is shown in assessment mode, enabled when no - more attempts remaining, is disabled and displays correct answers when - clicked. - """ - with self.assertRaises(NoSuchElementException): - self._get_show_answer_button() - - self.place_item(0, TOP_ZONE_ID, Keys.RETURN) - for _ in range(self.MAX_ATTEMPTS-1): - with self.assertRaises(NoSuchElementException): - self._get_show_answer_button() - self.click_submit() - - # Place an incorrect item on the final attempt. - self.place_item(1, TOP_ZONE_ID, Keys.RETURN) - self.click_submit() - - # A feedback popup should open upon final submission. - popup = self._get_popup() - show_answer_button = self._get_show_answer_button() - - self.assertTrue(popup.is_displayed()) - self.assertIsNone(show_answer_button.get_attribute('disabled')) - self.click_show_answer() - - # The popup should be closed upon clicking Show Answer. - self.assertFalse(popup.is_displayed()) - - self.assertEqual(show_answer_button.get_attribute('disabled'), 'true') - self._assert_show_answer_item_placement() - - @ddt.data(MIDDLE_ZONE_ID, TOP_ZONE_ID) - def test_show_answer_user_selected_zone(self, dropped_zone_id): - """ - Test item stays in plane when showing answer if placed in correct zone. - """ - zones = dict(self.all_zones) - - # Place an item with multiple correct zones - self.place_item(3, dropped_zone_id, Keys.RETURN) - - # Submit maximum number of times - for _ in range(self.MAX_ATTEMPTS): - self.click_submit() - - show_answer_button = self._get_show_answer_button() - self.assertTrue(show_answer_button.is_displayed()) - self.assertIsNone(show_answer_button.get_attribute('disabled')) - self.click_show_answer() - - self.assert_placed_item(3, [zones[dropped_zone_id]], assessment_mode=False) - - def test_do_attempt_feedback_is_updated(self): - """ - Test updating overall feedback after submitting solution in assessment mode - """ - def check_feedback(overall_feedback_lines, per_item_feedback_lines=None): - # Check that the feedback is correctly displayed in the overall feedback area. - expected_overall_feedback = "\n".join(["FEEDBACK"] + overall_feedback_lines) - self.assertEqual(self._get_feedback().text, expected_overall_feedback) - - # Check that the SR.readText function was passed correct feedback messages. - sr_feedback_lines = overall_feedback_lines - if per_item_feedback_lines: - sr_feedback_lines += ["Some of your answers were not correct.", "Hints:"] - sr_feedback_lines += per_item_feedback_lines - self.assert_reader_feedback_messages(sr_feedback_lines) - - # used keyboard mode to avoid bug/feature with selenium "selecting" everything instead of dragging an element - self.place_item(0, TOP_ZONE_ID, Keys.RETURN) - - self.click_submit() - - # There are five items total (4 items with zones and one decoy item). - # We place the first item into correct zone and left the decoy item in the bank, - # which means the current grade is 2/5. - expected_grade = 2.0 / 5.0 - - feedback_lines = [ - FeedbackMessages.correctly_placed(1), - FeedbackMessages.not_placed(3), - FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=expected_grade), - START_FEEDBACK, - ] - check_feedback(feedback_lines) - - # Place the item into incorrect zone. The score does not change. - self.place_item(1, BOTTOM_ZONE_ID, Keys.RETURN) - self.click_submit() - - feedback_lines = [ - FeedbackMessages.correctly_placed(1), - FeedbackMessages.misplaced_returned(1), - FeedbackMessages.not_placed(2), - FeedbackMessages.GRADE_FEEDBACK_TPL.format(score=expected_grade), - START_FEEDBACK, - ] - check_feedback(feedback_lines, ["No, this item does not belong here. Try again."]) - - # reach final attempt - for _ in range(self.MAX_ATTEMPTS-3): - self.click_submit() - - self.place_item(1, MIDDLE_ZONE_ID, Keys.RETURN) - self.place_item(2, BOTTOM_ZONE_ID, Keys.RETURN) - self.place_item(3, TOP_ZONE_ID, Keys.RETURN) - - self.click_submit() - - # All items are correctly placed, so we get the full score (1.0). - expected_grade = 1.0 - - feedback_lines = [ - FeedbackMessages.correctly_placed(4), - FeedbackMessages.FINAL_ATTEMPT_TPL.format(score=expected_grade), - FINISH_FEEDBACK, - ] - check_feedback(feedback_lines) - - def test_per_item_feedback_multiple_misplaced(self): - self.place_item(0, MIDDLE_ZONE_ID, Keys.RETURN) - self.place_item(1, BOTTOM_ZONE_ID, Keys.RETURN) - self.place_item(2, TOP_ZONE_ID, Keys.RETURN) - - self.click_submit() - - placed_item_definitions = [self.items_map[item_id] for item_id in (1, 2, 3)] - - expected_message_elements = [ - "
  • {msg}
  • ".format(msg=definition.feedback_negative) - for definition in placed_item_definitions - ] - - for message_element in expected_message_elements: - self.assertIn(message_element, self._get_popup_content().get_attribute('innerHTML')) - - def test_submit_disabled_during_drop_item(self): - def delayed_drop_item(item_attempt, suffix=''): # pylint: disable=unused-argument - # some delay to allow selenium check submit button disabled status while "drop_item" - # XHR is still executing - time.sleep(0.1) - return {} - - self.place_item(0, TOP_ZONE_ID) - self.assert_placed_item(0, TOP_ZONE_TITLE, assessment_mode=True) - - submit_button = self._get_submit_button() - self.assert_button_enabled(submit_button) # precondition check - with patch('drag_and_drop_v2.DragAndDropBlock._drop_item_assessment', Mock(side_effect=delayed_drop_item)): - item_id = 1 - self.place_item(item_id, MIDDLE_ZONE_ID, wait=False) - # do not wait for XHR to complete - self.assert_button_enabled(submit_button, enabled=False) - self.wait_until_ondrop_xhr_finished(self._get_placed_item_by_value(item_id)) - - self.assert_button_enabled(submit_button, enabled=True) - - def test_grade_display(self): - progress = self._page.find_element_by_css_selector('.problem-progress') - self.assertEqual(progress.text, '1 point possible (ungraded)') - - items_with_zones = list(self._get_items_with_zone(self.items_map).values()) - items_without_zones = list(self._get_items_without_zone(self.items_map).values()) - total_items = len(items_with_zones) + len(items_without_zones) - - # Place items into correct zones one by one: - for idx, item in enumerate(items_with_zones): - self.place_item(item.item_id, item.zone_ids[0]) - # The number of items in correct positions currently equals: - # the number of items already placed + any decoy items which should stay in the bank. - grade = (idx + 1 + len(items_without_zones)) / float(total_items) - formatted_grade = '{:.04f}'.format(grade) # display 4 decimal places - formatted_grade = re.sub(r'\.?0+$', '', formatted_grade) # remove trailing zeros - expected_progress = '{}/1 point (ungraded)'.format(formatted_grade) - # Selenium does not see the refreshed text unless the text is in view (wtf??), so scroll back up. - self.scroll_down(pixels=0) - # Grade does NOT change until we submit. - self.assertNotEqual(progress.text, expected_progress) - self.click_submit() - self.scroll_down(pixels=0) - self.assertEqual(progress.text, expected_progress) - - # After placing all items, we get the full score. - self.assertEqual(progress.text, '1/1 point (ungraded)') - - -class TestMaxItemsPerZoneAssessment(TestMaxItemsPerZone): - assessment_mode = True - - def _get_scenario_xml(self): - scenario_data = loader.load_unicode("data/test_zone_align.json") - return self._make_scenario_xml(data=scenario_data, max_items_per_zone=2, mode=Constants.ASSESSMENT_MODE) - - def test_drop_item_to_same_zone_does_not_show_popup(self): - """ - Tests that picking item from saturated zone and dropping it back again does not trigger error popup - """ - zone_id = "Zone Left Align" - self.place_item(6, zone_id) - self.place_item(7, zone_id) - - popup = self._get_popup() - - # precondition check - max items placed into zone - self.assert_placed_item(6, zone_id, assessment_mode=self.assessment_mode) - self.assert_placed_item(7, zone_id, assessment_mode=self.assessment_mode) - - self.place_item(6, zone_id, Keys.RETURN) - self.assertFalse(popup.is_displayed()) - - self.place_item(7, zone_id, Keys.RETURN) - self.assertFalse(popup.is_displayed()) - - -@ddt.ddt -class ExplanationTest( - DefaultDataTestMixin, AssessmentTestMixin, - BaseIntegrationTest, InteractionTestBase -): - """ - Test the rendering of the "Explanation" block when "Show Answer" button is clicked. - Test "Explanation" block is not displayed if no explanation string is configured or - if the configured string is only white spaces. - """ - - CURRENT_EXPLANATION_TEXT = "" - - def _get_scenario_xml(self): - problem_data = deepcopy(DEFAULT_DATA) - problem_data['explanation'] = self.CURRENT_EXPLANATION_TEXT - scenario_xml = """ - - """.format( - mode=Constants.ASSESSMENT_MODE, - max_attempts=1, - data=escape(json.dumps(problem_data), self._additional_escapes) - ) - return scenario_xml - - def load_scenario(self, explanation_text: str, scenario_id: int): - self.CURRENT_EXPLANATION_TEXT = explanation_text - scenario_xml = self._get_scenario_xml() - - scenario_page_id = "{base_page_id}_explanation_{page_id}".format( - base_page_id=self.PAGE_ID, - page_id=scenario_id - ) - - scenario_page_title = "{base_page_title}_explanation_{page_id}".format( - base_page_title=self.PAGE_TITLE, - page_id=scenario_id - ) - - self._add_scenario(scenario_page_id, scenario_page_title, scenario_xml) - self._page = self.go_to_page(scenario_page_title) - - @staticmethod - def _get_explanation_html(explanation: str) -> str: - return f'

    Explanation

    {explanation}

    ' - - @ddt.data( - (1, "This is an explanation.", True, "Explanation\nThis is an explanation."), - (2, " ", False, None), - (3, None, False, None), - (4, "12m2", True, "Explanation\n12m2"), - ) - @ddt.unpack - def test_explanation(self, scenario_id: int, explanation: str, should_display: bool, rendered_explanation: str): - """ - Test that the "Explanation" is displayed after the "Show Answer" button is clicked. - The docstring of the class explains when the explanation should be visible. - """ - self.load_scenario(explanation, scenario_id) - - self.place_item(3, MIDDLE_ZONE_ID, Keys.RETURN) - - self.click_submit() - - show_answer_button = self._get_show_answer_button() - self.assertTrue(show_answer_button.is_displayed()) - self.assertIsNone(show_answer_button.get_attribute('disabled')) - self.click_show_answer() - - explanation_block = self._get_explanation() - self.assertEqual(explanation_block.is_displayed(), should_display) - if should_display: - self.assertEqual(rendered_explanation, explanation_block.text) - self.assertEqual(self._get_explanation_html(explanation), explanation_block.get_attribute('innerHTML')) diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py deleted file mode 100644 index 0dfb242de..000000000 --- a/tests/integration/test_render.py +++ /dev/null @@ -1,346 +0,0 @@ -# Imports ########################################################### - -from __future__ import absolute_import - -from ddt import data, ddt, unpack -from selenium.common.exceptions import NoSuchElementException -from selenium.webdriver.common.keys import Keys -from xblockutils.resources import ResourceLoader - -from drag_and_drop_v2.default_data import START_FEEDBACK - -from .test_base import BaseIntegrationTest - -# Globals ########################################################### - -loader = ResourceLoader(__name__) - - -# Classes ########################################################### - -class Colors(object): - WHITE = 'rgb(255, 255, 255)' - BLUE = 'rgb(29, 82, 128)' - GREY = 'rgb(237, 237, 237)' - CORAL = '#ff7f50' - DARK_GREY = 'rgb(86, 86, 86)' # == #565656 in CSS-land - CORNFLOWERBLUE = 'cornflowerblue' - - @classmethod - def rgb(cls, color): - if color in (cls.WHITE, cls.BLUE, cls.GREY): - return color - elif color == cls.CORAL: - return 'rgb(255, 127, 80)' - elif color == cls.CORNFLOWERBLUE: - return 'rgb(100, 149, 237)' - return None - - -@ddt -class TestDragAndDropRender(BaseIntegrationTest): - """ - Verifying Drag and Drop XBlock rendering against default data - if default data changes this - will probably break. - """ - PAGE_TITLE = 'Drag and Drop v2' - PAGE_ID = 'drag_and_drop_v2' - ITEM_PROPERTIES = [{'text': '1'}, {'text': '2'}, {'text': 'X'}, ] - SIDES = ['Top', 'Bottom', 'Left', 'Right'] - - def load_scenario(self, item_background_color="", item_text_color="", zone_labels=False, zone_borders=False): - problem_data = loader.load_unicode("data/test_data_a11y.json") - problem_data = problem_data.replace('{display_labels_value}', 'true' if zone_labels else 'false') - problem_data = problem_data.replace('{display_borders_value}', 'true' if zone_borders else 'false') - scenario_xml = """ - - - - """.format( - item_background_color=item_background_color, - item_text_color=item_text_color, - problem_data=problem_data - ) - self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml) - - self.browser.get(self.live_server_url) - self._page = self.go_to_page(self.PAGE_TITLE) - - def _assert_box_percentages(self, selector, left, top, width, height): - """ Assert that the element 'selector' has the specified position/size percentages """ - values = {key: self._get_style(selector, key, False) for key in ['left', 'top', 'width', 'height']} - for key in values: - self.assertTrue(values[key].endswith('%')) - values[key] = float(values[key][:-1]) - self.assertAlmostEqual(values['left'], left, places=2) - self.assertAlmostEqual(values['top'], top, places=2) - self.assertAlmostEqual(values['width'], width, places=2) - self.assertAlmostEqual(values['height'], height, places=2) - - def _test_item_style(self, item_element, style_settings): - item_val = item_element.get_attribute('data-value') - item_selector = '.item-bank .option[data-value=' + item_val + ']' - style = item_element.get_attribute('style') - # Check background color - background_color_property = 'background-color' - if background_color_property not in style_settings: - self.assertNotIn(background_color_property, style) - expected_background_color = Colors.BLUE - else: - expected_background_color = Colors.rgb(style_settings['background-color']) - background_color = self._get_style(item_selector, 'backgroundColor') - self.assertEqual(background_color, expected_background_color) - - # Check text color - color_property = 'color' - if color_property not in style_settings: - # Leading space below ensures that test does not find "color" in "background-color" - self.assertNotIn(' ' + color_property, style) - expected_color = Colors.WHITE - else: - expected_color = Colors.rgb(style_settings['color']) - color = self._get_style(item_selector, 'color') - self.assertEqual(color, expected_color) - - # Check outline color - outline_color_property = 'outline-color' - if outline_color_property not in style_settings: - self.assertNotIn(outline_color_property, style) - # Outline color should match text color to ensure it does not meld into background color: - expected_outline_color = expected_color - outline_color = self._get_style(item_selector, 'outlineColor') - self.assertEqual(outline_color, expected_outline_color) - - def test_items_default_colors(self): - self.load_scenario() - self._test_items() - - @unpack - @data( - (Colors.CORNFLOWERBLUE, Colors.GREY), - (Colors.CORAL, ''), - ('', Colors.GREY), - ) - def test_items_custom_colors(self, item_background_color, item_text_color): - self.load_scenario(item_background_color, item_text_color) - - color_settings = {} - if item_background_color: - color_settings['background-color'] = item_background_color - if item_text_color: - color_settings['color'] = item_text_color - color_settings['outline-color'] = item_text_color - - self._test_items(color_settings=color_settings) - - def _test_items(self, color_settings=None): - color_settings = color_settings or {} - - items = self._get_items() - - self.assertEqual(len(items), 3) - - for index, item in enumerate(items): - item_number = index + 1 - self.assertEqual(item.get_attribute('role'), 'button') - self.assertEqual(item.get_attribute('tabindex'), '0') - self.assertEqual(item.get_attribute('draggable'), 'true') - self.assertEqual(item.get_attribute('aria-grabbed'), 'false') - self.assertEqual(item.get_attribute('data-value'), str(index)) - self._test_item_style(item, color_settings) - try: - background_image = item.find_element_by_css_selector('img') - except NoSuchElementException: - item_content = item.find_element_by_css_selector('.item-content') - self.assertEqual(item_content.text, self.ITEM_PROPERTIES[index]['text']) - else: - self.assertEqual( - background_image.get_attribute('alt'), - 'This describes the background image of item {}'.format(item_number) - ) - - def test_drag_container(self): - self.load_scenario() - drag_container = self._page.find_element_by_css_selector('.drag-container') - self.assertIsNone(drag_container.get_attribute('role')) - - def test_item_bank(self): - self.load_scenario() - item_bank = self._page.find_element_by_css_selector('.item-bank') - self.assertEqual(item_bank.get_attribute("aria-label"), 'Item Bank') - - def test_zones(self): - self.load_scenario() - - zones = self._get_zones() - - self.assertEqual(len(zones), 2) - - box_percentages = [ - {"left": 31.1284, "top": 6.17284, "width": 38.1323, "height": 36.6255}, - {"left": 16.7315, "top": 43.2099, "width": 66.1479, "height": 28.8066} - ] - - for index, zone in enumerate(zones): - zone_number = index + 1 - self.assertEqual(zone.get_attribute('tabindex'), '0') - self.assertEqual(zone.get_attribute('dropzone'), 'move') - self.assertEqual(zone.get_attribute('aria-dropeffect'), 'move') - self.assertEqual(zone.get_attribute('data-uid'), 'Zone {}'.format(zone_number)) - self.assertEqual(zone.get_attribute('data-zone_align'), 'center') - zone_box_percentages = box_percentages[index] - self._assert_box_percentages( # pylint: disable=star-args - '#-Zone_{}'.format(zone_number), **zone_box_percentages - ) - zone_name = zone.find_element_by_css_selector('p.zone-name') - self.assertEqual(zone_name.text, 'Zone {}\n, dropzone'.format(zone_number)) - zone_description = zone.find_element_by_css_selector('p.zone-description') - self.assertEqual(zone_description.text, 'This describes zone {}'.format(zone_number)) - # Zone description should only be visible to screen readers: - self.assertEqual(zone_description.get_attribute('class'), 'zone-description sr') - - def test_popup(self): - self.load_scenario() - - popup = self._get_popup() - popup_content = self._get_popup_content() - self.assertFalse(popup.is_displayed()) - self.assertIn('popup', popup.get_attribute('class')) - self.assertEqual(popup_content.text, "") - - @data(None, Keys.RETURN) - def test_go_to_beginning_button(self, action_key): - self.load_scenario() - self.scroll_down(250) - - button = self._get_go_to_beginning_button() - # Button is only visible to screen reader users by default. - self.assertIn('sr', button.get_attribute('class').split()) - # Set focus to the element. We have to use execute_script here because while TAB-ing - # to the button to make it the active element works in selenium, the focus event is not - # emitted unless the Firefox window controlled by selenium is the focused window, which - # usually is not the case when running integration tests. - # See: https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/7346 - self.browser.execute_script('$("button.go-to-beginning-button").focus()') - - # For unknown reasons the element only becomes visible when focus() is called twice. - # See: https://openedx.atlassian.net/browse/TNL-6736 - self.browser.execute_script('$("button.go-to-beginning-button").focus()') - self.assertFocused(button) - # Button should be visible when focused. - self.assertNotIn('sr', button.get_attribute('class').split()) - # Click/activate the button to move focus to the top. - if action_key: - button.send_keys(action_key) - else: - button.click() - first_focusable_item = self._get_items()[0] - self.assertFocused(first_focusable_item) - # Button should only be visible to screen readers again. - self.assertIn('sr', button.get_attribute('class').split()) - - def test_keyboard_help(self): - self.load_scenario() - - keyboard_help_dialog = self._get_keyboard_help_dialog() - dialog_modal_overlay = keyboard_help_dialog.find_element_by_css_selector('.modal-window-overlay') - dialog_modal = keyboard_help_dialog.find_element_by_css_selector('.modal-window') - - self.assertFalse(dialog_modal_overlay.is_displayed()) - self.assertFalse(dialog_modal.is_displayed()) - self.assertEqual(dialog_modal.get_attribute('role'), 'dialog') - self.assertEqual(dialog_modal.get_attribute('aria-labelledby'), 'modal-window-title-') - - def test_feedback(self): - self.load_scenario() - - feedback_message = self._get_feedback_message() - self.assertEqual(feedback_message.text, START_FEEDBACK) - - def test_background_image(self): - self.load_scenario() - - bg_image = self.browser.find_element_by_css_selector(".xblock--drag-and-drop .target-img") - image_path = '/resource/drag-and-drop-v2/public/img/triangle.png' - self.assertTrue(bg_image.get_attribute("src").endswith(image_path)) - self.assertEqual(bg_image.get_attribute("alt"), 'This describes the target image') - - def test_zone_borders_hidden(self): - self.load_scenario() - zones = self._get_zones() - for index, dummy in enumerate(zones, start=1): - zone = '#-Zone_{}'.format(index) - for side in self.SIDES: - self.assertEqual(self._get_style(zone, 'border{}Width'.format(side), True), '0px') - self.assertEqual(self._get_style(zone, 'border{}Style'.format(side), True), 'none') - - def test_zone_borders_shown(self): - self.load_scenario(zone_borders=True) - zones = self._get_zones() - for index, dummy in enumerate(zones, start=1): - zone = '#-Zone_{}'.format(index) - for side in self.SIDES: - self.assertEqual(self._get_style(zone, 'border{}Width'.format(side), True), '1px') - self.assertEqual(self._get_style(zone, 'border{}Style'.format(side), True), 'dotted') - self.assertEqual(self._get_style(zone, 'border{}Color'.format(side), True), Colors.DARK_GREY) - - def test_zone_labels_hidden(self): - self.load_scenario() - zones = self._get_zones() - for zone in zones: - zone_name = zone.find_element_by_css_selector('p.zone-name') - self.assertIn('sr', zone_name.get_attribute('class')) - - def test_zone_labels_shown(self): - self.load_scenario(zone_labels=True) - zones = self._get_zones() - for zone in zones: - zone_name = zone.find_element_by_css_selector('p.zone-name') - self.assertNotIn('sr', zone_name.get_attribute('class')) - - def test_element_as_jquery_does_not_break_load_event_listeners(self): - """ - DragAndDropBlock.init should accept a jQuery object or a DOM element. - - Some XBlock initialization routes supply the element argument as a plain DOM element, others pass in a jQuery - object. DragAndDropBlock.init should cope with either, instead of throwing a TypeError when trying to call - addEventListener on jQuery objects, which would cause an infinite "loading" message when adding the block to a - unit in Studio. - """ - self.load_scenario() - - for entry in self.browser.get_log('browser'): - self.assertNotEqual( - 'TypeError: element.addEventListener is not a function', - entry['message'] - ) - - -@ddt -class TestDragAndDropRenderZoneAlign(BaseIntegrationTest): - """ - Verifying Drag and Drop XBlock rendering using zone alignment. - """ - PAGE_TITLE = 'Drag and Drop v2' - PAGE_ID = 'drag_and_drop_v2' - - def setUp(self): - super(TestDragAndDropRenderZoneAlign, self).setUp() - scenario_xml = self._get_custom_scenario_xml("data/test_zone_align.json") - self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml) - self._page = self.go_to_page(self.PAGE_TITLE) - - def test_zone_align(self): - expected_alignments = { - "#-Zone_No_Align": "center", - "#-Zone_Invalid_Align": "center", - "#-Zone_Left_Align": "left", - "#-Zone_Right_Align": "right", - "#-Zone_Center_Align": "center" - } - for zone_id, expected_alignment in expected_alignments.items(): - selector = "{zone_id} .item-wrapper".format(zone_id=zone_id) - self.assertEqual(self._get_style(selector, "textAlign"), expected_alignment) - self.assertEqual(self._get_style(selector, "textAlign", computed=True), expected_alignment) diff --git a/tests/integration/test_sizing.py b/tests/integration/test_sizing.py deleted file mode 100644 index 1ce155bd4..000000000 --- a/tests/integration/test_sizing.py +++ /dev/null @@ -1,365 +0,0 @@ -from __future__ import absolute_import, division - -import base64 -import os.path -from collections import namedtuple - -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support.ui import WebDriverWait -from xblockutils.resources import ResourceLoader - -from tests.integration.test_base import InteractionTestBase - -from .test_base import BaseIntegrationTest - -loader = ResourceLoader(__name__) - - -def _svg_to_data_uri(path): - """ Convert an SVG image (by path) to a data URI """ - data_path = os.path.dirname(__file__) + "/data/" - with open(data_path + path, "rb") as svg_fh: - encoded = base64.b64encode(svg_fh.read()) - return "data:image/svg+xml;base64,{}".format(encoded.decode('utf-8')) - - -Expectation = namedtuple('Expectation', [ - 'item_id', - 'zone_id', - 'width_percent_bank', # we expect this item to have this width relative to its container (item bank) - 'width_percent_image', # we expect this item to have this width relative to its container (image target) - 'width_percent', # we expect this item to have this width relative to its container (item bank or image target) - 'fixed_width_percent', # we expect this item to have this width (always relative to the target image) - 'img_pixel_size_exact', # we expect the image inside the draggable to have the exact size [w, h] in pixels -]) -Expectation.__new__.__defaults__ = (None,) * len(Expectation._fields) # pylint: disable=protected-access -ZONE_33 = "Zone 1/3" # Title of top zone in each image used in these tests (33% width) -ZONE_50 = "Zone 50%" -ZONE_75 = "Zone 75%" - -# iPhone 6 viewport size is 375x627; this is the closest Chrome can get. -MOBILE_WINDOW_WIDTH = 400 -MOBILE_WINDOW_HEIGHT = 627 - -# Maximum widths (as % of the parent container) for items with automatic sizing -AUTO_MAX_WIDTH_DESKTOP = 30 -AUTO_MAX_WIDTH_MOBILE_ITEM_BANK = 80 -AUTO_MAX_WIDTH_MOBILE_TARGET_IMG = 30 - - -class SizingTests(InteractionTestBase, BaseIntegrationTest): - """ - Tests that cover features like draggable blocks with automatic sizes vs. specified sizes, - different background image ratios, and responsive behavior. - - Tip: To see how these tests work, throw in an 'import time; time.sleep(200)' at the start of - one of the tests, so you can check it out in the selenium browser window that opens. - - These tests intentionally do not use ddt in order to run faster. Instead, each test iterates - through data and uses verbose assertion messages to clearly indicate where failures occur. - """ - PAGE_TITLE = 'Drag and Drop v2 Sizing' - PAGE_ID = 'drag_and_drop_v2_sizing' - ALIGN_ZONES = False # Set to True to test the feature that aligns draggable items when dropped. - - @classmethod - def _get_scenario_xml(cls): - """ - Set up the test scenario: - * An upper dndv2 xblock with a wide image (1600x900 SVG) - (on desktop and mobile, this background image will always fill the available width - and should have the same width as the item bank above) - * A lower dndv2 xblock with a small square image (500x500 SVG) - (on desktop, the square image is not as wide as the item bank, but on mobile it - may take up the whole width of the screen) - """ - params = { - "img": "wide", - "align_zones": cls.ALIGN_ZONES, - "img_wide_url": _svg_to_data_uri('dnd-bg-wide.svg'), - "img_square_url": _svg_to_data_uri('dnd-bg-square.svg'), - "img_400x300_url": _svg_to_data_uri('400x300.svg'), - "img_200x200_url": _svg_to_data_uri('200x200.svg'), - "img_60x60_url": _svg_to_data_uri('60x60.svg'), - } - upper_block = "".format( - data=loader.render_django_template("data/test_sizing_template.json", params) - ) - params["img"] = "square" - lower_block = "".format( - data=loader.render_django_template("data/test_sizing_template.json", params) - ) - - return "{}\n{}".format(upper_block, lower_block) - - EXPECTATIONS_DESKTOP = [ - # The text 'Auto' with no fixed size specified should be 3-20% wide - Expectation(item_id=0, zone_id=ZONE_33, width_percent=[3, AUTO_MAX_WIDTH_DESKTOP]), - # The long text with no fixed size specified should be wrapped at the maximum width - Expectation(item_id=1, zone_id=ZONE_33, width_percent=AUTO_MAX_WIDTH_DESKTOP), - # The text items that specify specific widths as a percentage of the background image: - Expectation(item_id=2, zone_id=ZONE_33, fixed_width_percent=33.3), - Expectation(item_id=3, zone_id=ZONE_50, fixed_width_percent=50), - Expectation(item_id=4, zone_id=ZONE_75, fixed_width_percent=75), - # A 400x300 image with automatic sizing should be constrained to the maximum width - Expectation(item_id=5, zone_id=ZONE_50, width_percent=AUTO_MAX_WIDTH_DESKTOP), - # A 200x200 image with automatic sizing - Expectation(item_id=6, zone_id=ZONE_50, width_percent=[24, 30.2]), - # A 400x300 image with a specified width of 50% - Expectation(item_id=7, zone_id=ZONE_50, fixed_width_percent=50), - # A 200x200 image with a specified width of 50% - Expectation(item_id=8, zone_id=ZONE_50, fixed_width_percent=50), - # A 60x60 auto-sized image should appear with pixel dimensions of 60x60 since it's - # too small to be shrunk be the default max-size. - Expectation(item_id=9, zone_id=ZONE_33, img_pixel_size_exact=[60, 60]), - ] - - EXPECTATIONS_MOBILE = [ - # The text 'Auto' with no fixed size specified should be 3-20% wide - Expectation( - item_id=0, - zone_id=ZONE_33, - width_percent_bank=[3, AUTO_MAX_WIDTH_MOBILE_TARGET_IMG], - width_percent_image=[3, AUTO_MAX_WIDTH_MOBILE_ITEM_BANK], - ), - # The long text with no fixed size specified should be wrapped at the maximum width - Expectation( - item_id=1, - zone_id=ZONE_33, - width_percent_bank=AUTO_MAX_WIDTH_MOBILE_ITEM_BANK, - width_percent_image=AUTO_MAX_WIDTH_MOBILE_TARGET_IMG, - ), - # The text items that specify specific widths as a percentage of the background image: - Expectation(item_id=2, zone_id=ZONE_33, fixed_width_percent=33.3), - Expectation(item_id=3, zone_id=ZONE_50, fixed_width_percent=50), - Expectation(item_id=4, zone_id=ZONE_75, fixed_width_percent=75), - # A 400x300 image with automatic sizing should be constrained to the maximum width, - # except on a large background image, where its natural size is smaller than max allowed size. - Expectation( - item_id=5, - zone_id=ZONE_50, - width_percent_bank=AUTO_MAX_WIDTH_MOBILE_ITEM_BANK, - width_percent_image=[25, AUTO_MAX_WIDTH_MOBILE_TARGET_IMG], - ), - # A 200x200 image with automatic sizing - Expectation( - item_id=6, - zone_id=ZONE_50, - width_percent_bank=[60, AUTO_MAX_WIDTH_MOBILE_ITEM_BANK], - width_percent_image=[10, AUTO_MAX_WIDTH_MOBILE_ITEM_BANK], - ), - # A 400x300 image with a specified width of 50% - Expectation(item_id=7, zone_id=ZONE_50, fixed_width_percent=50), - # A 200x200 image with a specified width of 50% - Expectation(item_id=8, zone_id=ZONE_50, fixed_width_percent=50), - # A 60x60 auto-sized image should appear with pixel dimensions of 60x60 since it's - # too small to be shrunk be the default max-size. - Expectation(item_id=9, zone_id=ZONE_33, img_pixel_size_exact=[60, 60]), - ] - - def test_wide_image_desktop(self): - """ Test the upper, larger, wide image in a desktop-sized window """ - self._check_sizes(0, self.EXPECTATIONS_DESKTOP) - - def test_square_image_desktop(self): - """ Test the lower, smaller, square image in a desktop-sized window """ - self._check_sizes(1, self.EXPECTATIONS_DESKTOP, expected_img_width=500) - - def _size_for_mobile(self): - self.browser.set_window_size(MOBILE_WINDOW_WIDTH, MOBILE_WINDOW_HEIGHT) - wait = WebDriverWait(self.browser, 2) - wait.until(lambda browser: browser.get_window_size()["width"] == MOBILE_WINDOW_WIDTH) - # Fix platform inconsistencies caused by scrollbar size: - self.browser.execute_script('$("body").css("margin-right", "40px")') - scrollbar_width = self.browser.execute_script( - "var $outer = $('
    ').css({visibility: 'hidden', width: 100, overflow: 'scroll'}).appendTo('body');" - "var widthWithScroll = $('
    ').css({width: '100%'}).appendTo($outer).outerWidth();" - "$outer.remove();" - "return 100 - widthWithScroll;" - ) - self.browser.execute_script('$(".wrapper-workbench").css("margin-right", "-{}px")'.format(40 + scrollbar_width)) - # And reduce the wasted space around our XBlock in the workbench: - self.browser.execute_script('return $(".workbench .preview").css("margin", "0")') - # Dynamically adjusting styles causes available screen width to change, but does not always emit - # resize events consistently, so emit resize manually to make sure the block adapts to the new size. - self.browser.execute_script('$(window).resize()') - - def _check_mobile_container_size(self): - """ Verify that the drag-container tightly fits into the available space. """ - drag_container = self._page.find_element_by_css_selector('.drag-container') - horizontal_padding = 20 - self.assertEqual(drag_container.size['width'], MOBILE_WINDOW_WIDTH - 2*horizontal_padding) - - def test_wide_image_mobile(self): - """ Test the upper, larger, wide image in a mobile-sized window """ - self._size_for_mobile() - self._check_mobile_container_size() - self._check_sizes(0, self.EXPECTATIONS_MOBILE, expected_img_width=1600, is_desktop=False) - - def test_square_image_mobile(self): - """ Test the lower, smaller, square image in a mobile-sized window """ - self._size_for_mobile() - self._check_mobile_container_size() - self._check_sizes(1, self.EXPECTATIONS_MOBILE, expected_img_width=500, is_desktop=False) - - def _check_width(self, item_description, item, container_width, expected_percent): - """ - Check that item 'item' has a width that is approximately the specified percentage - of container_width, or if expected_percent is a pair of numbers, that it is within - that range. - """ - width_pixels = item.size["width"] - width_percent = width_pixels / container_width * 100 - if isinstance(expected_percent, (list, tuple)): - min_expected, max_expected = expected_percent - msg = "{} should have width of {}% - {}%. Actual: {}px ({:.2f}% of {}px)".format( - item_description, min_expected, max_expected, width_pixels, width_percent, container_width - ) - self.assertGreaterEqual(width_percent, min_expected, msg) - self.assertLessEqual(width_percent, max_expected, msg) - else: - self.assertAlmostEqual( - width_percent, expected_percent, delta=1, - msg="{} should have width of ~{}% (+/- 1%). Actual: {}px ({:.2f}% of {}px)".format( - item_description, expected_percent, width_pixels, width_percent, container_width - ) - ) - - if item.find_elements_by_css_selector("img"): - # This item contains an image. The image should always fill the width of the draggable. - image = item.find_element_by_css_selector("img") - image_width_expected = item.size["width"] - 22 - self.assertAlmostEqual( - image.size["width"], image_width_expected, delta=1, - msg="{} image does not take up the full width of the draggable (width is {}px; expected {}px)".format( - item_description, image.size["width"], image_width_expected, - ) - ) - - def _check_img_pixel_dimensions(self, item_description, item, expect_w, expect_h): - img_element = item.find_element_by_css_selector("img") - self.assertEqual( - img_element.size, {"width": expect_w, "height": expect_h}, - msg="Expected {}'s image to have exact dimensions {}x{}px; found {}x{}px instead.".format( - item_description, expect_w, expect_h, img_element.size["width"], img_element.size["height"] - ) - ) - - def _check_sizes(self, block_index, expectations, expected_img_width=None, is_desktop=True): - """ Test the actual dimensions that each draggable has, in the bank and when placed """ - self._switch_to_block(block_index) - target_img = self._page.find_element_by_css_selector('.target-img') - target_img_width = target_img.size["width"] - item_bank = self._page.find_element_by_css_selector('.item-bank') - item_bank_width = item_bank.size["width"] - item_bank_height = item_bank.size["height"] - page_width = self._page.size["width"] # self._page is the .xblock--drag-and-drop div - - if is_desktop: - window_width = self.browser.get_window_size()["width"] - self.assertLessEqual(window_width, 1024) - else: - window_width = self.browser.get_window_size()["width"] - self.assertLessEqual(window_width, 400) - self.assertEqual(page_width, window_width - 40) - - # The item bank and other elements are inside a wrapper with 'padding: 1%', so we expect - # their width to be 98% of item_bank_width in general - self.assertAlmostEqual(target_img_width, expected_img_width or (page_width * 0.98), delta=1) - self.assertAlmostEqual(item_bank_width, page_width * 0.98, delta=1) - - # Test each element, before it is placed (while it is in the item bank). - for expect in expectations: - expected_width_percent = expect.width_percent_bank or expect.width_percent - if expected_width_percent is not None: - self._check_width( - item_description="Unplaced item {}".format(expect.item_id), - item=self._get_unplaced_item_by_value(expect.item_id), - container_width=item_bank_width, - expected_percent=expected_width_percent - ) - if expect.fixed_width_percent is not None: - self._check_width( - item_description="Unplaced item {} with fixed width".format(expect.item_id), - item=self._get_unplaced_item_by_value(expect.item_id), - container_width=target_img_width, - expected_percent=expect.fixed_width_percent, - ) - if expect.img_pixel_size_exact is not None: - self._check_img_pixel_dimensions( - "Unplaced item {}".format(expect.item_id), - self._get_unplaced_item_by_value(expect.item_id), - *expect.img_pixel_size_exact - ) - - # Test each element, after it it placed. - for expect in expectations: - self.place_item(expect.item_id, expect.zone_id, action_key=Keys.RETURN) - if expect.fixed_width_percent: - expected_width_percent = expect.fixed_width_percent - else: - expected_width_percent = expect.width_percent_image or expect.width_percent - if expected_width_percent is not None: - self._check_width( - item_description="Placed item {}".format(expect.item_id), - item=self._get_placed_item_by_value(expect.item_id), - container_width=target_img_width, - expected_percent=expected_width_percent, - ) - if expect.img_pixel_size_exact is not None: - self._check_img_pixel_dimensions( - "Placed item {}".format(expect.item_id), - self._get_placed_item_by_value(expect.item_id), - *expect.img_pixel_size_exact - ) - - # Test that the item bank maintains its original size. - self.assertEqual(item_bank.size["width"], item_bank_width) - self.assertEqual(item_bank.size["height"], item_bank_height) - - -class AlignedSizingTests(SizingTests): - """ - Run the same tests as SizingTests, but with aligned zones. - - The sizing of draggable items should be consistent when the "align" feature - of each zone is enabled. (This is the feature that aligns draggable items - once they're placed, rather than keeping them exactly where they were - dropped.) - """ - ALIGN_ZONES = True - - -class SizingBackwardsCompatibilityTests(InteractionTestBase, BaseIntegrationTest): - """ - Test backwards compatibility with data generated in older versions of this block. - - Older versions allowed authors to specify a fixed width and height for each draggable, in - pixels (new versions only have a configurable width, and it is a percent width). - """ - PAGE_TITLE = 'Drag and Drop v2 Sizing Backwards Compatibility' - PAGE_ID = 'drag_and_drop_v2_sizing_backwards_compatibility' - - @staticmethod - def _get_scenario_xml(): - """ - Set up the test scenario: - * One DndDv2 block using 'old_version_data.json' - """ - dnd_block = "".format( - data=loader.load_unicode("data/old_version_data.json") - ) - return "{}".format(dnd_block) - - def test_draggable_sizes(self): - """ Test the fixed pixel widths set in old versions of the block """ - self._expect_width_px(item_id=0, width_px=190, zone_id="Zone 1") - self._expect_width_px(item_id=1, width_px=190, zone_id="Zone 2") - self._expect_width_px(item_id=2, width_px=100, zone_id="Zone 1") - - def _expect_width_px(self, item_id, width_px, zone_id): - item = self._get_unplaced_item_by_value(item_id) - self.assertEqual(item.size["width"], width_px) - self.place_item(item_id, zone_id) - item = self._get_placed_item_by_value(item_id) - self.assertEqual(item.size["width"], width_px) diff --git a/tests/integration/test_studio.py b/tests/integration/test_studio.py deleted file mode 100644 index 4551c8acb..000000000 --- a/tests/integration/test_studio.py +++ /dev/null @@ -1,289 +0,0 @@ -from __future__ import absolute_import - -import time - -import six -from six.moves import range -from xblockutils.studio_editable_test import StudioEditableBaseTest - - -class TestStudio(StudioEditableBaseTest): - """ - Tests that cover the editing interface in the Studio. - """ - - def load_scenario(self, xml=''): - self.set_scenario_xml(xml) - self.element = self.go_to_view('studio_view') - self.fix_js_environment() - - def click_continue(self): - continue_button = self.element.find_element_by_css_selector('.continue-button') - self.scroll_into_view(continue_button) - continue_button.click() - - def scroll_into_view(self, element): - """ - Scrolls to the element and places cursor above it. - Useful when you want to click an element that is scrolled off - the visible area of the screen. - """ - # We have to use block: 'end' rather than the default 'start' because there's a fixed - # title bar in the studio view in the workbench that can obstruct the element. - script = "arguments[0].scrollIntoView({behavior: 'instant', block: 'end'})" - self.browser.execute_script(script, element) - - @property - def feedback_tab(self): - return self.element.find_element_by_css_selector('.feedback-tab') - - @property - def zones_tab(self): - return self.element.find_element_by_css_selector('.zones-tab') - - @property - def items_tab(self): - return self.element.find_element_by_css_selector('.items-tab') - - @property - def background_image_type_radio_buttons(self): - radio_buttons = self.zones_tab.find_elements_by_css_selector('.background-image-type input[type="radio"]') - self.assertEqual(len(radio_buttons), 2) - self.assertEqual(radio_buttons[0].get_attribute('value'), 'manual') - self.assertEqual(radio_buttons[1].get_attribute('value'), 'auto') - return {'manual': radio_buttons[0], 'auto': radio_buttons[1]} - - @property - def display_labels_checkbox(self): - return self.zones_tab.find_element_by_css_selector('.display-labels') - - @property - def background_image_url_field(self): - return self.zones_tab.find_element_by_css_selector('.background-manual .background-url') - - @property - def background_image_url_button(self): - return self.zones_tab.find_element_by_css_selector('.background-manual button') - - @property - def autozone_cols_field(self): - return self.zones_tab.find_element_by_css_selector('.background-auto .autozone-layout-cols') - - @property - def autozone_rows_field(self): - return self.zones_tab.find_element_by_css_selector('.background-auto .autozone-layout-rows') - - @property - def autozone_width_field(self): - return self.zones_tab.find_element_by_css_selector('.background-auto .autozone-size-width') - - @property - def autozone_height_field(self): - return self.zones_tab.find_element_by_css_selector('.background-auto .autozone-size-height') - - @property - def autozone_generate_button(self): - return self.zones_tab.find_element_by_css_selector('.background-auto button') - - @property - def target_preview_img(self): - return self.zones_tab.find_element_by_css_selector('.target-img') - - @property - def zones(self): - return self.zones_tab.find_elements_by_css_selector('.zone-row') - - def go_to_view(self, view_name='student_view', student_id="student_1"): - element = super().go_to_view(view_name, student_id) - time.sleep(0.1) # This method is unreliable without a delay. - return element - - def test_defaults(self): - """ - Basic test to verify stepping through the editor steps and saving works. - """ - self.load_scenario() - # We start on the feedback tab. - self.assertTrue(self.feedback_tab.is_displayed()) - self.assertFalse(self.zones_tab.is_displayed()) - self.assertFalse(self.items_tab.is_displayed()) - # Continue to the zones tab. - self.click_continue() - self.assertFalse(self.feedback_tab.is_displayed()) - self.assertTrue(self.zones_tab.is_displayed()) - self.assertFalse(self.items_tab.is_displayed()) - # And finally to the items tab. - self.click_continue() - self.assertFalse(self.feedback_tab.is_displayed()) - self.assertFalse(self.zones_tab.is_displayed()) - self.assertTrue(self.items_tab.is_displayed()) - # Save the block and expect success. - self.click_save(expect_success=True) - - def test_custom_image(self): - """" - Verify user can provide a custom background image URL. - """ - default_bg_img_src = 'http://localhost:8081/resource/drag-and-drop-v2/public/img/triangle.png' - - self.load_scenario() - # Go to zones tab. - self.click_continue() - radio_buttons = self.background_image_type_radio_buttons - # Manual mode should be selected by default. - self.assertTrue(radio_buttons['manual'].is_selected()) - self.assertFalse(radio_buttons['auto'].is_selected()) - url_field = self.background_image_url_field - self.assertEqual(url_field.get_attribute('value'), '') - self.assertIn( - default_bg_img_src.split('http://localhost:8081/')[1], self.target_preview_img.get_attribute('src') - ) - - custom_bg_img_src = '{}?my-custom-image=true'.format(self.target_preview_img.get_attribute('src')) - - url_field.send_keys(custom_bg_img_src) - self.scroll_into_view(self.background_image_url_button) - self.background_image_url_button.click() - self.assertEqual(self.target_preview_img.get_attribute('src'), custom_bg_img_src) - self.click_continue() - self.click_save(expect_success=True) - - # Verify the custom image src was saved successfully. - self.element = self.go_to_view('student_view') - target_img = self.element.find_element_by_css_selector('.target-img') - self.assertEqual(target_img.get_attribute('src'), custom_bg_img_src) - - # Verify the background image URL field is set to custom image src when we go back to studio view. - self.element = self.go_to_view('studio_view') - self.click_continue() - self.assertEqual(self.background_image_url_field.get_attribute('value'), custom_bg_img_src) - - def _verify_autogenerated_zones(self, cols, rows, zone_width, zone_height, padding): - zones = self.zones - self.assertEqual(len(zones), rows * cols) - for col in range(cols): - for row in range(rows): - idx = col + (row * cols) - zone = zones[idx] - expected_values = { - 'zone-title': 'Zone {}'.format(idx + 1), - 'zone-width': zone_width, - 'zone-height': zone_height, - 'zone-x': (zone_width * col) + (padding * (col + 1)), - 'zone-y': (zone_height * row) + (padding * (row + 1)), - } - for name, expected_value in six.iteritems(expected_values): - field = zone.find_element_by_css_selector('.' + name) - self.assertEqual(field.get_attribute('value'), str(expected_value)) - - def test_auto_generated_image(self): - """ - Verify that background image and zones get generated successfully. - """ - cols = 3 - rows = 2 - zone_width = 150 - zone_height = 100 - padding = 20 - - self.load_scenario() - # Go to zones tab. - self.click_continue() - radio_buttons = self.background_image_type_radio_buttons - self.scroll_into_view(radio_buttons['auto']) - radio_buttons['auto'].click() - # Manual background controls should be hidden. - self.assertFalse(self.background_image_url_field.is_displayed()) - self.assertFalse(self.background_image_url_button.is_displayed()) - # Display labels checkbox should be unchecked by default. - self.assertFalse(self.display_labels_checkbox.is_selected()) - # Enter zone properties for automatic generation. - self.autozone_cols_field.clear() - self.autozone_cols_field.send_keys(cols) - self.autozone_rows_field.clear() - self.autozone_rows_field.send_keys(rows) - self.autozone_width_field.clear() - self.autozone_width_field.send_keys(zone_width) - self.autozone_height_field.clear() - self.autozone_height_field.send_keys(zone_height) - # Click the generate button. - self.scroll_into_view(self.autozone_generate_button) - self.autozone_generate_button.click() - # Verify generated data-uri was set successfully. - generated_url = self.target_preview_img.get_attribute('src') - self.assertTrue(generated_url.startswith('data:image/svg+xml;')) - expected_width = (zone_width * cols) + (padding * (cols + 1)) - expected_height = (zone_height * rows) + (padding * (rows + 1)) - self.assertEqual(self.target_preview_img.get_attribute('naturalWidth'), str(expected_width)) - self.assertEqual(self.target_preview_img.get_attribute('naturalHeight'), str(expected_height)) - # Display labels checkbox should be automatically selected. - self.assertTrue(self.display_labels_checkbox.is_selected()) - # Verify there are exactly 6 zones, and their properties are correct. - self._verify_autogenerated_zones(cols, rows, zone_width, zone_height, padding) - - # Fill in zone descriptions to make the form valid (zone descriptions are required). - for zone in self.zones: - zone.find_element_by_css_selector('.zone-description').send_keys('Description') - - # Save the block. - self.click_continue() - self.click_save(expect_success=True) - - # Verify the custom image src was saved successfully. - self.element = self.go_to_view('student_view') - target_img = self.element.find_element_by_css_selector('.target-img') - self.assertTrue(target_img.get_attribute('src').startswith('data:image/svg+xml')) - self.assertEqual(target_img.get_attribute('naturalWidth'), str(expected_width)) - self.assertEqual(target_img.get_attribute('naturalHeight'), str(expected_height)) - - # Verify the background image URL field is set to custom image src when we go back to studio view. - self.element = self.go_to_view('studio_view') - self.click_continue() - radio_buttons = self.background_image_type_radio_buttons - self.assertFalse(radio_buttons['manual'].is_selected()) - self.assertTrue(radio_buttons['auto'].is_selected()) - self.assertEqual(self.autozone_cols_field.get_attribute('value'), str(cols)) - self.assertEqual(self.autozone_rows_field.get_attribute('value'), str(rows)) - self.assertEqual(self.autozone_width_field.get_attribute('value'), str(zone_width)) - self.assertEqual(self.autozone_height_field.get_attribute('value'), str(zone_height)) - - def test_autozone_parameter_validation(self): - """ - Test that autozone parameters are verified to be valid. - """ - self.load_scenario() - # Go to zones tab. - self.click_continue() - radio_buttons = self.background_image_type_radio_buttons - self.scroll_into_view(radio_buttons['auto']) - radio_buttons['auto'].click() - # All fields are valid initially. - self.assertFalse('field-error' in self.autozone_cols_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_rows_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_width_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_height_field.get_attribute('class')) - # Set two of the fields to invalid values. - self.autozone_cols_field.clear() - self.autozone_cols_field.send_keys('2.5') - self.autozone_height_field.clear() - self.autozone_height_field.send_keys('100A') - # Try to generate the image. - self.scroll_into_view(self.autozone_generate_button) - self.autozone_generate_button.click() - # The two bad fields should show errors. - self.assertTrue('field-error' in self.autozone_cols_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_rows_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_width_field.get_attribute('class')) - self.assertTrue('field-error' in self.autozone_height_field.get_attribute('class')) - # Fix the faulty values. - self.autozone_cols_field.clear() - self.autozone_cols_field.send_keys('2') - self.autozone_height_field.clear() - self.autozone_height_field.send_keys('100') - self.scroll_into_view(self.autozone_generate_button) - self.autozone_generate_button.click() - # All good now. - self.assertFalse('field-error' in self.autozone_cols_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_rows_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_width_field.get_attribute('class')) - self.assertFalse('field-error' in self.autozone_height_field.get_attribute('class')) diff --git a/tests/integration/test_title_and_question.py b/tests/integration/test_title_and_question.py deleted file mode 100644 index 4de5d961e..000000000 --- a/tests/integration/test_title_and_question.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import absolute_import - -from ddt import data, ddt, unpack -from selenium.common.exceptions import NoSuchElementException -from workbench import scenarios - -from .test_base import BaseIntegrationTest - - -@ddt -class TestDragAndDropTitleAndProblem(BaseIntegrationTest): - @unpack - @data( - ('Plain text problem 1, header visible.', True), - ('Plain text problem 2, header hidden.', False), - ('Problem/instructions with HTML and header.', True), - ('Span problem, no header', False), - ) - def test_problem_parameters(self, problem_text, show_problem_header): - const_page_name = 'Test title and problem parameters' - const_page_id = 'test_block_title_and_problem' - scenario_xml = self._make_scenario_xml( - display_name="Title", - show_title=True, - problem_text=problem_text, - show_problem_header=show_problem_header, - ) - scenarios.add_xml_scenario(const_page_id, const_page_name, scenario_xml) - self.addCleanup(scenarios.remove_scenario, const_page_id) - - page = self.go_to_page(const_page_name) - is_problem_header_visible = len(page.find_elements_by_css_selector('.problem > h4')) > 0 - self.assertEqual(is_problem_header_visible, show_problem_header) - - problem = page.find_element_by_css_selector('.problem > p') - self.assertEqual(self.get_element_html(problem), problem_text) - - @unpack - @data( - ('plain shown', 'title1', True), - ('plain hidden', 'title2', False), - ('html shown', 'title with HTML', True), - ('html hidden', 'Title: HTML?', False) - ) - def test_title_parameters(self, _, display_name, show_title): - const_page_name = 'Test show title parameter' - const_page_id = 'test_block_show_title' - scenario_xml = self._make_scenario_xml( - display_name=display_name, - show_title=show_title, - problem_text='Generic problem', - ) - scenarios.add_xml_scenario(const_page_id, const_page_name, scenario_xml) - self.addCleanup(scenarios.remove_scenario, const_page_id) - - page = self.go_to_page(const_page_name) - if show_title: - problem_header = page.find_element_by_css_selector('h3.hd.hd-3.problem-header') - self.assertEqual(self.get_element_html(problem_header), display_name) - else: - with self.assertRaises(NoSuchElementException): - page.find_element_by_css_selector('h3.hd.hd-3.problem-header') diff --git a/tests/unit/test_fixtures.py b/tests/unit/test_fixtures.py index 59048bda3..e99839144 100644 --- a/tests/unit/test_fixtures.py +++ b/tests/unit/test_fixtures.py @@ -2,7 +2,7 @@ import json -from xblockutils.resources import ResourceLoader +from xblock.utils.resources import ResourceLoader from tests.utils import TestCaseMixin, make_block diff --git a/tox.ini b/tox.ini index f41696c24..87c416a16 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38-django{32,42},quality,integration-django{32,42},translations-django{32,42} +envlist = py38-django{32,42},quality,translations-django{32,42} [pycodestyle] exclude = .git,.tox @@ -27,34 +27,6 @@ commands = mkdir -p var pytest {posargs:tests/unit/ --cov drag_and_drop_v2} -[testenv:integration-django32] -allowlist_externals = - make - xvfb-run -deps = - Django>=3.2,<4.0 - -r{toxinidir}/requirements/workbench.txt -setenv = - PATH = test_helpers/firefox{:}{env:PATH} - WORKBENCH_DATABASES = \{"default": \{"ENGINE": "django.db.backends.mysql", "NAME": "db", "USER": "root", "PASSWORD": "rootpw", "HOST": "127.0.0.1", "PORT": "3307"\}\} -commands = - make install_firefox - xvfb-run ./run_tests.py {posargs:tests.integration} - -[testenv:integration-django42] -allowlist_externals = - make - xvfb-run -deps = - Django>=4.2,<4.3 - -r{toxinidir}/requirements/workbench.txt -setenv = - PATH = test_helpers/firefox{:}{env:PATH} - WORKBENCH_DATABASES = \{"default": \{"ENGINE": "django.db.backends.mysql", "NAME": "db", "USER": "root", "PASSWORD": "rootpw", "HOST": "127.0.0.1", "PORT": "3307"\}\} -commands = - make install_firefox - xvfb-run ./run_tests.py {posargs:tests.integration} - [testenv:quality] deps = -r{toxinidir}/requirements/quality.txt