-
Notifications
You must be signed in to change notification settings - Fork 218
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Adds xblock-utils repository code into this repository
- Loading branch information
Showing
41 changed files
with
2,786 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import pytest | ||
|
||
|
||
# https://pytest-django.readthedocs.io/en/latest/faq.html#how-can-i-give-database-access-to-all-my-tests-without-the-django-db-marker | ||
@pytest.fixture(autouse=True) | ||
def enable_db_access_for_all_tests(db): | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
|
||
fs | ||
lxml | ||
mako # Used by xblockutils.resources | ||
markupsafe | ||
python-dateutil | ||
pytz | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
""" | ||
Useful classes and functionality for building and testing XBlocks | ||
""" | ||
|
||
__version__ = '3.4.1' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
# | ||
# Copyright (C) 2014-2015 edX | ||
# | ||
# This software's license gives you freedom; you can copy, convey, | ||
# propagate, redistribute and/or modify this program under the terms of | ||
# the GNU Affero General Public License (AGPL) as published by the Free | ||
# Software Foundation (FSF), either version 3 of the License, or (at your | ||
# option) any later version of the AGPL published by the FSF. | ||
# | ||
# This program is distributed in the hope that it will be useful, but | ||
# WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero | ||
# General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU Affero General Public License | ||
# along with this program in a file in the toplevel directory called | ||
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>. | ||
# | ||
""" | ||
Base classes for Selenium or bok-choy based integration tests of XBlocks. | ||
""" | ||
|
||
import time | ||
|
||
from selenium.webdriver.support.ui import WebDriverWait | ||
from workbench.runtime import WorkbenchRuntime | ||
from workbench.scenarios import SCENARIOS, add_xml_scenario, remove_scenario | ||
from workbench.test.selenium_test import SeleniumTest | ||
|
||
from .resources import ResourceLoader | ||
|
||
|
||
class SeleniumXBlockTest(SeleniumTest): | ||
""" | ||
Base class for using the workbench to test XBlocks with Selenium or bok-choy. | ||
If you want to test an XBlock that's not already installed into the python environment, | ||
you can use @XBlock.register_temp_plugin around your test method[s]. | ||
""" | ||
timeout = 10 # seconds | ||
|
||
def setUp(self): | ||
super().setUp() | ||
# Delete all scenarios from the workbench: | ||
# Trigger initial scenario load. | ||
import workbench.urls # pylint: disable=import-outside-toplevel | ||
SCENARIOS.clear() | ||
# Disable CSRF checks on XBlock handlers: | ||
import workbench.views # pylint: disable=import-outside-toplevel | ||
workbench.views.handler.csrf_exempt = True | ||
|
||
def wait_until_visible(self, elem): | ||
""" Wait until the given element is visible """ | ||
wait = WebDriverWait(elem, self.timeout) | ||
wait.until(lambda e: e.is_displayed(), f"{elem.text} should be visible") | ||
|
||
def wait_until_hidden(self, elem): | ||
""" Wait until the DOM element elem is hidden """ | ||
wait = WebDriverWait(elem, self.timeout) | ||
wait.until(lambda e: not e.is_displayed(), f"{elem.text} should be hidden") | ||
|
||
def wait_until_disabled(self, elem): | ||
""" Wait until the DOM element elem is disabled """ | ||
wait = WebDriverWait(elem, self.timeout) | ||
wait.until(lambda e: not e.is_enabled(), f"{elem.text} should be disabled") | ||
|
||
def wait_until_clickable(self, elem): | ||
""" Wait until the DOM element elem is display and enabled """ | ||
wait = WebDriverWait(elem, self.timeout) | ||
wait.until(lambda e: e.is_displayed() and e.is_enabled(), f"{elem.text} should be clickable") | ||
|
||
def wait_until_text_in(self, text, elem): | ||
""" Wait until the specified text appears in the DOM element elem """ | ||
wait = WebDriverWait(elem, self.timeout) | ||
wait.until(lambda e: text in e.text, f"{text} should be in {elem.text}") | ||
|
||
def wait_until_html_in(self, html, elem): | ||
""" Wait until the specified HTML appears in the DOM element elem """ | ||
wait = WebDriverWait(elem, self.timeout) | ||
wait.until(lambda e: html in e.get_attribute('innerHTML'), | ||
"{} should be in {}".format(html, elem.get_attribute('innerHTML'))) | ||
|
||
def wait_until_exists(self, selector): | ||
""" Wait until the specified selector exists on the page """ | ||
wait = WebDriverWait(self.browser, self.timeout) | ||
wait.until( | ||
lambda driver: driver.find_element_by_css_selector(selector), | ||
f"Selector '{selector}' should exist." | ||
) | ||
|
||
@staticmethod | ||
def set_scenario_xml(xml): | ||
""" Reset the workbench to have only one scenario with the specified XML """ | ||
SCENARIOS.clear() | ||
add_xml_scenario("test", "Test Scenario", xml) | ||
|
||
def go_to_view(self, view_name='student_view', student_id="student_1"): | ||
""" | ||
Navigate to the page `page_name`, as listed on the workbench home | ||
Returns the DOM element on the visited page located by the `css_selector` | ||
""" | ||
url = self.live_server_url + f'/scenario/test/{view_name}/' | ||
if student_id: | ||
url += f'?student={student_id}' | ||
self.browser.get(url) | ||
return self.browser.find_element_by_css_selector('.workbench .preview > div.xblock-v1:first-child') | ||
|
||
def load_root_xblock(self, student_id="student_1"): | ||
""" | ||
Load (in Python) the XBlock at the root of the current scenario. | ||
""" | ||
dom_node = self.browser.find_element_by_css_selector('.workbench .preview > div.xblock-v1:first-child') | ||
usage_id = dom_node.get_attribute('data-usage') | ||
runtime = WorkbenchRuntime(student_id) | ||
return runtime.get_block(usage_id) | ||
|
||
|
||
class SeleniumBaseTest(SeleniumXBlockTest): | ||
""" | ||
Selenium Base Test for loading a whole folder of XML scenarios and then running tests. | ||
This is kept for compatibility, but it is recommended that SeleniumXBlockTest be used | ||
instead, since it is faster and more flexible (specifically, scenarios are only loaded | ||
as needed, and can be defined inline with the tests). | ||
""" | ||
module_name = None # You must set this to __name__ in any subclass so ResourceLoader can find scenario XML files | ||
default_css_selector = None # Selector used by go_to_page to return the XBlock DOM element | ||
relative_scenario_path = 'xml' # Path from the module (module_name) to the secnario XML files | ||
|
||
@property | ||
def _module_name(self): | ||
""" Internal method to access module_name with a friendly warning if it's unset """ | ||
if self.module_name is None: | ||
raise NotImplementedError("Overwrite cls.module_name in your derived class.") | ||
return self.module_name | ||
|
||
@property | ||
def _default_css_selector(self): | ||
""" Internal method to access default_css_selector with a warning if it's unset """ | ||
if self.default_css_selector is None: | ||
raise NotImplementedError("Overwrite cls.default_css_selector in your derived class.") | ||
return self.default_css_selector | ||
|
||
def setUp(self): | ||
super().setUp() | ||
# Use test scenarios: | ||
loader = ResourceLoader(self._module_name) | ||
scenarios_list = loader.load_scenarios_from_path(self.relative_scenario_path, include_identifier=True) | ||
for identifier, title, xml in scenarios_list: | ||
add_xml_scenario(identifier, title, xml) | ||
self.addCleanup(remove_scenario, identifier) | ||
|
||
# Suzy opens the browser to visit the workbench | ||
self.browser.get(self.live_server_url) | ||
|
||
# She knows it's the site by the header | ||
header1 = self.browser.find_element_by_css_selector('h1') | ||
self.assertEqual(header1.text, 'XBlock scenarios') | ||
|
||
def go_to_page(self, page_name, css_selector=None, view_name=None): | ||
""" | ||
Navigate to the page `page_name`, as listed on the workbench home | ||
Returns the DOM element on the visited page located by the `css_selector` | ||
""" | ||
if css_selector is None: | ||
css_selector = self._default_css_selector | ||
|
||
self.browser.get(self.live_server_url) | ||
target_url = self.browser.find_element_by_link_text(page_name).get_attribute('href') | ||
if view_name: | ||
target_url += f'{view_name}/' | ||
self.browser.get(target_url) | ||
time.sleep(1) | ||
block = self.browser.find_element_by_css_selector(css_selector) | ||
return block |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
""" | ||
Useful helper methods | ||
""" | ||
|
||
|
||
def child_isinstance(block, child_id, block_class_or_mixin): | ||
""" | ||
Efficiently check if a child of an XBlock is an instance of the given class. | ||
Arguments: | ||
block -- the parent (or ancestor) of the child block in question | ||
child_id -- the usage key of the child block we are wondering about | ||
block_class_or_mixin -- We return true if block's child indentified by child_id is an | ||
instance of this. | ||
This method is equivalent to | ||
isinstance(block.runtime.get_block(child_id), block_class_or_mixin) | ||
but is far more efficient, as it avoids the need to instantiate the child. | ||
""" | ||
def_id = block.runtime.id_reader.get_definition_id(child_id) | ||
type_name = block.runtime.id_reader.get_block_type(def_id) | ||
child_class = block.runtime.load_block_type(type_name) | ||
return issubclass(child_class, block_class_or_mixin) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
function StudioContainerXBlockWithNestedXBlocksMixin(runtime, element) { | ||
var $buttons = $(".add-xblock-component-button", element), | ||
$addComponent = $('.add-xblock-component', element), | ||
$element = $(element); | ||
|
||
function isSingleInstance($button) { | ||
return $button.data('single-instance'); | ||
} | ||
|
||
// We use delegated events here, i.e., not binding a click event listener | ||
// directly to $buttons, because we want to make sure any other click event | ||
// listeners of the button are called first before we disable the button. | ||
// Ref: OSPR-1393 | ||
$addComponent.on('click', '.add-xblock-component-button', function(ev) { | ||
var $button = $(ev.currentTarget); | ||
if ($button.is('.disabled')) { | ||
ev.preventDefault(); | ||
ev.stopPropagation(); | ||
} else { | ||
if (isSingleInstance($button)) { | ||
$button.addClass('disabled'); | ||
$button.attr('disabled', 'disabled'); | ||
} | ||
} | ||
}); | ||
|
||
function updateButtons() { | ||
var nestedBlockLocations = $.map($element.find(".studio-xblock-wrapper"), function(block_wrapper) { | ||
return $(block_wrapper).data('locator'); | ||
}); | ||
|
||
$buttons.each(function() { | ||
var $this = $(this); | ||
if (!isSingleInstance($this)) { | ||
return; | ||
} | ||
var category = $this.data('category'); | ||
var childExists = false; | ||
|
||
// FIXME: This is potentially buggy - if some XBlock's category is a substring of some other XBlock category | ||
// it will exhibit wrong behavior. However, it's not possible to do anything about that unless studio runtime | ||
// announces which block was deleted, not it's parent. | ||
for (var i = 0; i < nestedBlockLocations.length; i++) { | ||
if (nestedBlockLocations[i].indexOf(category) > -1) { | ||
childExists = true; | ||
break; | ||
} | ||
} | ||
|
||
if (childExists) { | ||
$this.attr('disabled', 'disabled'); | ||
$this.addClass('disabled') | ||
} | ||
else { | ||
$this.removeAttr('disabled'); | ||
$this.removeClass('disabled'); | ||
} | ||
}); | ||
} | ||
|
||
updateButtons(); | ||
runtime.listenTo('deleted-child', updateButtons); | ||
} |
Oops, something went wrong.