From 891bddcdf947b46e9e9e123ed40083311e52324d Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 7 Mar 2013 14:28:09 -0500 Subject: [PATCH 01/18] need to support link references to static content that is in the code base (e.g. module's .js/.css) --- common/djangoapps/static_replace/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py index fb1f48d143..c839212c26 100644 --- a/common/djangoapps/static_replace/__init__.py +++ b/common/djangoapps/static_replace/__init__.py @@ -84,12 +84,15 @@ def replace_static_urls(text, data_directory, course_namespace=None): if rest.endswith('?raw'): return original - # course_namespace is not None, then use studio style urls - if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore): - url = StaticContent.convert_legacy_static_url(rest, course_namespace) # In debug mode, if we can find the url as is, - elif settings.DEBUG and finders.find(rest, True): + if settings.DEBUG and finders.find(rest, True): return original + # course_namespace is not None, then use studio style urls + elif course_namespace is not None and not isinstance(modulestore(), XMLModuleStore): + if staticfiles_storage.exists(rest): + url = staticfiles_storage.url(rest) + else: + url = StaticContent.convert_legacy_static_url(rest, course_namespace) # Otherwise, look the file up in staticfiles_storage, and append the data directory if needed else: course_path = "/".join((data_directory, rest)) From c23c5cc3d99c1772c339bc287c7c78de00945ea8 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 7 Mar 2013 16:12:21 -0500 Subject: [PATCH 02/18] add comments --- common/djangoapps/static_replace/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py index c839212c26..b73a658c5f 100644 --- a/common/djangoapps/static_replace/__init__.py +++ b/common/djangoapps/static_replace/__init__.py @@ -87,11 +87,15 @@ def replace_static_urls(text, data_directory, course_namespace=None): # In debug mode, if we can find the url as is, if settings.DEBUG and finders.find(rest, True): return original - # course_namespace is not None, then use studio style urls + # if we're running with a MongoBacked store course_namespace is not None, then use studio style urls elif course_namespace is not None and not isinstance(modulestore(), XMLModuleStore): + # first look in the static file pipeline and see if we are trying to reference + # a piece of static content which is in the mitx repo (e.g. JS associated with an xmodule) if staticfiles_storage.exists(rest): url = staticfiles_storage.url(rest) else: + # if not, then assume it's courseware specific content and then look in the + # Mongo-backed database url = StaticContent.convert_legacy_static_url(rest, course_namespace) # Otherwise, look the file up in staticfiles_storage, and append the data directory if needed else: From 6a54c2a2bc1cd54d265f7fda72b8cfa99ea778ef Mon Sep 17 00:00:00 2001 From: Will Daly Date: Thu, 7 Mar 2013 17:01:56 -0500 Subject: [PATCH 03/18] Fixed checkbox bug in capa_module --- common/lib/xmodule/xmodule/capa_module.py | 10 ++-- .../xmodule/xmodule/tests/test_capa_module.py | 54 ++++++++++++++----- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index b0d3950f06..2597690572 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -582,7 +582,7 @@ class CapaModule(XModule): @staticmethod def make_dict_of_responses(get): '''Make dictionary of student responses (aka "answers") - get is POST dictionary. + get is POST dictionary (Djano QueryDict). The *get* dict has keys of the form 'x_y', which are mapped to key 'y' in the returned dict. For example, @@ -606,6 +606,7 @@ class CapaModule(XModule): to 'input_1' in the returned dict) ''' answers = dict() + for key in get: # e.g. input_resistor_1 ==> resistor_1 _, _, name = key.partition('_') @@ -613,7 +614,7 @@ class CapaModule(XModule): # If key has no underscores, then partition # will return (key, '', '') # We detect this and raise an error - if name is '': + if not name: raise ValueError("%s must contain at least one underscore" % str(key)) else: @@ -625,10 +626,7 @@ class CapaModule(XModule): name = name[:-2] if is_list_key else name if is_list_key: - if type(get[key]) is list: - val = get[key] - else: - val = [get[key]] + val = get.getlist(key) else: val = get[key] diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 6330511fc5..cb77921957 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -11,6 +11,8 @@ from xmodule.capa_module import CapaModule from xmodule.modulestore import Location from lxml import etree +from django.http import QueryDict + from . import test_system @@ -326,14 +328,18 @@ class CapaModuleTest(unittest.TestCase): def test_parse_get_params(self): + # We have to set up Django settings in order to use QueryDict + from django.conf import settings + settings.configure() + # Valid GET param dict - valid_get_dict = {'input_1': 'test', - 'input_1_2': 'test', - 'input_1_2_3': 'test', - 'input_[]_3': 'test', - 'input_4': None, - 'input_5': [], - 'input_6': 5} + valid_get_dict = self._querydict_from_dict({'input_1': 'test', + 'input_1_2': 'test', + 'input_1_2_3': 'test', + 'input_[]_3': 'test', + 'input_4': None, + 'input_5': [], + 'input_6': 5}) result = CapaModule.make_dict_of_responses(valid_get_dict) @@ -347,20 +353,19 @@ class CapaModuleTest(unittest.TestCase): # Valid GET param dict with list keys - valid_get_dict = {'input_2[]': ['test1', 'test2']} + valid_get_dict = self._querydict_from_dict({'input_2[]': ['test1', 'test2']}) result = CapaModule.make_dict_of_responses(valid_get_dict) self.assertTrue('2' in result) - self.assertEqual(valid_get_dict['input_2[]'], result['2']) + self.assertEqual(['test1','test2'], result['2']) # If we use [] at the end of a key name, we should always # get a list, even if there's just one value - valid_get_dict = {'input_1[]': 'test'} + valid_get_dict = self._querydict_from_dict({'input_1[]': 'test'}) result = CapaModule.make_dict_of_responses(valid_get_dict) self.assertEqual(result['1'], ['test']) - # If we have no underscores in the name, then the key is invalid - invalid_get_dict = {'input': 'test'} + invalid_get_dict = self._querydict_from_dict({'input': 'test'}) with self.assertRaises(ValueError): result = CapaModule.make_dict_of_responses(invalid_get_dict) @@ -368,11 +373,32 @@ class CapaModuleTest(unittest.TestCase): # Two equivalent names (one list, one non-list) # One of the values would overwrite the other, so detect this # and raise an exception - invalid_get_dict = {'input_1[]': 'test 1', - 'input_1': 'test 2' } + invalid_get_dict = self._querydict_from_dict({'input_1[]': 'test 1', + 'input_1': 'test 2' }) with self.assertRaises(ValueError): result = CapaModule.make_dict_of_responses(invalid_get_dict) + def _querydict_from_dict(self, param_dict): + """ Create a Django QueryDict from a Python dictionary """ + + # QueryDict objects are immutable by default, so we make + # a copy that we can update. + querydict = QueryDict('') + copyDict = querydict.copy() + + for (key, val) in param_dict.items(): + + # QueryDicts handle lists differently from ordinary values, + # so we have to specifically tell the QueryDict that + # this is a list + if type(val) is list: + copyDict.setlist(key, val) + else: + copyDict[key] = val + + return copyDict + + def test_check_problem_correct(self): module = CapaFactory.create(attempts=1) From 2c6ee50ac328849312d60f4af78cea5dcdf1dd1b Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 7 Mar 2013 19:39:43 -0500 Subject: [PATCH 04/18] Fix issue where javascript was disappearing from problem html --- common/lib/capa/capa/capa_problem.py | 4 ++-- .../lib/capa/capa/tests/test_html_render.py | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 14c590a660..8b32686985 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -29,6 +29,7 @@ import sys from lxml import etree from xml.sax.saxutils import unescape +from copy import deepcopy import chem import chem.chemcalc @@ -497,11 +498,10 @@ class LoncapaProblem(object): Used by get_html. ''' - if (problemtree.tag == 'script' and problemtree.get('type') and 'javascript' in problemtree.get('type')): # leave javascript intact. - return problemtree + return deepcopy(problemtree) if problemtree.tag in html_problem_semantics: return diff --git a/common/lib/capa/capa/tests/test_html_render.py b/common/lib/capa/capa/tests/test_html_render.py index ca2a3c2e2c..6c74d06ef4 100644 --- a/common/lib/capa/capa/tests/test_html_render.py +++ b/common/lib/capa/capa/tests/test_html_render.py @@ -3,6 +3,7 @@ from lxml import etree import os import textwrap import json + import mock from capa.capa_problem import LoncapaProblem @@ -49,6 +50,8 @@ class CapaHtmlRenderTest(unittest.TestCase): self.assertEqual(test_element.text, "Test include") + + def test_process_outtext(self): # Generate some XML with and xml_str = textwrap.dedent(""" @@ -86,6 +89,25 @@ class CapaHtmlRenderTest(unittest.TestCase): script_element = rendered_html.find('script') self.assertEqual(None, script_element) + def test_render_javascript(self): + # Generate some XML with a + + """) + + # Create the problem + problem = LoncapaProblem(xml_str, '1', system=test_system) + + # Render the HTML + rendered_html = etree.XML(problem.get_html()) + + + # expect the javascript is still present in the rendered html + self.assertTrue("" in etree.tostring(rendered_html)) + + def test_render_response_xml(self): # Generate some XML for a string response kwargs = {'question_text': "Test question", From 30f3923c156fb49e69525e8649aa83619349bbb7 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Thu, 7 Mar 2013 23:50:09 -0500 Subject: [PATCH 05/18] Quick patch to regression causing partial credit to break on 6.00x --- common/lib/capa/capa/correctmap.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py index f246b406d5..fd83fc29f8 100644 --- a/common/lib/capa/capa/correctmap.py +++ b/common/lib/capa/capa/correctmap.py @@ -115,11 +115,13 @@ class CorrectMap(object): If the answer is correct, return the assigned number of points (default: 1 point) Otherwise, return 0 points """ - if self.is_correct(answer_id): - npoints = self.get_property(answer_id, 'npoints') - return npoints if npoints is not None else 1 - else: - return 0 + npoints = self.get_property(answer_id, 'npoints') + if npoints is not None: + return npoints + elif self.is_correct(answer_id): + return 1 + # if not correct and no points have been assigned, return 0 + return 0 def set_property(self, answer_id, property, value): if answer_id in self.cmap: From 5f44b2de04f005dd17f917b47c28d0382ea397ec Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Thu, 7 Mar 2013 23:52:01 -0500 Subject: [PATCH 06/18] minor comment on partial credit --- common/lib/capa/capa/correctmap.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py index fd83fc29f8..ea56863a2f 100644 --- a/common/lib/capa/capa/correctmap.py +++ b/common/lib/capa/capa/correctmap.py @@ -111,10 +111,7 @@ class CorrectMap(object): return None def get_npoints(self, answer_id): - """ Return the number of points for an answer: - If the answer is correct, return the assigned - number of points (default: 1 point) - Otherwise, return 0 points """ + """Return the number of points for an answer, used for partial credit.""" npoints = self.get_property(answer_id, 'npoints') if npoints is not None: return npoints From 352126b4173ea51411a4bcf13cccc51d3dde7d8a Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 8 Mar 2013 10:08:20 -0500 Subject: [PATCH 07/18] Updated tests for correct_map to match changes made to get_npoints --- common/lib/capa/capa/tests/test_correctmap.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/tests/test_correctmap.py b/common/lib/capa/capa/tests/test_correctmap.py index 23734467bb..270ba4d849 100644 --- a/common/lib/capa/capa/tests/test_correctmap.py +++ b/common/lib/capa/capa/tests/test_correctmap.py @@ -91,12 +91,12 @@ class CorrectMapTest(unittest.TestCase): npoints=0) # Assert that we get the expected points - # If points assigned and correct --> npoints + # If points assigned --> npoints # If no points assigned and correct --> 1 point - # Otherwise --> 0 points + # If no points assigned and incorrect --> 0 points self.assertEqual(self.cmap.get_npoints('1_2_1'), 5) self.assertEqual(self.cmap.get_npoints('2_2_1'), 1) - self.assertEqual(self.cmap.get_npoints('3_2_1'), 0) + self.assertEqual(self.cmap.get_npoints('3_2_1'), 5) self.assertEqual(self.cmap.get_npoints('4_2_1'), 0) self.assertEqual(self.cmap.get_npoints('5_2_1'), 0) From a44c0932d288e3112baf2a35a5400dd03ac60f49 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 8 Mar 2013 10:08:39 -0500 Subject: [PATCH 08/18] Fix https://edx.lighthouseapp.com/projects/101932-lms/tickets/237-Production-500-error-for-undefined-variable-user --- lms/djangoapps/courseware/module_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 23d27c72ac..ec2178f642 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -500,7 +500,7 @@ def modx_dispatch(request, dispatch, location, course_id): if instance is None: # Either permissions just changed, or someone is trying to be clever # and load something they shouldn't have access to. - log.debug("No module {0} for user {1}--access denied?".format(location, user)) + log.debug("No module {0} for user {1}--access denied?".format(location, request.user)) raise Http404 instance_module = get_instance_module(course_id, request.user, instance, student_module_cache) From b9e5ed9525d102304eb72499875596a5bca4234e Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Mon, 4 Mar 2013 15:35:09 -0500 Subject: [PATCH 09/18] Click in the upper left of an element instead of the middle. --- cms/djangoapps/contentstore/features/common.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 61b4fee9f6..3ad037b5a9 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -94,8 +94,11 @@ def assert_css_with_text(css, text): def css_click(css): - assert_true(world.browser.is_element_present_by_css(css, 5)) - world.browser.find_by_css(css).first.click() + ''' + Rather than click in the middle of an element, + click in the upper left + ''' + css_click_at(css) def css_click_at(css, x=10, y=10): From 3cec8f1a76e10c914b0b863fb26e1e53ff7bdd6c Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Mon, 4 Mar 2013 15:50:25 -0500 Subject: [PATCH 10/18] Catch WebDriverException --- .../contentstore/features/advanced-settings.py | 7 ++++++- cms/djangoapps/contentstore/features/common.py | 13 ++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 91daf70718..4ce9421ad3 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -1,6 +1,7 @@ from lettuce import world, step from common import * import time +from selenium.common.exceptions import WebDriverException from nose.tools import assert_equal from nose.tools import assert_true @@ -42,7 +43,11 @@ def edit_the_name_of_a_policy_key(step): @step(u'I press the "([^"]*)" notification button$') def press_the_notification_button(step, name): - world.browser.click_link_by_text(name) + try: + world.browser.click_link_by_text(name) + except WebDriverException, e: + css = 'a.%s-button' % name.lower() + css_click_at(css) @step(u'I edit the value of a policy key$') diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 3ad037b5a9..a8da3dc2eb 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -3,6 +3,7 @@ from lettuce.django import django_url from nose.tools import assert_true from nose.tools import assert_equal from selenium.webdriver.support.ui import WebDriverWait +from selenium.common.exceptions import WebDriverException from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory from terrain.factories import CourseFactory, GroupFactory @@ -95,10 +96,16 @@ def assert_css_with_text(css, text): def css_click(css): ''' - Rather than click in the middle of an element, - click in the upper left + First try to use the regular click method, + but if clicking in the middle of an element + doesn't work it might be that it thinks some other + element is on top of it there so click in the upper left ''' - css_click_at(css) + try: + assert_true(world.browser.is_element_present_by_css(css, 5)) + world.browser.find_by_css(css).first.click() + except WebDriverException, e: + css_click_at(css) def css_click_at(css, x=10, y=10): From 21c15d8f1f480e88eed443f5c0a152158388c1b2 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Mon, 4 Mar 2013 15:35:09 -0500 Subject: [PATCH 11/18] Click in the upper left of an element instead of the middle. --- cms/djangoapps/contentstore/features/common.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index a8da3dc2eb..e374f240b9 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -96,6 +96,7 @@ def assert_css_with_text(css, text): def css_click(css): ''' +<<<<<<< HEAD First try to use the regular click method, but if clicking in the middle of an element doesn't work it might be that it thinks some other @@ -106,6 +107,12 @@ def css_click(css): world.browser.find_by_css(css).first.click() except WebDriverException, e: css_click_at(css) +======= + Rather than click in the middle of an element, + click in the upper left + ''' + css_click_at(css) +>>>>>>> Click in the upper left of an element instead of the middle. def css_click_at(css, x=10, y=10): From e84fa6382661b0eb71754425bd2e747386212abf Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Mon, 4 Mar 2013 15:50:25 -0500 Subject: [PATCH 12/18] Catch WebDriverException --- cms/djangoapps/contentstore/features/common.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index e374f240b9..b5c42436e2 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -97,10 +97,14 @@ def assert_css_with_text(css, text): def css_click(css): ''' <<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> Catch WebDriverException First try to use the regular click method, but if clicking in the middle of an element doesn't work it might be that it thinks some other element is on top of it there so click in the upper left +<<<<<<< HEAD ''' try: assert_true(world.browser.is_element_present_by_css(css, 5)) @@ -113,6 +117,14 @@ def css_click(css): ''' css_click_at(css) >>>>>>> Click in the upper left of an element instead of the middle. +======= + ''' + try: + assert_true(world.browser.is_element_present_by_css(css, 5)) + world.browser.find_by_css(css).first.click() + except WebDriverException, e: + css_click_at(css) +>>>>>>> Catch WebDriverException def css_click_at(css, x=10, y=10): From ad5d2a9624a954efc6fe85d7fdd15ff1e4bd0aa0 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Tue, 5 Mar 2013 14:32:06 -0500 Subject: [PATCH 13/18] Update update_templates reference --- cms/djangoapps/contentstore/features/common.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index b5c42436e2..b6e2b2b429 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -7,7 +7,8 @@ from selenium.common.exceptions import WebDriverException from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory from terrain.factories import CourseFactory, GroupFactory -import xmodule.modulestore.django +from xmodule.modulestore.django import _MODULESTORES, modulestore +from xmodule.templates import update_templates from auth.authz import get_user_by_email from logging import getLogger @@ -84,9 +85,9 @@ def flush_xmodule_store(): # (though it shouldn't), do this manually # from the bash shell to drop it: # $ mongo test_xmodule --eval "db.dropDatabase()" - xmodule.modulestore.django._MODULESTORES = {} - xmodule.modulestore.django.modulestore().collection.drop() - xmodule.templates.update_templates() + _MODULESTORES = {} + modulestore().collection.drop() + update_templates() def assert_css_with_text(css, text): From b66d0cf5da7f7da951de85f841678d51d09c5d79 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Tue, 5 Mar 2013 17:15:17 -0500 Subject: [PATCH 14/18] Fix signup test on ubuntu --- cms/djangoapps/contentstore/features/signup.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index a786225ead..e8d0dd8229 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -1,4 +1,5 @@ from lettuce import world, step +from common import * @step('I fill in the registration form$') @@ -13,10 +14,11 @@ def i_fill_in_the_registration_form(step): @step('I press the Create My Account button on the registration form$') def i_press_the_button_on_the_registration_form(step): - register_form = world.browser.find_by_css('form#register_form') - submit_css = 'button#submit' - register_form.find_by_css(submit_css).click() - + submit_css = 'form#register_form button#submit' + # Workaround for click not working on ubuntu + # for some unknown reason. + e = css_find(submit_css) + e.type(' ') @step('I should see be on the studio home page$') def i_should_see_be_on_the_studio_home_page(step): From 21e7bc5128b42c54b204cf7558aff65889850d11 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Thu, 7 Mar 2013 11:01:15 -0500 Subject: [PATCH 15/18] More robust handling for finding and clicking on objects by css. Tag tests not working under PhantomJS to skip for now. --- .gitignore | 3 +- .../features/advanced-settings.feature | 3 + .../features/advanced-settings.py | 59 +++++++++++-------- .../contentstore/features/common.py | 41 ++++--------- .../contentstore/features/section.feature | 1 + .../studio-overview-togglesection.feature | 1 + .../contentstore/features/subsection.feature | 1 + common/djangoapps/terrain/browser.py | 3 +- 8 files changed, 56 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index 493df5a7fd..b13a128a63 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ nosetests.xml cover_html/ .idea/ .redcar/ -chromedriver.log \ No newline at end of file +chromedriver.log +ghostdriver.log diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index 4708a60be1..779d44e4b2 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -7,6 +7,7 @@ Feature: Advanced (manual) course policy When I select the Advanced Settings Then I see only the display name + @skip-phantom Scenario: Test if there are no policy settings without existing UI controls Given I am on the Advanced Course Settings page in Studio When I delete the display name @@ -14,6 +15,7 @@ Feature: Advanced (manual) course policy And I reload the page Then there are no advanced policy settings + @skip-phantom Scenario: Test cancel editing key name Given I am on the Advanced Course Settings page in Studio When I edit the name of a policy key @@ -32,6 +34,7 @@ Feature: Advanced (manual) course policy And I press the "Cancel" notification button Then the policy key value is unchanged + @skip-phantom Scenario: Test editing key value Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 4ce9421ad3..dbbe769b8e 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -2,9 +2,9 @@ from lettuce import world, step from common import * import time from selenium.common.exceptions import WebDriverException +from selenium.webdriver.support import expected_conditions as EC -from nose.tools import assert_equal -from nose.tools import assert_true +from nose.tools import assert_true, assert_false, assert_equal """ http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html @@ -20,6 +20,7 @@ def i_select_advanced_settings(step): css_click(expand_icon_css) link_css = 'li.nav-course-settings-advanced a' css_click(link_css) + # world.browser.click_link_by_text('Advanced Settings') @step('I am on the Advanced Course Settings page in Studio$') @@ -43,12 +44,20 @@ def edit_the_name_of_a_policy_key(step): @step(u'I press the "([^"]*)" notification button$') def press_the_notification_button(step, name): - try: - world.browser.click_link_by_text(name) - except WebDriverException, e: - css = 'a.%s-button' % name.lower() - css_click_at(css) + def is_visible(driver): + return EC.visibility_of_element_located((By.CSS_SELECTOR,css,)) + def is_invisible(driver): + return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,)) + css = 'a.%s-button' % name.lower() + wait_for(is_visible) + + try: + css_click_at(css) + wait_for(is_invisible) + except WebDriverException, e: + css_click_at(css) + wait_for(is_invisible) @step(u'I edit the value of a policy key$') def edit_the_value_of_a_policy_key(step): @@ -104,29 +113,29 @@ def it_is_formatted(step): @step(u'the policy key name is unchanged$') def the_policy_key_name_is_unchanged(step): policy_key_css = 'input.policy-key' - e = css_find(policy_key_css).first - assert_equal(e.value, 'display_name') + val = css_find(policy_key_css).first.value + assert_equal(val, 'display_name') @step(u'the policy key name is changed$') def the_policy_key_name_is_changed(step): policy_key_css = 'input.policy-key' - e = css_find(policy_key_css).first - assert_equal(e.value, 'new') + val = css_find(policy_key_css).first.value + assert_equal(val, 'new') @step(u'the policy key value is unchanged$') def the_policy_key_value_is_unchanged(step): policy_value_css = 'li.course-advanced-policy-list-item div.value textarea' - e = css_find(policy_value_css).first - assert_equal(e.value, '"Robot Super Course"') + val = css_find(policy_value_css).first.value + assert_equal(val, '"Robot Super Course"') @step(u'the policy key value is changed$') def the_policy_key_value_is_unchanged(step): policy_value_css = 'li.course-advanced-policy-list-item div.value textarea' - e = css_find(policy_value_css).first - assert_equal(e.value, '"Robot Super Course X"') + val = css_find(policy_value_css).first.value + assert_equal(val, '"Robot Super Course X"') ############# HELPERS ############### @@ -149,7 +158,7 @@ def delete_entry(index): """ Delete the nth entry where index is 0-based """ - css = '.delete-button' + css = 'a.delete-button' assert_true(world.browser.is_element_present_by_css(css, 5)) delete_buttons = css_find(css) assert_true(len(delete_buttons) > index, "no delete button exists for entry " + str(index)) @@ -170,16 +179,16 @@ def assert_entries(css, expected_values): def click_save(): - css = ".save-button" + css = "a.save-button" - def is_shown(driver): - visible = css_find(css).first.visible - if visible: - # Even when waiting for visible, this fails sporadically. Adding in a small wait. - time.sleep(float(1)) - return visible - wait_for(is_shown) - css_click(css) + # def is_shown(driver): + # visible = css_find(css).first.visible + # if visible: + # # Even when waiting for visible, this fails sporadically. Adding in a small wait. + # time.sleep(float(1)) + # return visible + # wait_for(is_shown) + css_click_at(css) def fill_last_field(value): diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index b6e2b2b429..f7e76ecf7f 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -3,7 +3,9 @@ from lettuce.django import django_url from nose.tools import assert_true from nose.tools import assert_equal from selenium.webdriver.support.ui import WebDriverWait -from selenium.common.exceptions import WebDriverException +from selenium.common.exceptions import WebDriverException, StaleElementReferenceException +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.by import By from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory from terrain.factories import CourseFactory, GroupFactory @@ -15,8 +17,6 @@ from logging import getLogger logger = getLogger(__name__) ########### STEP HELPERS ############## - - @step('I (?:visit|access|open) the Studio homepage$') def i_visit_the_studio_homepage(step): # To make this go to port 8001, put @@ -54,9 +54,8 @@ def i_have_opened_a_new_course(step): log_into_studio() create_a_course() + ####### HELPER FUNCTIONS ############## - - def create_studio_user( uname='robot', email='robot+studio@edx.org', @@ -97,35 +96,15 @@ def assert_css_with_text(css, text): def css_click(css): ''' -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> Catch WebDriverException First try to use the regular click method, but if clicking in the middle of an element doesn't work it might be that it thinks some other element is on top of it there so click in the upper left -<<<<<<< HEAD ''' try: - assert_true(world.browser.is_element_present_by_css(css, 5)) - world.browser.find_by_css(css).first.click() + css_find(css).first.click() except WebDriverException, e: css_click_at(css) -======= - Rather than click in the middle of an element, - click in the upper left - ''' - css_click_at(css) ->>>>>>> Click in the upper left of an element instead of the middle. -======= - ''' - try: - assert_true(world.browser.is_element_present_by_css(css, 5)) - world.browser.find_by_css(css).first.click() - except WebDriverException, e: - css_click_at(css) ->>>>>>> Catch WebDriverException def css_click_at(css, x=10, y=10): @@ -133,8 +112,7 @@ def css_click_at(css, x=10, y=10): A method to click at x,y coordinates of the element rather than in the center of the element ''' - assert_true(world.browser.is_element_present_by_css(css, 5)) - e = world.browser.find_by_css(css).first + e = css_find(css).first e.action_chains.move_to_element_with_offset(e._element, x, y) e.action_chains.click() e.action_chains.perform() @@ -145,11 +123,16 @@ def css_fill(css, value): def css_find(css): + def is_visible(driver): + return EC.visibility_of_element_located((By.CSS_SELECTOR,css,)) + + assert_true(world.browser.is_element_present_by_css(css, 5)) + wait_for(is_visible) return world.browser.find_by_css(css) def wait_for(func): - WebDriverWait(world.browser.driver, 10).until(func) + WebDriverWait(world.browser.driver, 5).until(func) def id_find(id): diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature index 75e7a4af10..93ff1ca247 100644 --- a/cms/djangoapps/contentstore/features/section.feature +++ b/cms/djangoapps/contentstore/features/section.feature @@ -26,6 +26,7 @@ Feature: Create Section And I save a new section release date Then the section release date is updated + @skip-phantom Scenario: Delete section Given I have opened a new course in Studio And I have added a new section diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature index 5276b90d12..52c10e41a8 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature @@ -21,6 +21,7 @@ Feature: Overview Toggle Section Then I see the "Collapse All Sections" link And all sections are expanded + @skip-phantom Scenario: Collapse link is not removed after last section of a course is deleted Given I have a course with 1 section And I navigate to the course overview page diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature index 4b5f5b869d..1be5f4aeb9 100644 --- a/cms/djangoapps/contentstore/features/subsection.feature +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -17,6 +17,7 @@ Feature: Create Subsection And I click to edit the subsection name Then I see the complete subsection name with a quote in the editor + @skip-phantom Scenario: Delete a subsection Given I have opened a new course section in Studio And I have added a new subsection diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 8c2a8ba7a5..ddd39196de 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -12,7 +12,8 @@ from django.core.management import call_command @before.harvest def initial_setup(server): # Launch the browser app (choose one of these below) - world.browser = Browser('chrome') + # world.browser = Browser('chrome') + world.browser = Browser('phantomjs') # world.browser = Browser('firefox') From 1f30baabfd1f45e2bfde56ee35e2b4f281a1aa85 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Thu, 7 Mar 2013 11:23:58 -0500 Subject: [PATCH 16/18] Change default browser for lettuce back to chrome. --- common/djangoapps/terrain/browser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index ddd39196de..0881d86124 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -12,8 +12,8 @@ from django.core.management import call_command @before.harvest def initial_setup(server): # Launch the browser app (choose one of these below) - # world.browser = Browser('chrome') - world.browser = Browser('phantomjs') + world.browser = Browser('chrome') + # world.browser = Browser('phantomjs') # world.browser = Browser('firefox') From 24d088933e022a101f6a3ed5bfa7f8280d2996f5 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Thu, 7 Mar 2013 11:37:34 -0500 Subject: [PATCH 17/18] Fix intermittent lettuce test failure on ubuntu. --- cms/djangoapps/contentstore/features/advanced-settings.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index dbbe769b8e..ec58aeccc6 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -146,12 +146,16 @@ def create_entry(key, value): new_key_css = 'div#__new_advanced_key__ input' new_key_element = css_find(new_key_css).first new_key_element.fill(key) -# For some reason have to get the instance for each command (get error that it is no longer attached to the DOM) -# Have to do all this because Selenium has a bug that fill does not remove existing text +# For some reason have to get the instance for each command +# (get error that it is no longer attached to the DOM) +# Have to do all this because Selenium fill does not remove existing text new_value_css = 'div.CodeMirror textarea' css_find(new_value_css).last.fill("") css_find(new_value_css).last._element.send_keys(Keys.DELETE, Keys.DELETE) css_find(new_value_css).last.fill(value) + # Add in a TAB key press because intermittently on ubuntu the + # last character of "value" above was not getting typed in + css_find(new_value_css).last._element.send_keys(Keys.TAB) def delete_entry(index): From 55eefd0ef6ba8230ad2e5d8944c28238fbc93ce8 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Fri, 8 Mar 2013 11:11:32 -0500 Subject: [PATCH 18/18] Fix test that sets release date to work on all platforms --- .../contentstore/features/section.feature | 2 +- cms/djangoapps/contentstore/features/section.py | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature index 93ff1ca247..08d38367bc 100644 --- a/cms/djangoapps/contentstore/features/section.feature +++ b/cms/djangoapps/contentstore/features/section.feature @@ -32,4 +32,4 @@ Feature: Create Section And I have added a new section When I press the "section" delete icon And I confirm the alert - Then the section does not exist \ No newline at end of file + Then the section does not exist diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index cfa4e4bb52..b5ddb48a09 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -1,6 +1,8 @@ from lettuce import world, step from common import * from nose.tools import assert_equal +from selenium.webdriver.common.keys import Keys +import time ############### ACTIONS #################### @@ -37,10 +39,14 @@ def i_save_a_new_section_release_date(step): date_css = 'input.start-date.date.hasDatepicker' time_css = 'input.start-time.time.ui-timepicker-input' css_fill(date_css, '12/25/2013') - # click here to make the calendar go away - css_click(time_css) + # hit TAB to get to the time field + e = css_find(date_css).first + e._element.send_keys(Keys.TAB) css_fill(time_css, '12:00am') - css_click('a.save-button') + e = css_find(time_css).first + e._element.send_keys(Keys.TAB) + time.sleep(float(1)) + world.browser.click_link_by_text('Save') ############ ASSERTIONS ################### @@ -106,7 +112,7 @@ def the_section_release_date_picker_not_visible(step): def the_section_release_date_is_updated(step): css = 'span.published-status' status_text = world.browser.find_by_css(css).text - assert status_text == 'Will Release: 12/25/2013 at 12:00am' + assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am') ############ HELPER METHODS ################### @@ -120,4 +126,4 @@ def save_section_name(name): def see_my_section_on_the_courseware_page(name): section_css = 'span.section-name-span' - assert_css_with_text(section_css, name) \ No newline at end of file + assert_css_with_text(section_css, name)