Merge pull request #1370 from edx/zoldak/refactor-cms-acceptance-js
refactor studio component creation in acceptance tests
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
# pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true, assert_equal, assert_in, assert_false # pylint: disable=E0611
|
||||
from nose.tools import assert_true, assert_in, assert_false # pylint: disable=E0611
|
||||
|
||||
from auth.authz import get_user_by_email, get_course_groupname_for_role
|
||||
from django.conf import settings
|
||||
@@ -224,14 +224,50 @@ def i_enabled_the_advanced_module(step, module):
|
||||
press_the_notification_button(step, 'Save')
|
||||
|
||||
|
||||
@step('I have clicked the new unit button')
|
||||
def open_new_unit(step):
|
||||
step.given('I have opened a new course section in Studio')
|
||||
step.given('I have added a new subsection')
|
||||
step.given('I expand the first section')
|
||||
old_url = world.browser.url
|
||||
world.css_click('a.new-unit-item')
|
||||
world.wait_for(lambda x: world.browser.url != old_url)
|
||||
@world.absorb
|
||||
def create_course_with_unit():
|
||||
"""
|
||||
Prepare for tests by creating a course with a section, subsection, and unit.
|
||||
Performs the following:
|
||||
Clear out all courseware
|
||||
Create a course with a section, subsection, and unit
|
||||
Create a user and make that user a course author
|
||||
Log the user into studio
|
||||
Open the course from the dashboard
|
||||
Expand the section and click on the New Unit link
|
||||
The end result is the page where the user is editing the new unit
|
||||
"""
|
||||
world.clear_courses()
|
||||
course = world.CourseFactory.create()
|
||||
world.scenario_dict['COURSE'] = course
|
||||
section = world.ItemFactory.create(parent_location=course.location)
|
||||
world.ItemFactory.create(
|
||||
parent_location=section.location,
|
||||
category='sequential',
|
||||
display_name='Subsection One',
|
||||
)
|
||||
user = create_studio_user(is_staff=False)
|
||||
add_course_author(user, course)
|
||||
|
||||
log_into_studio()
|
||||
world.css_click('a.course-link')
|
||||
|
||||
css_selectors = [
|
||||
'div.section-item a.expand-collapse-icon', 'a.new-unit-item'
|
||||
]
|
||||
for selector in css_selectors:
|
||||
world.css_click(selector)
|
||||
|
||||
world.wait_for_mathjax()
|
||||
world.wait_for_xmodule()
|
||||
|
||||
assert world.is_css_present('ul.new-component-type')
|
||||
|
||||
|
||||
@step('I have clicked the new unit button$')
|
||||
@step(u'I am in Studio editing a new unit$')
|
||||
def edit_new_unit(step):
|
||||
create_course_with_unit()
|
||||
|
||||
|
||||
@step('the save notification button is disabled')
|
||||
@@ -267,9 +303,9 @@ def confirm_the_prompt(step):
|
||||
assert_false(world.css_find(btn_css).visible)
|
||||
|
||||
|
||||
@step(u'I am shown a (.*)$')
|
||||
def i_am_shown_a_notification(step, notification_type):
|
||||
assert world.is_css_present('.wrapper-%s' % notification_type)
|
||||
@step(u'I am shown a prompt$')
|
||||
def i_am_shown_a_notification(step):
|
||||
assert world.is_css_present('.wrapper-prompt')
|
||||
|
||||
|
||||
def type_in_codemirror(index, text):
|
||||
|
||||
@@ -80,9 +80,3 @@ Feature: CMS.Component Adding
|
||||
And I add a "Blank Advanced Problem" "Advanced Problem" component
|
||||
And I delete all components
|
||||
Then I see no components
|
||||
|
||||
Scenario: I see a notification on save
|
||||
Given I am in Studio editing a new unit
|
||||
And I add a "Discussion" "single step" component
|
||||
And I edit and save a component
|
||||
Then I am shown a notification
|
||||
|
||||
@@ -2,52 +2,19 @@
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true, assert_in, assert_equal # pylint: disable=E0611
|
||||
from common import create_studio_user, add_course_author, log_into_studio
|
||||
|
||||
|
||||
@step(u'I am in Studio editing a new unit$')
|
||||
def add_unit(step):
|
||||
world.clear_courses()
|
||||
course = world.CourseFactory.create()
|
||||
section = world.ItemFactory.create(parent_location=course.location)
|
||||
world.ItemFactory.create(
|
||||
parent_location=section.location,
|
||||
category='sequential',
|
||||
display_name='Subsection One',)
|
||||
user = create_studio_user(is_staff=False)
|
||||
add_course_author(user, course)
|
||||
log_into_studio()
|
||||
world.wait_for_requirejs([
|
||||
"jquery", "gettext", "js/models/course", "coffee/src/models/module",
|
||||
"coffee/src/views/unit", "jquery.ui",
|
||||
])
|
||||
world.wait_for_mathjax()
|
||||
css_selectors = [
|
||||
'a.course-link', 'div.section-item a.expand-collapse-icon',
|
||||
'a.new-unit-item',
|
||||
]
|
||||
for selector in css_selectors:
|
||||
world.css_click(selector)
|
||||
from nose.tools import assert_true, assert_in # pylint: disable=E0611
|
||||
|
||||
|
||||
@step(u'I add this type of single step component:$')
|
||||
def add_a_single_step_component(step):
|
||||
world.wait_for_xmodule()
|
||||
for step_hash in step.hashes:
|
||||
component = step_hash['Component']
|
||||
assert_in(component, ['Discussion', 'Video'])
|
||||
css_selector = 'a[data-type="{}"]'.format(component.lower())
|
||||
world.css_click(css_selector)
|
||||
|
||||
# In the current implementation, all the "new component"
|
||||
# buttons are handled by one BackBone.js view.
|
||||
# If we click two buttons at super-human speed,
|
||||
# the view will miss the second click while it's
|
||||
# processing the first.
|
||||
# To account for this, we wait for each component
|
||||
# to be created before clicking the next component.
|
||||
world.wait_for_visible('section.xmodule_{}Module'.format(component))
|
||||
world.create_component_instance(
|
||||
step=step,
|
||||
category='{}'.format(component.lower()),
|
||||
)
|
||||
|
||||
|
||||
@step(u'I see this type of single step component:$')
|
||||
@@ -62,45 +29,13 @@ def see_a_single_step_component(step):
|
||||
|
||||
@step(u'I add this type of( Advanced)? (HTML|Problem) component:$')
|
||||
def add_a_multi_step_component(step, is_advanced, category):
|
||||
def click_advanced():
|
||||
css = 'ul.problem-type-tabs a[href="#tab2"]'
|
||||
world.css_click(css)
|
||||
my_css = 'ul.problem-type-tabs li.ui-state-active a[href="#tab2"]'
|
||||
assert(world.css_find(my_css))
|
||||
|
||||
def find_matching_link():
|
||||
"""
|
||||
Find the link with the specified text. There should be one and only one.
|
||||
"""
|
||||
# The tab shows links for the given category
|
||||
links = world.css_find('div.new-component-{} a'.format(category))
|
||||
|
||||
# Find the link whose text matches what you're looking for
|
||||
matched_links = [link for link in links if link.text == step_hash['Component']]
|
||||
|
||||
# There should be one and only one
|
||||
assert_equal(len(matched_links), 1)
|
||||
return matched_links[0]
|
||||
|
||||
def click_link():
|
||||
link.click()
|
||||
|
||||
world.wait_for_xmodule()
|
||||
category = category.lower()
|
||||
for step_hash in step.hashes:
|
||||
css_selector = 'a[data-type="{}"]'.format(category)
|
||||
world.css_click(css_selector)
|
||||
world.wait_for_invisible(css_selector)
|
||||
|
||||
if is_advanced:
|
||||
# Sometimes this click does not work if you go too fast.
|
||||
world.retry_on_exception(click_advanced, max_attempts=5, ignored_exceptions=AssertionError)
|
||||
|
||||
# Retry this in case the list is empty because you tried too fast.
|
||||
link = world.retry_on_exception(func=find_matching_link, ignored_exceptions=AssertionError)
|
||||
|
||||
# Wait for the link to be clickable. If you go too fast it is not.
|
||||
world.retry_on_exception(click_link)
|
||||
world.create_component_instance(
|
||||
step=step,
|
||||
category='{}'.format(category.lower()),
|
||||
component_type=step_hash['Component'],
|
||||
is_advanced=bool(is_advanced),
|
||||
)
|
||||
|
||||
|
||||
@step(u'I see (HTML|Problem) components in this order:')
|
||||
|
||||
@@ -2,30 +2,35 @@
|
||||
#pylint: disable=C0111
|
||||
|
||||
from lettuce import world
|
||||
from nose.tools import assert_equal, assert_true # pylint: disable=E0611
|
||||
from nose.tools import assert_equal, assert_true, assert_in # pylint: disable=E0611
|
||||
from terrain.steps import reload_the_page
|
||||
|
||||
|
||||
@world.absorb
|
||||
def create_component_instance(step, component_button_css, category,
|
||||
expected_css, boilerplate=None,
|
||||
has_multiple_templates=True):
|
||||
def create_component_instance(step, category, component_type=None, is_advanced=False):
|
||||
"""
|
||||
Create a new component in a Unit.
|
||||
|
||||
click_new_component_button(step, component_button_css)
|
||||
Parameters
|
||||
----------
|
||||
category: component type (discussion, html, problem, video)
|
||||
component_type: for components with multiple templates, the link text in the menu
|
||||
is_advanced: for html and problem, is the desired component under the
|
||||
advanced menu
|
||||
"""
|
||||
assert_in(category, ['problem', 'html', 'video', 'discussion'])
|
||||
|
||||
component_button_css = '.large-{}-icon'.format(category.lower())
|
||||
world.css_click(component_button_css)
|
||||
|
||||
if category in ('problem', 'html'):
|
||||
world.wait_for_invisible(component_button_css)
|
||||
click_component_from_menu(category, component_type, is_advanced)
|
||||
|
||||
def animation_done(_driver):
|
||||
script = "$('div.new-component').css('display')"
|
||||
return world.browser.evaluate_script(script) == 'none'
|
||||
|
||||
world.wait_for(animation_done)
|
||||
|
||||
if has_multiple_templates:
|
||||
click_component_from_menu(category, boilerplate, expected_css)
|
||||
|
||||
if category in ('video',):
|
||||
world.wait_for_xmodule()
|
||||
if category == 'problem':
|
||||
expected_css = 'section.xmodule_CapaModule'
|
||||
else:
|
||||
expected_css = 'section.xmodule_{}Module'.format(category.title())
|
||||
|
||||
assert_true(world.is_css_present(expected_css))
|
||||
|
||||
@@ -33,29 +38,53 @@ def create_component_instance(step, component_button_css, category,
|
||||
@world.absorb
|
||||
def click_new_component_button(step, component_button_css):
|
||||
step.given('I have clicked the new unit button')
|
||||
world.wait_for_requirejs(
|
||||
["jquery", "js/models/course", "coffee/src/models/module",
|
||||
"coffee/src/views/unit", "jquery.ui", "domReady!"]
|
||||
)
|
||||
world.css_click(component_button_css)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def click_component_from_menu(category, boilerplate, expected_css):
|
||||
def _click_advanced():
|
||||
css = 'ul.problem-type-tabs a[href="#tab2"]'
|
||||
world.css_click(css)
|
||||
my_css = 'ul.problem-type-tabs li.ui-state-active a[href="#tab2"]'
|
||||
assert(world.css_find(my_css))
|
||||
|
||||
|
||||
def _find_matching_link(category, component_type):
|
||||
"""
|
||||
Creates a component from `instance_id`. For components with more
|
||||
than one template, clicks on `elem_css` to create the new
|
||||
component. Components with only one template are created as soon
|
||||
as the user clicks the appropriate button, so we assert that the
|
||||
expected component is present.
|
||||
Find the link with the specified text. There should be one and only one.
|
||||
"""
|
||||
if boilerplate:
|
||||
elem_css = "a[data-category='{}'][data-boilerplate='{}']".format(category, boilerplate)
|
||||
else:
|
||||
elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category)
|
||||
elements = world.css_find(elem_css)
|
||||
assert_equal(len(elements), 1)
|
||||
world.css_click(elem_css)
|
||||
|
||||
# The tab shows links for the given category
|
||||
links = world.css_find('div.new-component-{} a'.format(category))
|
||||
|
||||
# Find the link whose text matches what you're looking for
|
||||
matched_links = [link for link in links if link.text == component_type]
|
||||
|
||||
# There should be one and only one
|
||||
assert_equal(len(matched_links), 1)
|
||||
return matched_links[0]
|
||||
|
||||
|
||||
def click_component_from_menu(category, component_type, is_advanced):
|
||||
"""
|
||||
Creates a component for a category with more
|
||||
than one template, i.e. HTML and Problem.
|
||||
For some problem types, it is necessary to click to
|
||||
the Advanced tab.
|
||||
The component_type is the link text, e.g. "Blank Common Problem"
|
||||
"""
|
||||
if is_advanced:
|
||||
# Sometimes this click does not work if you go too fast.
|
||||
world.retry_on_exception(_click_advanced,
|
||||
ignored_exceptions=AssertionError)
|
||||
|
||||
# Retry this in case the list is empty because you tried too fast.
|
||||
link = world.retry_on_exception(
|
||||
lambda: _find_matching_link(category, component_type),
|
||||
ignored_exceptions=AssertionError
|
||||
)
|
||||
|
||||
# Wait for the link to be clickable. If you go too fast it is not.
|
||||
world.retry_on_exception(lambda: link.click())
|
||||
|
||||
|
||||
@world.absorb
|
||||
|
||||
@@ -58,20 +58,3 @@ Feature: CMS.Course Overview
|
||||
And I click the "Expand All Sections" link
|
||||
Then I see the "Collapse All Sections" link
|
||||
And all sections are expanded
|
||||
|
||||
Scenario: Notification is shown on grading status changes
|
||||
Given I have a course with 1 section
|
||||
When I navigate to the course overview page
|
||||
And I change an assignment's grading status
|
||||
Then I am shown a notification
|
||||
|
||||
# Notification is not shown on reorder for IE
|
||||
# Safari does not have moveMouseTo implemented
|
||||
@skip_internetexplorer
|
||||
@skip_safari
|
||||
Scenario: Notification is shown on subsection reorder
|
||||
Given I have opened a new course section in Studio
|
||||
And I have added a new subsection
|
||||
And I have added a new subsection
|
||||
When I reorder subsections
|
||||
Then I am shown a notification
|
||||
|
||||
@@ -50,8 +50,8 @@ def other_delete_self(_step):
|
||||
|
||||
@step(u'I make "([^"]*)" a course team admin')
|
||||
def make_course_team_admin(_step, name):
|
||||
admin_btn_css = '.user-item[data-email="{email}"] .user-actions .add-admin-role'.format(
|
||||
email=name+'@edx.org')
|
||||
admin_btn_css = '.user-item[data-email="{name}@edx.org"] .user-actions .add-admin-role'.format(
|
||||
name=name)
|
||||
world.css_click(admin_btn_css)
|
||||
|
||||
|
||||
@@ -80,8 +80,8 @@ def see_course(_step, do_not_see, gender='self'):
|
||||
|
||||
@step(u'"([^"]*)" should( not)? be marked as an admin')
|
||||
def marked_as_admin(_step, name, not_marked_admin):
|
||||
flag_css = '.user-item[data-email="{email}"] .flag-role.flag-role-admin'.format(
|
||||
email=name+'@edx.org')
|
||||
flag_css = '.user-item[data-email="{name}@edx.org"] .flag-role.flag-role-admin'.format(
|
||||
name=name)
|
||||
if not_marked_admin:
|
||||
assert world.is_css_not_present(flag_css)
|
||||
else:
|
||||
|
||||
@@ -2,6 +2,7 @@ import os
|
||||
from lettuce import world
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def import_file(filename):
|
||||
world.browser.execute_script("$('input.file-input').css('display', 'block')")
|
||||
path = os.path.join(settings.COMMON_TEST_DATA_ROOT, "imports", filename)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Feature: CMS.Discussion Component Editor
|
||||
As a course author, I want to be able to create discussion components.
|
||||
|
||||
Scenario: User can view metadata
|
||||
Scenario: User can view discussion component metadata
|
||||
Given I have created a Discussion Tag
|
||||
And I edit and select Settings
|
||||
Then I see three alphabetized settings and their expected values
|
||||
@@ -14,7 +14,3 @@ Feature: CMS.Discussion Component Editor
|
||||
And I edit and select Settings
|
||||
Then I can modify the display name
|
||||
And my display name change is persisted on save
|
||||
|
||||
Scenario: Creating a discussion takes a single click
|
||||
Given I have clicked the new unit button
|
||||
Then creating a discussion takes a single click
|
||||
|
||||
@@ -6,11 +6,10 @@ from lettuce import world, step
|
||||
|
||||
@step('I have created a Discussion Tag$')
|
||||
def i_created_discussion_tag(step):
|
||||
world.create_course_with_unit()
|
||||
world.create_component_instance(
|
||||
step, '.large-discussion-icon',
|
||||
'discussion',
|
||||
'.xmodule_DiscussionModule',
|
||||
has_multiple_templates=False
|
||||
step=step,
|
||||
category='discussion',
|
||||
)
|
||||
|
||||
|
||||
@@ -22,12 +21,3 @@ def i_see_only_the_settings_and_values(step):
|
||||
['Display Name', "Discussion", False],
|
||||
['Subcategory', "Topic-Level Student-Visible Label", False]
|
||||
])
|
||||
|
||||
|
||||
@step('creating a discussion takes a single click')
|
||||
def discussion_takes_a_single_click(step):
|
||||
component_css = '.xmodule_DiscussionModule'
|
||||
assert world.is_css_not_present(component_css)
|
||||
|
||||
world.css_click("a[data-category='discussion']")
|
||||
assert world.is_css_present(component_css)
|
||||
|
||||
@@ -180,7 +180,7 @@ def cannot_edit_fail(_step):
|
||||
def i_change_grace_period(_step, grace_period):
|
||||
grace_period_css = '#course-grading-graceperiod'
|
||||
ele = world.css_find(grace_period_css).first
|
||||
|
||||
|
||||
# Sometimes it takes a moment for the JavaScript
|
||||
# to populate the field. If we don't wait for
|
||||
# this to happen, then we can end up with
|
||||
|
||||
@@ -6,9 +6,11 @@ from lettuce import world, step
|
||||
|
||||
@step('I have created a Blank HTML Page$')
|
||||
def i_created_blank_html_page(step):
|
||||
world.create_course_with_unit()
|
||||
world.create_component_instance(
|
||||
step, '.large-html-icon', 'html',
|
||||
'.xmodule_HtmlModule'
|
||||
step=step,
|
||||
category='html',
|
||||
component_type='Text'
|
||||
)
|
||||
|
||||
|
||||
@@ -18,11 +20,10 @@ def i_see_only_the_html_display_name(step):
|
||||
|
||||
|
||||
@step('I have created an E-text Written in LaTeX$')
|
||||
def i_created_blank_html_page(step):
|
||||
def i_created_etext_in_latex(step):
|
||||
world.create_course_with_unit()
|
||||
world.create_component_instance(
|
||||
step,
|
||||
'.large-html-icon',
|
||||
'html',
|
||||
'.xmodule_HtmlModule',
|
||||
'latex_html.yaml'
|
||||
step=step,
|
||||
category='html',
|
||||
component_type='E-text Written in LaTeX'
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# disable missing docstring
|
||||
#pylint: disable=C0111
|
||||
|
||||
import os
|
||||
import json
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_equal, assert_true # pylint: disable=E0611
|
||||
@@ -18,12 +17,11 @@ SHOW_ANSWER = "Show Answer"
|
||||
|
||||
@step('I have created a Blank Common Problem$')
|
||||
def i_created_blank_common_problem(step):
|
||||
world.create_course_with_unit()
|
||||
world.create_component_instance(
|
||||
step,
|
||||
'.large-problem-icon',
|
||||
'problem',
|
||||
'.xmodule_CapaModule',
|
||||
'blank_common.yaml'
|
||||
step=step,
|
||||
category='problem',
|
||||
component_type='Blank Common Problem'
|
||||
)
|
||||
|
||||
|
||||
@@ -168,14 +166,13 @@ def cancel_does_not_save_changes(step):
|
||||
|
||||
@step('I have created a LaTeX Problem')
|
||||
def create_latex_problem(step):
|
||||
world.click_new_component_button(step, '.large-problem-icon')
|
||||
|
||||
def animation_done(_driver):
|
||||
return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none'
|
||||
world.wait_for(animation_done)
|
||||
# Go to advanced tab.
|
||||
world.css_click('#ui-id-2')
|
||||
world.click_component_from_menu("problem", "latex_problem.yaml", '.xmodule_CapaModule')
|
||||
world.create_course_with_unit()
|
||||
world.create_component_instance(
|
||||
step=step,
|
||||
category='problem',
|
||||
component_type='Problem Written in LaTeX',
|
||||
is_advanced=True
|
||||
)
|
||||
|
||||
|
||||
@step('I edit and compile the High Level Source')
|
||||
|
||||
@@ -5,8 +5,6 @@ from lettuce import world, step
|
||||
from common import *
|
||||
from nose.tools import assert_equal # pylint: disable=E0611
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
|
||||
@step('I click the New Section link$')
|
||||
def i_click_new_section_link(_step):
|
||||
@@ -53,9 +51,6 @@ def i_see_a_mini_notification(_step, _type):
|
||||
assert world.is_css_present(saving_css)
|
||||
|
||||
|
||||
############ ASSERTIONS ###################
|
||||
|
||||
|
||||
@step('I see my section on the Courseware page$')
|
||||
def i_see_my_section_on_the_courseware_page(_step):
|
||||
see_my_section_on_the_courseware_page('My Section')
|
||||
@@ -125,8 +120,6 @@ def the_section_release_date_is_updated(_step):
|
||||
assert_equal(status_text, 'Will Release: 12/25/2013 at 00:00 UTC')
|
||||
|
||||
|
||||
############ HELPER METHODS ###################
|
||||
|
||||
def save_section_name(name):
|
||||
name_css = '.new-section-name'
|
||||
save_css = '.new-section-name-save'
|
||||
|
||||
@@ -47,7 +47,7 @@ def name_textbook(_step, name):
|
||||
@step(u'I name the (first|second|third) chapter "([^"]*)"')
|
||||
def name_chapter(_step, ordinal, name):
|
||||
index = ["first", "second", "third"].index(ordinal)
|
||||
input_css = ".textbook .chapter{i} input.chapter-name".format(i=index+1)
|
||||
input_css = ".textbook .chapter{i} input.chapter-name".format(i=index + 1)
|
||||
world.css_fill(input_css, name)
|
||||
if world.is_firefox():
|
||||
world.trigger_event(input_css)
|
||||
@@ -56,7 +56,7 @@ def name_chapter(_step, ordinal, name):
|
||||
@step(u'I type in "([^"]*)" for the (first|second|third) chapter asset')
|
||||
def asset_chapter(_step, name, ordinal):
|
||||
index = ["first", "second", "third"].index(ordinal)
|
||||
input_css = ".textbook .chapter{i} input.chapter-asset-path".format(i=index+1)
|
||||
input_css = ".textbook .chapter{i} input.chapter-asset-path".format(i=index + 1)
|
||||
world.css_fill(input_css, name)
|
||||
if world.is_firefox():
|
||||
world.trigger_event(input_css)
|
||||
@@ -65,7 +65,7 @@ def asset_chapter(_step, name, ordinal):
|
||||
@step(u'I click the Upload Asset link for the (first|second|third) chapter')
|
||||
def click_upload_asset(_step, ordinal):
|
||||
index = ["first", "second", "third"].index(ordinal)
|
||||
button_css = ".textbook .chapter{i} .action-upload".format(i=index+1)
|
||||
button_css = ".textbook .chapter{i} .action-upload".format(i=index + 1)
|
||||
world.css_click(button_css)
|
||||
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ def view_asset(_step, status):
|
||||
# Note that world.visit would trigger a 403 error instead of displaying "Unauthorized"
|
||||
# Instead, we can drop back into the selenium driver get command.
|
||||
world.browser.driver.get(url)
|
||||
assert_equal(world.css_text('body'),expected_text)
|
||||
assert_equal(world.css_text('body'), expected_text)
|
||||
|
||||
|
||||
@step('I see a confirmation that the file was deleted$')
|
||||
|
||||
@@ -12,11 +12,10 @@ BUTTONS = {
|
||||
|
||||
@step('I have created a Video component$')
|
||||
def i_created_a_video_component(step):
|
||||
world.create_course_with_unit()
|
||||
world.create_component_instance(
|
||||
step, '.large-video-icon',
|
||||
'video',
|
||||
'.xmodule_VideoModule',
|
||||
has_multiple_templates=False
|
||||
step=step,
|
||||
category='video',
|
||||
)
|
||||
|
||||
|
||||
@@ -155,4 +154,3 @@ def check_captions_visibility_state(_step, visibility_state, timeout):
|
||||
assert world.css_visible('.subtitles')
|
||||
else:
|
||||
assert not world.css_visible('.subtitles')
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
# Disable the "unused argument" warning because lettuce uses "step"
|
||||
#pylint: disable=W0613
|
||||
|
||||
import re
|
||||
from lettuce import world, step
|
||||
from .course_helpers import *
|
||||
from .ui_helpers import *
|
||||
@@ -23,32 +22,15 @@ logger = getLogger(__name__)
|
||||
|
||||
|
||||
@step(r'I wait (?:for )?"(\d+\.?\d*)" seconds?$')
|
||||
def wait(step, seconds):
|
||||
def wait_for_seconds(step, seconds):
|
||||
world.wait(seconds)
|
||||
|
||||
REQUIREJS_WAIT = {
|
||||
re.compile('settings-details'): [
|
||||
"jquery", "js/models/course",
|
||||
"js/models/settings/course_details", "js/views/settings/main"],
|
||||
re.compile('settings-advanced'): [
|
||||
"jquery", "js/models/course", "js/models/settings/advanced",
|
||||
"js/views/settings/advanced", "codemirror"],
|
||||
re.compile('edit\/.+vertical'): [
|
||||
"jquery", "js/models/course", "coffee/src/models/module",
|
||||
"coffee/src/views/unit", "jquery.ui"],
|
||||
}
|
||||
|
||||
|
||||
@step('I reload the page$')
|
||||
def reload_the_page(step):
|
||||
world.wait_for_ajax_complete()
|
||||
world.browser.reload()
|
||||
requirements = None
|
||||
for test, req in REQUIREJS_WAIT.items():
|
||||
if test.search(world.browser.url):
|
||||
requirements = req
|
||||
break
|
||||
world.wait_for_requirejs(requirements)
|
||||
world.wait_for_js_to_load()
|
||||
|
||||
|
||||
@step('I press the browser back button$')
|
||||
@@ -163,9 +145,9 @@ def should_see_in_the_page(step, doesnt_appear, text):
|
||||
else:
|
||||
multiplier = 1
|
||||
if doesnt_appear:
|
||||
assert world.browser.is_text_not_present(text, wait_time=5*multiplier)
|
||||
assert world.browser.is_text_not_present(text, wait_time=5 * multiplier)
|
||||
else:
|
||||
assert world.browser.is_text_present(text, wait_time=5*multiplier)
|
||||
assert world.browser.is_text_present(text, wait_time=5 * multiplier)
|
||||
|
||||
|
||||
@step('I am logged in$')
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
from lettuce import world
|
||||
import time
|
||||
import json
|
||||
import re
|
||||
import platform
|
||||
from textwrap import dedent
|
||||
from urllib import quote_plus
|
||||
from selenium.common.exceptions import (
|
||||
WebDriverException, TimeoutException, StaleElementReferenceException)
|
||||
WebDriverException, TimeoutException,
|
||||
StaleElementReferenceException)
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
@@ -16,11 +18,50 @@ from lettuce.django import django_url
|
||||
from nose.tools import assert_true # pylint: disable=E0611
|
||||
|
||||
|
||||
REQUIREJS_WAIT = {
|
||||
# Settings - Schedule & Details
|
||||
re.compile('^Schedule & Details Settings \|'): [
|
||||
"jquery", "js/models/course",
|
||||
"js/models/settings/course_details", "js/views/settings/main"],
|
||||
|
||||
# Settings - Advanced Settings
|
||||
re.compile('^Advanced Settings \|'): [
|
||||
"jquery", "js/models/course", "js/models/settings/advanced",
|
||||
"js/views/settings/advanced", "codemirror"],
|
||||
|
||||
# Individual Unit (editing)
|
||||
re.compile('^Individual Unit \|'): [
|
||||
"coffee/src/models/module", "coffee/src/views/unit",
|
||||
"coffee/src/views/module_edit"],
|
||||
|
||||
# Content - Outline
|
||||
# Note that calling your org, course number, or display name, 'course' will mess this up
|
||||
re.compile('^Course Outline \|'): [
|
||||
"js/models/course", "js/models/location", "js/models/section",
|
||||
"js/views/overview", "js/views/section_edit"],
|
||||
|
||||
# Dashboard
|
||||
re.compile('^My Courses \|'): [
|
||||
"js/sock", "gettext", "js/base",
|
||||
"jquery.ui", "coffee/src/main", "underscore"],
|
||||
}
|
||||
|
||||
|
||||
@world.absorb
|
||||
def wait(seconds):
|
||||
time.sleep(float(seconds))
|
||||
|
||||
|
||||
@world.absorb
|
||||
def wait_for_js_to_load():
|
||||
requirements = None
|
||||
for test, req in REQUIREJS_WAIT.items():
|
||||
if test.search(world.browser.title):
|
||||
requirements = req
|
||||
break
|
||||
world.wait_for_requirejs(requirements)
|
||||
|
||||
|
||||
# Selenium's `execute_async_script` function pauses Selenium's execution
|
||||
# until the browser calls a specific Javascript callback; in effect,
|
||||
# Selenium goes to sleep until the JS callback function wakes it back up again.
|
||||
@@ -28,8 +69,6 @@ def wait(seconds):
|
||||
# passed to this callback get returned from the `execute_async_script`
|
||||
# function, which allows the JS to communicate information back to Python.
|
||||
# Ref: https://selenium.googlecode.com/svn/trunk/docs/api/dotnet/html/M_OpenQA_Selenium_IJavaScriptExecutor_ExecuteAsyncScript.htm
|
||||
|
||||
|
||||
@world.absorb
|
||||
def wait_for_js_variable_truthy(variable):
|
||||
"""
|
||||
@@ -37,7 +76,7 @@ def wait_for_js_variable_truthy(variable):
|
||||
environment until the given variable is defined and truthy. This process
|
||||
guards against page reloads, and seamlessly retries on the next page.
|
||||
"""
|
||||
js = """
|
||||
javascript = """
|
||||
var callback = arguments[arguments.length - 1];
|
||||
var unloadHandler = function() {{
|
||||
callback("unload");
|
||||
@@ -56,7 +95,13 @@ def wait_for_js_variable_truthy(variable):
|
||||
}}, 10);
|
||||
""".format(variable=variable)
|
||||
for _ in range(5): # 5 attempts max
|
||||
result = world.browser.driver.execute_async_script(dedent(js))
|
||||
try:
|
||||
result = world.browser.driver.execute_async_script(dedent(javascript))
|
||||
except WebDriverException as wde:
|
||||
if "document unloaded while waiting for result" in wde.msg:
|
||||
result = "unload"
|
||||
else:
|
||||
raise
|
||||
if result == "unload":
|
||||
# we ran this on the wrong page. Wait a bit, and try again, when the
|
||||
# browser has loaded the next page.
|
||||
@@ -105,7 +150,7 @@ def wait_for_requirejs(dependencies=None):
|
||||
if dependencies[0] != "jquery":
|
||||
dependencies.insert(0, "jquery")
|
||||
|
||||
js = """
|
||||
javascript = """
|
||||
var callback = arguments[arguments.length - 1];
|
||||
if(window.require) {{
|
||||
requirejs.onError = callback;
|
||||
@@ -126,7 +171,13 @@ def wait_for_requirejs(dependencies=None):
|
||||
}}
|
||||
""".format(deps=json.dumps(dependencies))
|
||||
for _ in range(5): # 5 attempts max
|
||||
result = world.browser.driver.execute_async_script(dedent(js))
|
||||
try:
|
||||
result = world.browser.driver.execute_async_script(dedent(javascript))
|
||||
except WebDriverException as wde:
|
||||
if "document unloaded while waiting for result" in wde.msg:
|
||||
result = "unload"
|
||||
else:
|
||||
raise
|
||||
if result == "unload":
|
||||
# we ran this on the wrong page. Wait a bit, and try again, when the
|
||||
# browser has loaded the next page.
|
||||
@@ -161,7 +212,7 @@ def wait_for_ajax_complete():
|
||||
keeps track of this information, go here:
|
||||
http://stackoverflow.com/questions/3148225/jquery-active-function#3148506
|
||||
"""
|
||||
js = """
|
||||
javascript = """
|
||||
var callback = arguments[arguments.length - 1];
|
||||
if(!window.jQuery) {callback(false);}
|
||||
var intervalID = setInterval(function() {
|
||||
@@ -171,13 +222,13 @@ def wait_for_ajax_complete():
|
||||
}
|
||||
}, 100);
|
||||
"""
|
||||
world.browser.driver.execute_async_script(dedent(js))
|
||||
world.browser.driver.execute_async_script(dedent(javascript))
|
||||
|
||||
|
||||
@world.absorb
|
||||
def visit(url):
|
||||
world.browser.visit(django_url(url))
|
||||
wait_for_requirejs()
|
||||
wait_for_js_to_load()
|
||||
|
||||
|
||||
@world.absorb
|
||||
@@ -246,11 +297,11 @@ def css_has_value(css_selector, value, index=0):
|
||||
|
||||
@world.absorb
|
||||
def wait_for(func, timeout=5):
|
||||
WebDriverWait(
|
||||
driver=world.browser.driver,
|
||||
timeout=timeout,
|
||||
ignored_exceptions=(StaleElementReferenceException)
|
||||
).until(func)
|
||||
WebDriverWait(
|
||||
driver=world.browser.driver,
|
||||
timeout=timeout,
|
||||
ignored_exceptions=(StaleElementReferenceException)
|
||||
).until(func)
|
||||
|
||||
|
||||
@world.absorb
|
||||
@@ -349,14 +400,10 @@ def css_click(css_selector, index=0, wait_time=30):
|
||||
msg="Element {}[{}] is present but not visible".format(css_selector, index)
|
||||
)
|
||||
|
||||
# Sometimes you can't click in the center of the element, as
|
||||
# another element might be on top of it. In this case, try
|
||||
# clicking in the upper left corner.
|
||||
try:
|
||||
return retry_on_exception(lambda: world.css_find(css_selector)[index].click())
|
||||
|
||||
except WebDriverException:
|
||||
return css_click_at(css_selector, index=index)
|
||||
result = retry_on_exception(lambda: world.css_find(css_selector)[index].click())
|
||||
if result:
|
||||
wait_for_js_to_load()
|
||||
return result
|
||||
|
||||
|
||||
@world.absorb
|
||||
@@ -371,23 +418,6 @@ def css_check(css_selector, index=0, wait_time=30):
|
||||
return css_click(css_selector=css_selector, index=index, wait_time=wait_time)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def css_click_at(css_selector, index=0, x_coord=10, y_coord=10, timeout=5):
|
||||
'''
|
||||
A method to click at x,y coordinates of the element
|
||||
rather than in the center of the element
|
||||
'''
|
||||
wait_for_clickable(css_selector, timeout=timeout)
|
||||
assert_true(
|
||||
world.css_visible(css_selector, index=index),
|
||||
msg="Element {}[{}] is present but not visible".format(css_selector, index)
|
||||
)
|
||||
|
||||
element.action_chains.move_to_element_with_offset(element._element, x_coord, y_coord)
|
||||
element.action_chains.click()
|
||||
element.action_chains.perform()
|
||||
|
||||
|
||||
@world.absorb
|
||||
def select_option(name, value, index=0, wait_time=30):
|
||||
'''
|
||||
@@ -417,6 +447,7 @@ def css_fill(css_selector, text, index=0):
|
||||
@world.absorb
|
||||
def click_link(partial_text, index=0):
|
||||
retry_on_exception(lambda: world.browser.find_link_by_partial_text(partial_text)[index].click())
|
||||
wait_for_js_to_load()
|
||||
|
||||
|
||||
@world.absorb
|
||||
|
||||
Reference in New Issue
Block a user