From e10adaba265c09bcc9783dfd552e2876cd8d01db Mon Sep 17 00:00:00 2001 From: bjones1 Date: Mon, 15 Mar 2021 23:50:54 +0100 Subject: [PATCH 1/4] Add: more testing for a timed exam. Fix: Improve unit testing approach: Run webpack before each test Properly close the Selenium driver Provide a wait_until_ready method --- runestone/timed/test/test_timed.py | 122 ++++++++++++++++++++++++----- runestone/unittest_base.py | 34 ++++++-- 2 files changed, 128 insertions(+), 28 deletions(-) diff --git a/runestone/timed/test/test_timed.py b/runestone/timed/test/test_timed.py index 1f0fb3503..3bbdd570e 100644 --- a/runestone/timed/test/test_timed.py +++ b/runestone/timed/test/test_timed.py @@ -1,36 +1,23 @@ -import warnings - -warnings.filterwarnings("ignore", category=DeprecationWarning) -import unittest import time + from runestone.unittest_base import module_fixture_maker, RunestoneTestCase -from selenium import webdriver -from selenium.webdriver.common.by import By from selenium.webdriver import ActionChains -from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC -from selenium.common.exceptions import TimeoutException -from selenium.common.exceptions import NoSuchElementException setUpModule, tearDownModule = module_fixture_maker(__file__) class TimedTests(RunestoneTestCase): - def test_one_question_timed_exam(self): - self.driver.get(self.host + "/index.html") + def start(self, timed_divid): self.driver.execute_script("window.localStorage.clear();") - + ##self.wait_until_ready(timed_divid) + time.sleep(1) start = self.driver.find_element_by_id("start") start.click() - t1 = self.driver.find_element_by_id("time_test_1_q1_form") - t1.find_element_by_id("time_test_1_q1_opt_0").click() - t1.find_element_by_id("time_test_1_q1_opt_1").click() - t1.find_element_by_id("time_test_1_q1_opt_2").click() - t1.find_element_by_id("time_test_1_q1_opt_3").click() - - # finish = self.driver.find_element_by_id("finish") - finish = WebDriverWait(self.driver, 20).until( + def finish(self): + finish = self.wait.until( EC.element_to_be_clickable((By.ID, "finish")) ) self.assertIsNotNone(finish) @@ -39,7 +26,102 @@ def test_one_question_timed_exam(self): alert = self.driver.switch_to_alert() alert.accept() + def dragndrop(self, src, dest): + ActionChains(self.driver).drag_and_drop(src, dest).perform() + + def js_dragndrop(self): + with open("../../dragndrop/test/drag_and_drop_helper.js") as f: + self.driver.execute_script(f.read()) + + def test_one_question_timed_exam(self): + self.driver.get(self.host + "/index.html") + self.start("time_q1") + + t1 = self.driver.find_element_by_id("time_test_1_q1_form") + t1.find_element_by_id("time_test_1_q1_opt_0").click() + t1.find_element_by_id("time_test_1_q1_opt_1").click() + t1.find_element_by_id("time_test_1_q1_opt_2").click() + t1.find_element_by_id("time_test_1_q1_opt_3").click() + + self.finish() + fb = t1.find_element_by_id("time_test_1_q1_eachFeedback_1") self.assertIsNotNone(fb) cnamestr = fb.get_attribute("class") self.assertEqual(cnamestr, "eachFeedback alert alert-danger") + + def test_1(self): + self.driver.get(self.host + "/multiquestion.html") + self.start("timed1") + next = self.driver.find_element_by_id("next") + + # Select answer A in the mchoice question (which is correct). + self.driver.find_element_by_id("questiontimed1_1_opt_0").click() + next.click() + + # Click the correct cells in the table. There should be only one table. + ##self.wait_until_ready("clicktimed1") + time.sleep(1) + table = self.driver.find_elements_by_id("clicktimed1") + assert len(table) == 1 + table = table[0] + first_ellipsis = True + for elem in table.find_elements_by_css_selector("p"): + if elem.text == "correct": + elem.click() + if "…" in elem.text: + if first_ellipsis: + first_ellipsis = False + else: + elem.click() + next.click() + + # The drag and drop question. + ##self.wait_until_ready("dnd2") + time.sleep(1) + self.js_dragndrop() + src, dest = self.driver.find_elements_by_class_name("rsdraggable") + src_items = src.find_elements_by_tag_name("span") + for i in range(3): + self.driver.execute_script(f"""$("#{src_items[i].get_attribute('id')}").simulateDragDrop({{ dropTarget: 'span:contains("Answer {src_items[i].text[8:]}")' }})""") + next.click() + + # Fill in the blank question + ##self.wait_until_ready("fill1412") + time.sleep(1) + filb = self.driver.find_element_by_id("fill1412") + blank1, blank2 = filb.find_elements_by_tag_name("input") + blank1.send_keys("red") + blank2.send_keys("away") + next.click() + + # ActiveCode question + ##self.wait_until_ready("units2") + time.sleep(1) + code_mirror = self.driver.find_element_by_class_name("CodeMirror") + code_mirror.find_elements_by_class_name("CodeMirror-line")[1].click() + code_mirror.find_element_by_css_selector("textarea").send_keys(" - 4 + a + b") + next.click() + + # The Parson's problem. + ##self.wait_until_ready("morning") + time.sleep(1) + source = self.driver.find_element_by_id("parsons-1-source") + answer = self.driver.find_element_by_id("parsons-1-answer") + self.dragndrop(source.find_element_by_id("parsons-1-block-0"), answer) + time.sleep(1) + self.dragndrop(source.find_element_by_id("parsons-1-block-2"), answer) + time.sleep(1) + self.dragndrop(source.find_element_by_id("parsons-1-block-1"), answer) + next.click() + + # The short answer question. + ##self.wait_until_ready("question2") + time.sleep(1) + self.driver.find_element_by_id("question2_solution").send_keys("ROYGBIV circle area") + + self.finish() + results = self.driver.find_element_by_id("timed1results") + assert "Num Correct: 6" in results.text + assert "Num Wrong: 0" in results.text + assert "Num Skipped: 1" in results.text diff --git a/runestone/unittest_base.py b/runestone/unittest_base.py index 4ca8107e5..460d2dd7f 100644 --- a/runestone/unittest_base.py +++ b/runestone/unittest_base.py @@ -22,11 +22,13 @@ # Third-party imports # ------------------- +import pytest +from pyvirtualdisplay import Display from selenium import webdriver +from selenium.common.exceptions import StaleElementReferenceException from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait -import pytest -from pyvirtualdisplay import Display logging.basicConfig(level=logging.WARN) mylogger = logging.getLogger() @@ -54,6 +56,12 @@ # Code # ==== +# Run this once, before all tests, to update the webpacked JS. +@pytest.fixture(scope="session", autouse=True) +def run_webpack(): + subprocess.run(["npm", "run", "build"], check=True) + + # Define `module fixtures `_ to build the test Runestone project, run the server, then shut it down when the tests complete. class ModuleFixture(unittest.TestCase): def __init__( @@ -184,6 +192,7 @@ def setUpModule(self): def tearDownModule(self): # Shut down Selenium. + self.driver.close() self.driver.quit() if self.display: self.display.stop() @@ -213,6 +222,13 @@ def module_fixture_maker(module_path, return_mf=False, exit_status_success=True) # Provide a base test case which sets up the `Selenium `_ driver. class RunestoneTestCase(unittest.TestCase): + # Wait until a Runestone component has finished rendering itself, given the ID of the component. + def wait_until_ready(self, id): + # The component is ready when it has the class below. + self.wait.until( + element_has_css_class((By.ID, id), "runestone-component-ready") + ) + def setUp(self): # Use the shared module-wide driver. self.driver = mf.driver @@ -248,9 +264,11 @@ def __init__( self.css_class = css_class def __call__(self, driver): - # Find the referenced element. - element = driver.find_element(*self.locator) - if self.css_class in element.get_attribute("class"): - return element - else: - return False + # Find the referenced element. Ignore stale elements. + try: + element = driver.find_element(*self.locator) + if self.css_class in element.get_attribute("class"): + return element + except StaleElementReferenceException: + pass + return False From 3b03cb4ca807f159ac3c232ee7a4cd6c7388d94a Mon Sep 17 00:00:00 2001 From: bjones1 Date: Tue, 16 Mar 2021 00:21:04 +0100 Subject: [PATCH 2/4] Fix: Remove tests that depend on fixed components. --- runestone/timed/test/test_timed.py | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/runestone/timed/test/test_timed.py b/runestone/timed/test/test_timed.py index 3bbdd570e..2b4537757 100644 --- a/runestone/timed/test/test_timed.py +++ b/runestone/timed/test/test_timed.py @@ -60,30 +60,11 @@ def test_1(self): next.click() # Click the correct cells in the table. There should be only one table. - ##self.wait_until_ready("clicktimed1") time.sleep(1) - table = self.driver.find_elements_by_id("clicktimed1") - assert len(table) == 1 - table = table[0] - first_ellipsis = True - for elem in table.find_elements_by_css_selector("p"): - if elem.text == "correct": - elem.click() - if "…" in elem.text: - if first_ellipsis: - first_ellipsis = False - else: - elem.click() next.click() # The drag and drop question. - ##self.wait_until_ready("dnd2") time.sleep(1) - self.js_dragndrop() - src, dest = self.driver.find_elements_by_class_name("rsdraggable") - src_items = src.find_elements_by_tag_name("span") - for i in range(3): - self.driver.execute_script(f"""$("#{src_items[i].get_attribute('id')}").simulateDragDrop({{ dropTarget: 'span:contains("Answer {src_items[i].text[8:]}")' }})""") next.click() # Fill in the blank question @@ -122,6 +103,6 @@ def test_1(self): self.finish() results = self.driver.find_element_by_id("timed1results") - assert "Num Correct: 6" in results.text + assert "Num Correct: 4" in results.text assert "Num Wrong: 0" in results.text - assert "Num Skipped: 1" in results.text + assert "Num Skipped: 3" in results.text From 8465c83b87111d86a0d93edde032c088b32f122a Mon Sep 17 00:00:00 2001 From: bjones1 Date: Tue, 16 Mar 2021 09:14:13 +0100 Subject: [PATCH 3/4] Fix: webpack build is now run by the unit test framework. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0dd8b2026..5e54de44e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,6 @@ script: - "python -m pip install -U pip" - "pip install -e ." - "pip install -U -r requirements-dev.txt" -- "npm run build" - "pytest" notifications: slack: runestoneteam:aKSajF6qfp9WpgisFH8mocs6 From 1e39b9b84ec3e562a5dc1d00782540c6cfff93b0 Mon Sep 17 00:00:00 2001 From: bjones1 Date: Tue, 16 Mar 2021 15:32:18 +0100 Subject: [PATCH 4/4] Fix: Add ugly delay to fix test failure. --- runestone/shortanswer/test/test_shortanswer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runestone/shortanswer/test/test_shortanswer.py b/runestone/shortanswer/test/test_shortanswer.py index 7d01e407f..6d2b2c100 100644 --- a/runestone/shortanswer/test/test_shortanswer.py +++ b/runestone/shortanswer/test/test_shortanswer.py @@ -4,6 +4,7 @@ __author__ = "yasinovskyy" +from time import sleep from runestone.unittest_base import module_fixture_maker, RunestoneTestCase setUpModule, tearDownModule = module_fixture_maker(__file__) @@ -23,6 +24,7 @@ def test_sa1(self): def test_sa2(self): """No input. Button clicked""" self.driver.get(self.host + "/index.html") + sleep(1) t1 = self.driver.find_element_by_id("question1") btn_check = t1.find_element_by_tag_name("button")